Leaderboard Model & Result Classification
1. Overview
A leaderboard turns the individual race results of a series into a ranked set of standings — who is winning the season, which school has the most points, which team leads the club competition. This page describes the durable model: the schema that stores a leaderboard’s configuration, and the way race results are scored and classified into standings.
This is the foundation. It is deliberately separate from two other concerns:
-
How a specific league is set up — the operational steps to stand up the leaderboards for a real series — lives in Operational Procedures (Western Cape Schools Cycling and WP Cycling Road League).
-
How standings recalculate automatically when results change — the event-driven sync layer — lives in Leaderboard Synchronization. That layer builds on this model; this model stands on its own and is in production use today via manual aggregation.
|
Automated recalculation (Leaderboard Sync) is deferred. Today,
standings are aggregated manually (SQL or admin tooling) by reading |
2. The two-stage pipeline
Producing standings is a two-stage pipeline. The model keeps the two stages cleanly separated — they are configured independently and implemented by two different abstractions.
Race finish positions Stage 1: SCORING Stage 2: AGGREGATION
┌───────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ RaceResult.position│─────▶│ PointsCalculator │───▶│ LeaderboardCalculator │───▶ Standings
│ (1, 2, 3, …) │ │ position → points │ │ group + sum + rank │
└───────────────────┘ └──────────────────────┘ └──────────────────────┘
per-series scoring scale per-LeaderboardType strategy
-
Scoring (
PointsCalculator) converts a finishing position into points using the series' scoring scale. Points are written toRaceResult.pointsat result-import time. -
Aggregation (
LeaderboardCalculator) groups the scored results by the leaderboard’s subject (rider, school, club/team), sums the points, and ranks them into standings. The strategy is chosen by the leaderboard’sLeaderboardType.
Keeping these separate is what lets the same scoring scale feed several different leaderboards, and the same aggregation strategy work with different scoring scales.
3. Schema
3.1. Leaderboard
The configuration entity. One row per leaderboard (e.g. "Individual — Road League 2026").
| Field | Type | Purpose |
|---|---|---|
|
String(100) |
Display name. |
|
|
Aggregation strategy — see Classification & aggregation: LeaderboardType. |
|
|
Optional gender filter; |
|
Integer |
For TOP_SCHOOLS-style boards: how many best results per subject to count. |
|
Integer |
Max entries shown in the standings. |
|
Boolean |
Whether the board is live. |
|
String(255) |
The scoring scale — short code or fully-qualified |
|
|
The series this board scores. |
|
|
Optional sub-classification filter (e.g. PS vs HS) — see Level filtering (PS vs HS). |
|
|
The CustomList whose values the board groups by (e.g. Schools, Clubs). |
|
|
Which series categories feed this board — see below. |
3.2. LeaderboardCategory
Maps a leaderboard to the series categories whose results it aggregates. This replaces the
old approach of hardcoding series_category_id lists in SQL.
| Field | Type | Purpose |
|---|---|---|
|
|
Owning board. |
|
|
A series-level category that feeds the board. |
|
BigDecimal (nullable) |
Optional weighting applied to this category’s points. |
3.3. Series, ResultSet, RaceResult
The leaderboard model sits on top of the existing results schema:
-
Series— the season/competition. ALeaderboardbelongs to exactly one series; itscategories(EventCategoryrows withseriesset) are the series-level categories thatLeaderboardCategoryreferences. -
ResultSet— a set of results for one event/category/state. Carries thestate(ResultSetState), an optionalleaderboardlink, and (for the sync layer) anerrorMessage. -
RaceResult— one rider’s result within a ResultSet. The fields the model depends on:Field Role in the model positionInput to scoring (
PointsCalculator).pointsOutput of scoring; the value aggregation sums.
seqDeterministic ordering key (positions can tie); also a secondary upsert key.
statusResultStatus— FINISHED / DNS / DNF / RELEGATED / DQ / LAPPED. Non-finishers are scored per series rules.person,eventParticipantThe rider; the EP carries the grouping CustomListValues.
custom1,custom2,custom3CustomListValue slots used for grouping (school, club, team — see Classification & aggregation: LeaderboardType).
4. Scoring: PointsCalculator
A PointsCalculator maps a finishing position to points for a given series. It is a pure
function:
public interface PointsCalculator {
int getPoints(Integer position);
default String name() { return getClass().getSimpleName(); }
}
Two scales ship today, each matching a league’s published rulebook:
| Calculator | Scale |
|---|---|
|
Deep non-linear curve: 1→250, 2→200, 3–9 → |
|
Shallow truncated scale, positions 1–12: |
4.1. Selecting the scale
PointsCalculatorFactory resolves a calculator from the value of
Leaderboard.pointsCalculatorClass (or the results-import pointsCalculator query parameter):
by short code, then by fully-qualified class name via the Spring context, falling back to no-arg
reflection. A null/blank/unresolvable value returns the default
(SASchoolCyclingSeriesPointsCalculator).
|
Scoring happens at import time. |
5. Classification & aggregation: LeaderboardType
A LeaderboardCalculator aggregates scored results into standings. The strategy is selected by
the leaderboard’s LeaderboardType. Each calculator filters results to the board’s
categories (and optional gender / levelFilter), groups them by the board’s subject, sums
points, and ranks.
| Type | Code | Aggregation strategy |
|---|---|---|
|
S |
Group by school ( |
|
T |
As SCHOOL, but per subject count only the best |
|
A |
Cross-gender aggregate of top schools — combines the male and female school totals into one ranking. |
|
I |
Group by |
The shared base (AbstractLeaderboardCalculator) applies the common filters: the gender filter
(skip when gender is null) and the level filter (match RaceResult.custom1 against
Leaderboard.levelFilter), and resolves the scoring scale via the factory.
|
Type reuse over schema churn. |
5.1. Level filtering (PS vs HS)
Sub-classifications such as Primary School vs High School are not hardcoded category-ID lists.
The board’s levelFilter references the PS or HS CustomListValue, and LeaderboardCategory
links the board to its series categories. Two boards (one PS, one HS) over the same series
differ only by levelFilter.
6. Worked shape
The two production leagues instantiate this model as follows — full setup steps in their procedures:
| League | Scoring | Boards (LeaderboardType) |
Grouped by |
|---|---|---|---|
|
SCHOOL / TOP_SCHOOLS / ALL_STARS (+ INDIVIDUAL_RIDER), per PS/HS |
School ( |
|
|
INDIVIDUAL_RIDER (Individual), SCHOOL reused (Team/Club), All Rounder |
Person; Club ( |
7. Related Documentation
-
Leaderboard Synchronization — the deferred automated-recalculation layer that builds on this model.
-
Results Import Design — how results (and their points) enter the system.
-
Import Operations Guide — the operator steps for importing results with a chosen calculator.