commit d68d1697aa1244a9af25269e1298ef8f56284a9d Author: Aaron Weiker Date: Mon May 11 00:25:29 2026 -0700 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. diff --git a/DDD-CHECKLIST.md b/DDD-CHECKLIST.md new file mode 100644 index 0000000..8d8c476 --- /dev/null +++ b/DDD-CHECKLIST.md @@ -0,0 +1,165 @@ +# 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..2707066 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# DDD & Event Sourcing Patterns + +Review guidance for domain-driven design and event sourcing. Focus on subtle mistakes models make and patterns they get wrong. + +## Contents + +- `DDD-CHECKLIST.md` - The review checklist covering: + - Aggregate Design (boundaries, invariants, identity) + - Event Sourcing (immutability, naming, content, reconstruction) + - Domain vs Integration Events + - Eventual Consistency + - Projections (read models) + - Snapshotting + - Process Managers / Sagas + - Value Objects + - Anti-Patterns to Flag + +## Why This Exists + +Models know the textbook definitions but often get the nuances wrong: +- Making aggregates too large (data grouping vs consistency boundary) +- Suggesting "fixes" to immutable events +- Mixing domain and integration events +- Expecting synchronous read model updates +- Snapshotting prematurely + +This checklist catches those mistakes. + +## Integration + +```yaml +# In your review workflow +patterns-repo: rodin/ddd-patterns +patterns-files: '.' +``` + +## License + +MIT