# 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