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.
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user