Files
ddd-patterns/DDD-CHECKLIST.md
T
Aaron Weiker d68d1697aa Initial DDD and event sourcing review patterns
Covers:
- Aggregate design (boundaries, invariants, identity)
- Event sourcing (immutability, naming, content)
- Domain vs integration events
- Eventual consistency
- Projections and read models
- Snapshotting (when and how)
- Process managers / sagas
- Value objects
- Common anti-patterns

Focus on subtle mistakes models make, not textbook definitions.
2026-05-11 00:25:29 -07:00

6.4 KiB

DDD & Event Sourcing Review Checklist

Patterns and gotchas for domain-driven design and event sourcing. Focus on what models get subtly wrong.


Aggregate Design

Boundaries

  • Aggregate defines a consistency boundary, not a data grouping
  • Ask: "What MUST be consistent in a single transaction?" - that's the aggregate
  • Prefer smaller aggregates; large aggregates = contention and scaling pain
  • Reference other aggregates by ID, never by embedding the object

Common mistake: Making aggregates too large. An Order aggregate doesn't need to contain Customer - it needs a customer_id.

Invariants

  • Business rules enforced inside the aggregate, not in services
  • Aggregate is always valid after any operation (or rejects the operation)
  • Constructor enforces required fields - no invalid aggregate should exist

Common mistake: Putting validation in application services. If Order requires at least one line item, the Order aggregate enforces this, not OrderService.

Identity

  • Aggregate root has a stable identity (UUID preferred over DB sequence)
  • Identity assigned at creation, never changes
  • Child entities within aggregate may have local IDs (only unique within aggregate)

Event Sourcing

Event Immutability

  • Events are facts that happened - never modify, never delete
  • Bad data? Emit a compensating event (e.g., OrderCorrected, AmountAdjusted)
  • Event schema versioned; old events must remain readable forever

Critical: Models sometimes suggest "fixing" events in the store. This is always wrong. Events are immutable historical facts.

Event Naming

  • Events are past tense: OrderPlaced, PaymentReceived, ItemShipped
  • Commands are imperative: PlaceOrder, ProcessPayment, ShipItem
  • Events describe what happened, not what to do

Event Content

  • Include all data needed to understand the event in isolation
  • Don't reference external state that might change
  • Include actor/causation metadata (who, why, correlation_id)
  • Avoid large payloads - events aren't documents

Common mistake: Storing just IDs and looking up current state. Event should be self-contained: ItemAdded { product_id, name, price, quantity } not just ItemAdded { product_id }.

Aggregate Reconstruction

  • Replay all events to rebuild current state
  • apply/2 is pure: event in, state out, no side effects
  • Handle unknown event types gracefully (forward compatibility)

Domain vs Integration Events

Domain Events

  • Internal to bounded context
  • Rich, contains domain concepts
  • Can evolve with internal refactoring
  • Consumed by projections, process managers within same context

Integration Events

  • Cross bounded context communication
  • Stable contract, versioned schema
  • Minimal data (anti-corruption layer transforms as needed)
  • Published to message bus/event broker

Common mistake: Publishing internal domain events directly to other services. Use an anti-corruption layer to transform to integration events.


Eventual Consistency

Expectations

  • Read models may lag behind writes (milliseconds to seconds typically)
  • UI must handle "command accepted" != "projection updated"
  • Idempotent event handlers (same event delivered twice = same result)

Patterns

  • Optimistic UI: show expected state, reconcile when projection catches up
  • Polling/subscription for read model updates
  • Correlation IDs to track command → event → projection flow

Common mistake: Returning the "updated" read model immediately after a command. The projection might not have processed the event yet.


Projections (Read Models)

Design

  • Projections are disposable - can always rebuild from events
  • One projection per query need (don't share if requirements differ)
  • Denormalized for query performance - no joins at read time
  • Include projection version/position for consistency checks

Rebuilding

  • Support full rebuild from event stream
  • Handle schema migrations via rebuild or versioned projections
  • Track last processed event position for resume

Common mistake: Treating projections as source of truth. If projection is wrong, fix the projection logic and rebuild - don't "fix" the projection data directly.


Snapshotting

When to Snapshot

  • Only when replay performance is a problem (measure first)
  • Typical threshold: 100-1000 events before snapshot becomes worthwhile
  • Snapshot frequency based on write volume and rebuild tolerance

Implementation

  • Snapshot = serialized aggregate state at event N
  • Load snapshot, then replay events after snapshot
  • Snapshot schema must evolve with aggregate (version snapshots)
  • Keep events forever - snapshots are optimization, not replacement

Common mistake: Snapshotting too early (premature optimization) or deleting events after snapshot (data loss).


Process Managers / Sagas

Coordination

  • Long-running processes spanning multiple aggregates/contexts
  • Maintain own state (what step are we on, what's pending)
  • React to events, emit commands
  • Handle failures: compensating actions, retries, timeouts

State Machine

  • Explicit states and transitions
  • Idempotent transitions (receiving same event twice is safe)
  • Timeout handling for stuck processes

Common mistake: Putting multi-aggregate coordination in application services with direct calls. Use process managers for anything that spans consistency boundaries.


Value Objects

  • Immutable (no setters, all state set in constructor)
  • Equality by value, not reference (Money(100, :USD) == Money(100, :USD))
  • Self-validating (invalid value object cannot be constructed)
  • No identity - two VOs with same values are interchangeable

Examples: Money, EmailAddress, DateRange, Coordinates, OrderLineItem

Common mistake: Making value objects mutable or giving them database IDs.


Anti-Patterns to Flag

  • Anemic domain model: Aggregates with only getters/setters, logic in services
  • God aggregate: Everything in one aggregate, massive contention
  • Event sourcing the UI: Storing UI state changes as domain events
  • Synchronous projections: Blocking command until projection updates
  • Shared kernel abuse: Too much shared code between bounded contexts
  • CRUD in disguise: EntityUpdated events that just dump all fields