ADR-0006: Auto-assign prefers latest-used only; UNFIT-latest forces manual assignment
1. Context
The RaceNumber lifecycle work (Feature #473) introduced a DETACH-and-reconcile model (ADR-0005): manual reassignment on one EP triggers a reset of the participant’s same-type future EPs, letting each event’s auto-assign reconcile later. Reconciliation depends on auto-assign making a sensible choice per EP — specifically, carrying the participant’s "same number across the series" continuity wherever that is still correct, without assuming too much when things are in flux.
Three real-world signals complicate this:
-
Participants typically expect the same physical number across a series. WS1b added a
last_usedstamp and a pick-list endpoint to support manual selection at registration with history ordered most-recent-first. Auto-assign should not undermine that by pulling random stock. -
A participant’s number history contains failed or abandoned numbers. Numbers can be
LOST(log entry only; state preserved), physically returned after breakage, or flaggedUNFIT_FOR_SERVICE. The database cannot reliably distinguish "old but fine" from "old because something went wrong". -
The most recent swap is typically the reason a new auto-assign is running. If an admin just flagged the latest number as UNFIT, auto-assign re-issuing an older number would silently undo the admin’s intent — the older number may itself be broken, lost, or missing, and nobody has signalled that.
Without a clear rule, auto-assign tends toward "pick any plausible candidate" — which is user-hostile in the failure case: ghost numbers resurface, damaged numbers get re-issued, and the human who should have made the call is not prompted.
2. Decision
Auto-assign applies the following preference, in order:
-
Latest used, if usable. If the participant has a previous number in the matching subtype pool that is either
IN_STOCKorISSUEDto this same participant, use it.last_usedresolves ties; the most recent win. -
Stock pool fallback. Otherwise, pull next-available from the subtype’s
IN_STOCKpool (ADR-0001 / existingpullNextAvailableprimitive). -
No auto-assign when latest is
UNFIT_FOR_SERVICE. If the participant’s most recent number (in the matching subtype) is flagged UNFIT at the time auto-assign is considering it, auto-assign does not run for this EP. The EP stays withnumber_id = NULLuntil an explicit assignment arrives — admin UI, CSV-suppliedNUMBER/NUMBER_IDon EP import, or result-import number-change.
The rule is deliberately narrow: auto-assign never falls through from "latest unusable" to "second-latest", "third-latest", etc. There is only one candidate from history; if it is not usable, the pool takes over — or, in the UNFIT case, a human must take over.
3. Consequences
3.1. Positive
-
Series continuity is preserved in the common case. A participant who carried number 42 through three events continues to get 42 on event four unless something deliberately changed.
-
Admin intent on UNFIT is never silently reversed. Flagging a number UNFIT is a terminal decision for auto-assign purposes — the system will not re-issue a different number from history without a human.
-
The DETACH-and-reconcile model (ADR-0005) stays honest: the reset signal really does reset things, and reconciliation uses current rules rather than inferring from deep history.
-
Ghost-number resurrection is eliminated. A number that was physically lost two events ago does not reappear on auto-assign because the rule never reaches it.
3.2. Negative
-
NULL EPs that cannot be auto-assigned (UNFIT-latest case) are visible as a validation gap at event finalisation until someone acts. The admin UI surfaces this as a validation error; operationally this is the intended escalation point.
-
Customers whose latest number is flagged UNFIT experience the "no number yet" state until manual assignment completes. Customer notification on UNFIT flagging is a separate requirement (see the communication design journal) so that the customer is informed, not surprised at registration.
-
Reassignment workflows must explicitly pick the number for participants whose latest was UNFIT-flagged. No "the system will sort it" path.
4. Alternatives Considered
4.1. Alternative A: Walk the full history, falling through to older numbers
If latest is UNFIT, try second-latest. If that was LOST, try third-latest. Continue until a usable candidate is found, else fall through to the pool. Rejected because older numbers in the person’s history may themselves be broken, lost, or retired — the system has no reliable signal and a naïve walk would resurrect ghosts. The rule would need to re-implement "is this old number actually safe to issue" logic that does not exist.
4.2. Alternative B: UNFIT-latest falls through to the pool instead of blocking
If the latest is UNFIT, quietly pick a fresh number from the pool — participant gets a new number silently. Rejected because it breaks the series-continuity expectation in a way the participant will not have been told about. An admin who flagged a number UNFIT likely needs to communicate with the customer (see the UNFIT notification requirement). A silent pool pick cuts the admin out of the loop and denies them the chance to coordinate.
4.3. Alternative C: Always pool; no history preference at all
Auto-assign ignores history entirely and always pulls next-available. Simpler but directly contradicts ADR-0002's "same number across the series" intent and defeats the WS1b last_used stamping work.
5. References
-
Design journal:
design-journal/2026-03/number-tag-management.adoc(Session 9 — cascade redesigned) -
Design journal:
design-journal/2026-04/async-signal-sweep.adoc(future-EP auto-assign sweep — honours this ADR) -
Code:
admin-service/src/main/java/za/co/idealogic/event/admin/service/RaceNumberPoolService.java—pullNextAvailable,autoAssignNumbers,autoAssignOneEp -
ADO: Feature #473, US #505, Feature #403, and the sweep US created from that journal entry