EventCategory Cascading Inheritance

Overview

EventCategory rows can inherit shared attributes from a parent definition rather than duplicating every value. A national body’s "Elite Men" category — defined once on the organisational Category — flows down through every series and every event in those series, unless a series or an event explicitly overrides the value at its own layer.

This page describes the inheritance model, the parentage states an event-level row can be in, and the per-field origin surface the API exposes so an operator UI can render "inherited from …​" badges and a "revert to inherited" action.

The three-tier model

Category                 (organisation-wide standard)
   ▲
   │ sourceCategory
   │
EventCategory (series)   (series set, event null)
   ▲
   │ seriesCategory
   │
EventCategory (event)    (event set)
  • Category — defined by a national / sanctioning body. Holds the canonical name, minAge, maxAge, gender for a category like "Elite Men U23".

  • EventCategory (series-level) — points at a Category via sourceCategory, and may override any inherited value (e.g. relax the age range for a development series).

  • EventCategory (event-level) — points at the series-level row via seriesCategory, and may override any inherited value (e.g. open the entry for a single race weekend).

sourceCategory and seriesCategory are not duplicates: sourceCategory reaches up to the organisational Category; seriesCategory reaches up to the series-level EventCategory.

Parentage rule for event-level rows

An event-level EventCategory is in exactly one of three states:

State seriesCategory sourceCategory Meaning

series-linked

set

null

Template reached via the series-level row’s own sourceCategory

standalone-standard

null

set

Event not part of a series; inherits directly from the national Category

ad-hoc

null

null

Event-only custom category with no upstream template

Setting both seriesCategory and sourceCategory on the same event-level row is rejected by:

  • the bean-validation @AssertTrue isValidParentage() on the entity, and

  • the ec_event_parent_exclusive CHECK constraint on event_category in the database.

Series-level rows are not subject to this exclusivity — they always have sourceCategory set (or null if ad-hoc at series level), and never have seriesCategory.

Cascade depth per field

Field Layers Notes

name

3-layer

Cascades through Category. The label shown to participants.

minAge / maxAge

3-layer

Age boundaries, often defined nationally.

gender

3-layer

Category.getGender() defaults null → UNKNOWN; the origin surface reports CATEGORY whenever a Category is in the chain.

sourceCategory (FK)

2-layer

Event-level row inherits the pointer from seriesCategory.sourceCategory when its own column is null. This is the linchpin that makes the 3-layer cascade above work for series-linked event rows.

product

2-layer

Commercial. Series default; events override for one-off pricing.

entryCategory / raceCategory

2-layer

Boolean policy flags. Null means "inherit"; explicit true / false is an override.

defaultNumberSubType / defaultNumberSubType2

2-layer

Number subtype pools (WS2 / WS3). Null still falls through to Event.numberType at the service layer.

"Elite Men" worked example

National body publishes a Category with name = "Elite Men", minAge = 19, gender = MALE.

Series A is built on it: a series-level EventCategory with sourceCategory = "Elite Men" and no local overrides.

Each of the 8 events in Series A has an event-level EventCategory with seriesCategory = (Series A’s Elite Men row) and no local overrides.

Read on…​ minAge minAgeOrigin Why

Category "Elite Men"

19

LOCAL (on Category — represented as the column itself)

Defined on the organisation

Series-level "Elite Men" row

19

CATEGORY

Inherited from sourceCategory

Event-level "Elite Men" row

19

CATEGORY

Cascaded through seriesCategory.sourceCategory

Series B wants U21 athletes too — it overrides on the series-level row: minAge = 16. Event-level rows that don’t override locally now report:

Read on…​ minAge minAgeOrigin Why

Series-level "Elite Men" row (Series B)

16

LOCAL

Override at the series

Event-level "Elite Men" row in Series B

16

SERIES

Inherited from `seriesCategory’s local override

A single event in Series B raises the floor back to 19 by setting minAge = 19 on its event-level row directly. That row reports minAgeOrigin = LOCAL and minAgeLocal = 19.

Origin surface on the API

EventCategoryDTO exposes three properties per inheritable field:

Property Read / write Meaning

xxx

read + write

Effective (cascaded) value — what the system uses for validation, display, and scoring. On write, sets the local column.

xxxLocal

read-only

The raw column value on this row. null = "inherited, not overridden".

xxxOrigin

read-only

LOCAL | SERIES | CATEGORY | NONE — where the effective value came from. UI uses this to render the inheritance badge.

The current API keeps xxx writable for backwards compatibility — clients setting minAge = 12 write 12 to the local column (override); setting minAge = null clears the override (re-inherits). A future revision may flip xxxLocal to be the writable surface and deprecate xxx-on-write.

Operator rules of thumb

  • To change a value for every event in a series — set it on the series-level row. Don’t edit each event row individually.

  • To revert an event-level override and re-inherit — clear the value (null) on the event-level row. The DTO’s xxxLocal will then be null and xxxOrigin will report whichever upstream layer the value comes from.

  • "Standard" eligibilityisOrganisationStandard() and isSeriesStandard() (used to decide records / leaderboard eligibility) require the row to have no inheritable overrides. Setting any of minAge, maxAge, gender, product, entryCategory, raceCategory, defaultNumberSubType, defaultNumberSubType2 locally disqualifies the row.

  • name is intentionally excluded from the "standard" check — a custom event label doesn’t disqualify the row from comparison.

  • Ad-hoc rows — when both seriesCategory and sourceCategory are null on an event-level row, the isAdHoc() helper returns true. Ad-hoc categories never inherit anything; their fields stand alone.

See also