GRIDIRON LEGEND
Engineering Specification — Part 4: Backend Architecture
Supersedes the earlier draft. Implements Part 3's contracts against Part 2's aggregates, inside Part 1's boundaries. No production code. No frameworks named unless the pattern is meaningless without one. No new gameplay mechanics.
1. DOMAIN MODULE ORGANIZATION
Seven domain modules plus a Shared Kernel (Blueprint 2.5), each domain module owning its commands, queries, handlers, repositories, services, and events exclusively — restated here as the backend's organizing unit, since everything else in this document is scoped by domain first, technical concern second.
2. FOLDER / PROJECT STRUCTURE
/src
/shared-kernel
/event-bus /state-machine-framework /resolution-layer /league-structure
/domains
/league /commands /queries /handlers /repositories /services /events
/franchise-economy /commands /queries /handlers /repositories /services /events
/roster-contract /commands /queries /handlers /repositories /services /events
/simulation /commands /queries /handlers /repositories /services /events
/ai /commands /queries /handlers /repositories /services /events
/story-history /queries /handlers /repositories /events -- no /commands folder,
not empty, absent
/online /commands /queries /handlers /repositories /services /events
/read-models -- not nested under any domain; subscribes to Event Bus like
any other consumer, never writes back
/franchise-dashboard /league-history-search /coaching-tree /standings /draft-board
/application
/dependency-injection /scheduler /background-jobs /configuration /logging
/infrastructure
/persistence -- repository implementations
/event-bus-transport
/observability -- logging sinks, metrics, tracing
The absent /commands folder under story-history remains the single most important line in this structure — a folder that was never created cannot be misconfigured, which is a stronger guarantee than any runtime check below can offer.
3. DEPENDENCY INJECTION STRATEGY
Domain-scoped DI modules; cross-domain repository injection is not wireable, not merely discouraged — a command handler in one domain requesting another domain's repository fails at container-build time.
SharedKernelModule (singleton, visible to all domains):
→ EventBus, StateMachineFrameworkService, ResolutionLayerService, LeagueStructureRepo
FranchiseEconomyDomainModule:
→ exports (public): FranchiseQueryService, CapQueryService
→ internal (not exported, unreachable from other modules' containers):
OwnerRepository, CapLedgerRepository, FinancialLedgerRepository,
StadiumRepository, FacilityRepository, FanLoyaltyRepository,
FrontOfficeReputationRepository, CoachRepository, CoordinatorRepository,
MedicalStaffRepository
AI Domain's registration depends on every domain's command-handling entry points (not their repositories) — identical to a human-triggered command's dependency graph, wired the same way rather than specially, resolving the tension Part 1 Section 2.5 already named between "AI Domain is a bounded context" and "AI and humans use the same command path."
4. COMMAND HANDLERS
CommandHandler<TCommand>:
handle(command: TCommand, issuedBy: ActorContext) → CommandResultDTO:
1. authorize(issuedBy, command) → AuthorizationError
2. validateSchema(command.payload) → ValidationError
3. validateBusinessRules(command, domainState) → DomainRuleViolation
4. checkAggregateState(command.targetAggregateId) → AggregateStateError
5. repository.save(mutatedAggregate) → ConcurrencyConflict possible
6. eventBus.publish(resultingEvents)
Any failure at 1-4 → audit_log entry, structured error returned (Section 15),
no state change, no event published.
No handler branches on issuedBy.actorType anywhere in steps 1–6 — a lint-rule-worthy check against Blueprint Principle 2, not a suggestion.
5. QUERY HANDLERS
QueryHandler<TQuery>:
handle(query: TQuery) → TOutputDTO:
- default: read model (eventually consistent)
- explicitly whitelisted exception (e.g. GetCapSpaceSummary): direct
write-side repository read, for zero-staleness-tolerance validation
queries only — adding to this whitelist requires the same justification
already on record for the existing exception, never developer convenience
6. EVENT HANDLERS
Wired directly from Part 3 Section 10's subscription map. Story/History Domain's handlers receive a read-only repository binding — no writable repository is ever injected into that domain's container scope (Section 3), so there is no code path by which a Story/History handler could call repository.save() on anything, regardless of what a future engineer attempts to write.
7. WORKER ARCHITECTURE
Two parallel-worker pools, one concurrency pattern (Blueprint Section 9's gather-then-apply-deterministically), reused rather than duplicated:
- AI Decision Workers — one per AI-controlled franchise per decision cycle, computing from a frozen state view, outputs collected and applied in deterministic (franchise-ID) order through the normal Command Handler pipeline.
- Simulation Workers — games within a week are mutually independent until standings/injuries update, so a week's games resolve in parallel workers, results gathered, standings/injuries committed deterministically afterward. This extension wasn't in Part 1 (scoped there to the AI-parallelism risk specifically) but follows the identical pattern, not a new one.
8. SIMULATION SCHEDULER
Two distinct schedulers, matching Part 2 Section 8.7's Simulation-vs-Real time distinction — conflating them would be the same class of bug the simulation_sequence fix (Part 2, Section 4) already corrected once, now at the scheduling layer.
- Simulation Scheduler: advances discrete simulation phases (week → week, season → offseason → next season). Triggered by an explicit player command (
SimulateWeek,SimulateSeason) or, in auto-sim/testing contexts, by theAutoSimTestHarness— never by a real-world clock. This is what drives the Simulation Loop (Part 1, Section 6). - Real-Time Scheduler: manages wall-clock-bound events — multiplayer bidding window open/close (Finding 9), turn timers, media rights cycle telegraphing lead time. Operates entirely on Real Timestamps and never influences simulation outcome ordering, only when a simulation-affecting command becomes eligible to be issued.
9. BACKGROUND JOBS
| Job | Trigger | Purpose |
|---|---|---|
AutoSimTestHarness runs |
Scheduled (pre-launch gate, periodic post-launch) | Strategy-dominance detection, AI difficulty calibration (Finding 3), long-horizon economic validation (Finding 8) |
| Read-model rebuild | On-demand (schema version change) or automatic at startup if a version mismatch is detected | Cheapest migration category (Part 2, Section 7) — read models are disposable projections of event_log, never a source of truth |
CapLoopholeMonitorService audit pass |
Scheduled, tied to Module 33's tuning cycle | Cross-system emergent exploit detection (Bible 32.2) |
TankingDetectorService / RosterInvestmentDetectorService evaluation |
End of each season, after Resolution Layer completes | Reads audit_log and event_log for the season just closed |
CollusionDetectorService evaluation |
Periodic, multiplayer leagues only | Statistical anomaly detection over trade-value ratios (Finding 5) |
10. AI EXECUTION PIPELINE
The full path from "an AI-controlled franchise exists" to "a command is applied," stated end-to-end since Section 7 covered the worker mechanics but not the decision-production logic itself:
1. DecisionCycleSchedulerService invokes each AI-controlled franchise's worker at
the appropriate cadence phase (offseason / in-season / draft-day / trade-deadline
— Bible 24.2), never more or less frequently than a human GM's own opportunities
to act at that phase.
2. Worker reads: AIPersonality, CompetenceTier (per relevant staff role), and a
frozen snapshot of current domain state via that domain's QueryService — never
a direct repository read, same rule as any other cross-domain access.
3. Decision-policy logic evaluates the frozen state against personality/competence
and produces zero or more Command DTOs (Part 3, Section 1) — the SAME DTO shapes
a human's UI would produce, not an AI-specific variant.
4. Produced commands respect the Decision Taxonomy identically to a human's:
AI does not skip Strategic-tier decisions or resolve them via a shortcut —
it "decides" them through the same command path, evaluated at the cadence
phase where that tier is normally addressed.
5. Commands from all workers in this cycle are collected (Section 7), ordered
deterministically, and each is submitted to the Command Handler pipeline
(Section 4) exactly as a human-submitted command would be — including full
authorization, validation, and audit_log treatment.
AI never bypasses step 5's pipeline under any circumstance, including performance pressure — restating Blueprint Principle 9 ("performance optimizations may not change outcomes") at the one point in the system most likely to tempt a shortcut.
11. REPOSITORY IMPLEMENTATION STRATEGY
Framework-agnostic by design, since naming a specific ORM or query builder here would be exactly the kind of premature framework choice this phase is instructed to avoid — the pattern is specified, not the library.
Repository<TAggregate> implementation shape:
- backed by a Unit-of-Work scoped to exactly one aggregate transaction
(Section 12)
- aggregate reconstruction from persisted row(s) via a Data Mapper — the
aggregate's in-memory shape and its storage shape are never assumed
identical, even where they happen to look similar today
- getById() hydrates a full aggregate; save() diffs against the loaded
version and writes only changed fields, checking the version column
(Section 13) as part of the same write
- softDelete() (Part 2, Section 8.5) is the only deletion path any
repository implements — there is no hardDelete() method to accidentally
call, at the interface level (Part 3, Section 6), not just by convention
A framework choice is genuinely required at one specific point, flagged rather than silently made: something has to execute SQL and manage connections. That choice is deferred to actual implementation time, not this document — what's specified here is the shape the chosen tool must support (Unit-of-Work, Data Mapper, optimistic version checks), which any reasonable persistence library can satisfy.
12. TRANSACTION BOUNDARIES
No database transaction ever spans more than one aggregate root. This is a hard rule, not a default — restating Section 1's aggregate decomposition (Part 2, Section 1) at the transaction level, since that decomposition only actually delivers its parallelism benefit if transactions respect it.
Multi-aggregate consistency is achieved without multi-aggregate transactions, via patterns already established elsewhere in this document set rather than a new one invented here:
- End-of-Season Resolution Layer (Part 1, Section 4): reads frozen snapshots, computes independently, commits each metric's aggregate separately within the same logical pass.
- Trade (touching two franchises' rosters): the
Tradeaggregate itself (proposal, acceptance, status) is one transaction. Asset transfer — updating each affectedPlayer/DraftPickaggregate'sfranchise_id— is a separate, subsequent transaction per asset, coordinated by a domain service acting as a lightweight saga: if an asset-transfer step fails partway through, the saga's compensating action reverts theTradeaggregate to a failed state and logs the partial-failure condition toaudit_log, rather than leaving the system in a silently inconsistent state.
13. OPTIMISTIC CONCURRENCY
Every aggregate row carries a monotonically-incrementing aggregate_version column (an addition to Part 2's schema, made explicit here since Part 2 didn't specify a concurrency-control column). repository.save() includes the version read at load time in its write condition; if the stored version has advanced since the read (another worker or request modified the same aggregate first), the write fails with ConcurrencyConflict rather than silently overwriting.
This matters most exactly where parallelism is heaviest: AI Decision Workers (Sections 7/10) and concurrent multiplayer submissions are the two places overlapping writes to the same aggregate are actually likely — two AI trade proposals touching the same player, for instance. ConcurrencyConflict handling (retry policy) is specified in Section 18 (Error Propagation) rather than here, since the concern here is detection, not response.
14. LOGGING
Logging is operational/debugging infrastructure — it is explicitly not a substitute for event_log or audit_log (Part 2, Sections 4 and 4.5), and the three must not be conflated. event_log is permanent gameplay truth; audit_log is permanent operational truth for exploit/collusion investigation; application logs are ephemeral, rotated, and exist for engineers debugging the system itself, not for any in-game or investigative purpose.
Every command execution carries a correlation_id, generated at step 1 of the
Command Handler pipeline (Section 4), threaded through every log line the
handler emits AND stored in the corresponding audit_log row — this is what
lets an engineer trace "what actually happened for this one request" across
logs without those logs themselves needing to be permanent or queryable at
the same integrity level as audit_log.
Log levels: standard severity tiers (debug/info/warn/error), environment-
configured (Section 15) — verbose in development, restrained in production,
never gameplay-affecting regardless of level, since logging must never be on
a code path that influences simulation outcomes.
15. CONFIGURATION
Two categories of configuration, requiring different change-control processes — conflating them would let a gameplay-balance change slip through as if it were an infrastructure tweak.
- Infrastructure configuration (DB connection details, log levels, environment flags): standard environment-scoped config, changeable by ops engineers through normal deployment processes.
- Tuning configuration — the
TuningVariableRegistry(Bible 33.2): cap collar percentage, realignment cadence, RiskState severity thresholds, and every other value the Bible itself marked "config, pending Module 33 balancing." These are gameplay-affecting values and must go through the same rigor as any other Bible-governed decision — version-controlled, tied toAutoSimTestHarnessvalidation before a change ships, never editable as a casual runtime environment variable.
16. INTERNAL SERVICE COMMUNICATION
Within a domain: direct method calls, same process, no indirection needed. Across domains: exclusively Event Bus (async, for anything that isn't a direct read) or QueryService interfaces (sync, read-only) — never a direct call into another domain's service or repository. This consolidates a rule that's been stated piecemeal across Sections 3, 6, and 11 into one explicit place.
17. PERFORMANCE CONSIDERATIONS
Distinct from Part 1's gameplay-facing Performance Budgets (game simulation speed, AI cycle time) — this section covers backend-internal concerns that serve those budgets without being identical to them:
- Connection pooling sized to the AI Worker pool's parallelism (Section 7) — up to ~40 concurrent workers means the pool must not become the bottleneck the parallelism was designed to avoid elsewhere.
- Read-model projection lag is itself a budget: Part 1's <1s League History query target is only achievable if the projection pipeline (Section 6's Event Bus subscribers building read models) keeps pace with
event_loggrowth. - Caching for frequently-read, rarely-changed data — League/Conference/Division structure,
TuningVariableRegistrycurrent values. - N+1 query avoidance in repository implementations, particularly for any read path that hydrates multiple related aggregates (a franchise dashboard touching seven separate aggregate roots per Part 2, Section 1).
18. ERROR PROPAGATION
Part 3 Section 9's error taxonomy propagates from repository/service through the Command Handler to the caller without being swallowed at any layer:
ConcurrencyConflict (Section 13) → automatic bounded retry (recommend 3 attempts)
at the Command Handler level BEFORE surfacing to the caller — transient
contention among parallel AI workers is expected and often self-resolves on
retry; only exhausted-retry conflicts surface upward.
ValidationError, AuthorizationError, DomainRuleViolation, AggregateStateError →
never retried automatically — surfaced immediately, with the structured
error shape from Part 3 Section 9 intact, never collapsed into a generic
message before it reaches the caller.
SystemError → always logged (Section 14) with full context BEFORE propagating,
never silently caught-and-ignored anywhere in the call chain.
19. TESTING HOOKS
Distinct from Part 1 Section 12's testing strategy (what to test) — this specifies the backend hooks that make that strategy executable:
- Repository swappability via DI (Section 3): test doubles substitute for real repository implementations without touching domain or handler code.
- Explicit seed injection point for the Simulation Core, satisfying Blueprint Section 6's determinism requirement operationally: a test harness supplies a seed and a starting snapshot, captures the resulting event sequence, and asserts on it.
- Event log replay hooks: a test instance can be seeded with a captured
event_logsequence and assert on the resulting read-model state after projection.
End of Engineering Specification, Part 4 (Backend Architecture), complete version. Supersedes the earlier partial draft. Part 5 (Frontend Architecture) follows, consuming only the DTOs and read models this document's contracts (Part 3) already define.