Event-Driven System

Concise architecture contract for game-domain write paths.

Purpose

The game service is event-sourced:

  • commands express intent
  • deciders produce decisions
  • accepted decisions emit immutable events
  • projections are derived by applying events in sequence order

The event journal is authoritative. Projection state is derived and rebuildable.

Core lifecycle

request -> command -> decider -> event append -> projection apply

Required outcomes:

  1. Mutations are represented by emitted events, not direct projection writes.
  2. Rejected commands emit no domain mutation event.
  3. Projection state can be reconstructed from the event journal.

Write-path sequence

sequenceDiagram
    autonumber
    participant H as Handler
    participant E as Domain Engine
    participant D as Decider
    participant J as Event Store
    participant P as Projection Applier

    H->>E: Execute(command)
    E->>D: Decide(command, state)
    D-->>E: decision (events or rejection)
    E->>J: Append(events)
    E->>P: Apply(events by intent)
    E-->>H: result

Non-negotiable invariants

  1. Event append is the source of truth for accepted mutations.
  2. Request handlers must not write projection stores directly.
  3. Core command paths must not emit system-owned event types.
  4. System command paths must not emit core-owned event types.
  5. System-owned envelopes must carry system_id and system_version.
  6. Event payloads must remain replay-safe and deterministic.
  7. Replay and projection behavior must be explicit through event intent.

Event intent model

Every event definition declares intent:

  • IntentProjectionAndReplay: fold + project (default for most events)
  • IntentReplayOnly: fold only, no projection write
  • IntentAuditOnly: journal-only, neither fold nor projection

Intent defines required handlers and startup validation obligations.

Registration and validation contract

Startup validation must fail fast when contracts are inconsistent.

Minimum guarantees:

  • every replay-relevant event has fold coverage
  • every projection-relevant event has adapter coverage
  • audit-only events do not accumulate dead fold handlers
  • unknown system module/adapter routing is treated as a startup or replay error

Event namespacing convention

Core events and system events occupy distinct namespaces:

  • Core events use flat <aggregate>.<action> names (e.g. campaign.created, session.started).
  • System events are prefixed with sys.<system_id>.* (e.g. sys.daggerheart.character.created).

System commands must carry system_id and system_version in the command envelope. The command registry validates that the system_id matches the sys.<system_id> prefix of the command type at registration time (ErrSystemTypeNamespaceMismatch). Core commands must not carry system metadata, and system commands must not emit core-owned event types. This namespace boundary is enforced at both registration and execution time.

System extension rules

When adding mechanics or systems:

  1. Register command and event definitions in the owning module.
  2. Keep command/event ownership boundaries explicit (core vs sys.<system_id>.*).
  3. Route all mutating paths through shared execute-and-apply orchestration.
  4. Add fold and adapter coverage for new projection/replay intents.
  5. Update generated catalogs in docs/events/.

Consistency model

Projection state is eventually consistent with the event journal, even in the inline-apply path.

The inline-apply write sequence is:

  1. Domain engine appends events to the journal (SQLite transaction commits).
  2. Handler loops over emitted events and calls Apply for each one (separate SQLite transactions).

Between steps 1 and 2, concurrent readers see durable events but stale projection state. This window is typically sub-millisecond under normal load. It is intentional: the event journal is authoritative and projections are derived state that can always be rebuilt.

The outbox-only apply mode has a wider consistency window (seconds, governed by the outbox worker poll interval) but identical semantics — events are always durable before projections update.

Deep references

Use these pages for detailed mechanics and troubleshooting: