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.
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/2is 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:
EntityUpdatedevents that just dump all fields