eventmodeling-validating-event-models
Validating Event Models
Core Architectural Rule (Non-Negotiable)
CRITICAL: Every command must have its own minimal state projection (NOT a DDD Aggregate) What DDD calls an "aggregate root" is actually a READ MODEL. Command handlers must NEVER use read models for validation.
This is the primary validation gate. If a model violates this rule, it fails validation immediately.
VIOLATION EXAMPLE (WILL FAIL VALIDATION):
OrderAggregate { orderId, customerId, items[], total, status, paymentId, address, shippedAt, cancelledAt, ... }
↑ This is a READ MODEL, not command state
Used by: ConfirmOrder, ShipOrder, CancelOrder, ApproveReturn
REJECTED: This is DDD aggregate pattern (a read model), not event sourcing command state
CORRECT PATTERN (PASSES VALIDATION):
ConfirmOrderState { status, orderId }
ShipOrderState { status, orderId, paymentId }
CancelOrderState { status, orderId, createdAt }
ApproveReturnState { status, orderId, paymentId }
APPROVED: Each command has minimal state
OrderSummaryView { orderId, customerId, items[], total, status, paymentId, ... }
OK: This is a READ MODEL for UI queries, separate from command state
Purpose
Ensures event-sourced models are complete, correct, and follow pure event sourcing principles (minimal per-command state).
Workflow
When given an event model, perform comprehensive validation:
1. Event Stream Completeness Check
Verify each event stream has:
- Clear stream name (identity)
- At least one event type
- Initial event (what creates the stream)
- State transitions documented
- Deterministic state projection
For each event:
- Uses past tense (Created, Confirmed, etc.)
- Contains only facts (no computed fields)
- All data is immutable
- Unique semantics (no duplicates)
- Includes timestamp/causality info
For each state projection:
- Can be deterministically rebuilt from events
- Replay logic is pure (no side effects)
- Used only for command validation
- Can be safely discarded and regenerated
For each command:
- Clear input parameters
- Validation rules (against state)
- Resulting events specified (or rejection reason)
- Pure logic (no side effects except event appending)
2. Consistency Checks
- Event-Stream Mapping: Every event belongs to exactly one stream
- Command Outcomes: Every command produces events OR documents rejection
- Deterministic Projections: State can only be derived one way from events
- No Side Effects in Projections: Pure state reconstruction logic
- Event Immutability: No event data is ever modified
- Naming Consistency: Are naming patterns consistent?
- Commands: Verb present (CreateOrder, ConfirmPayment)
- Events: Verb past tense (OrderCreated, PaymentConfirmed)
- Streams: Entity type + identity (Order:123, Payment:456)
3. Event Sourcing Principles Compliance
Check against event sourcing fundamentals:
-
Events are Facts: Events describe what happened, not potential futures
- "OrderMayBeConfirmed" → "OrderConfirmed"
- "PaymentPending" (in events) → "PaymentInitiated", "PaymentAuthorized"
-
Events are Immutable: No modification of event data
- "Update OrderCreated event with new total" → "Append OrderTotalCorrected event"
-
Complete Event Data: Events contain all facts needed for state rebuild
- Event: "OrderConfirmed" (missing paymentId) → Event includes paymentId
-
No Computed Fields in Events: Only raw captured facts
- OrderCreated includes "totalTax" (computed) → Includes items + amounts, tax computed in projection
-
Deterministic Projections: Replaying events always produces same state
- Projection uses: for each event, do X
- Projection uses: external API call during replay
-
Stream Identity Clear: Stream name uniquely identifies entity
- Order:order-123, Customer:cust-456
- Orders collection with Order1, Order2
-
State is Derived: Current state always comes from replaying events
- "Load state: replay all events for Order:123"
- "Load state: query database Orders table"
4. Event Flow Validation
- Command → Event Mapping: Clear what each command produces
- No Zombie Commands: Commands that never produce events (read-only OK)
- Event Versioning: How are old events handled if structure changes?
- Compensation Events: How are errors/reversals handled?
- "PaymentFailed event appended when payment declined"
- "Update payment event to status=failed"
- Cross-Stream Communication: Through events or external process?
- "OrderConfirmed event triggers InventoryReservation command"
- "Order directly modifies Inventory state"
5. Role & Actor Attribution Validation
Verify that every command has explicit actor attribution from the Role Catalog:
-
Role Catalog exists: A Role Catalog was defined in Step 1 (eventmodeling-brainstorming-events)
- CRITICAL: No Role Catalog found — commands have no actor attribution
- PASS: Role Catalog with human roles and system actors defined
-
Every command has actor attribution: No command uses generic "User"
- CRITICAL:
CreateOrderattributed to "User" (which user? Customer? Admin? Seller?) - PASS:
CreateOrderattributed to "Customer" (specific role from catalog)
- CRITICAL:
-
Every human role has at least one command path: No orphaned roles
- WARNING: "Support Agent" in Role Catalog but has zero commands
- PASS: Every role has at least one command
-
Every human role has at least one read model: Roles can see system state
- WARNING: "Seller" has no read model — how do they see their data?
- PASS: Every role has at least one view
-
Permission boundaries respected: Commands are only issued by authorized roles
- CRITICAL:
OverrideOrderStatusnot restricted to Support Agent role - PASS: Each command's role matches the permission boundary in the catalog
- CRITICAL:
Validation Verdict: If no Role Catalog exists → REJECT MODEL — Role Catalog is a prerequisite If any command lacks actor attribution → REJECT MODEL — all commands must have explicit role/actor
6. Command State Read Models Validation (CRITICAL)
This is the PRIMARY validation gate. Violations are CRITICAL and must be fixed before approval. Validate that command state read models are minimal and command-specific, not bundled like DDD aggregates.
Note: These are read models semantically categorized as "Command State" (optimized for command validation), as opposed to "Query Models" (optimized for UI queries).
CRITICAL RULE CHECK:
-
Naming Convention Compliance - For automation tooling
- CRITICAL: Interface named
OrderState(ambiguous - command? query?) - PASS:
ConfirmOrderState(clear: command state for ConfirmOrder) - PASS:
ShipOrderStateToDo(clear: planned, needs implementation) - PASS:
OrderQueryModel(clear: query read model)
- CRITICAL: Interface named
-
NO shared state between commands - This is the #1 rule
- CRITICAL: "OrderState used by ConfirmOrder, ShipOrder, CancelOrder, ApproveReturn"
- PASS: "ConfirmOrderState for ConfirmOrder, ShipOrderState for ShipOrder, etc."
-
Each command has its own state interface - CRITICAL: Only one OrderState interface for all commands
- PASS: ConfirmOrderState, ShipOrderState, CancelOrderState, ApproveReturnState
-
No Full Aggregate State: Commands don't load all entity fields upfront
- CRITICAL: "ConfirmOrder loads: { status, items[], total, shipping, paymentId, customer details, ... }"
- PASS: "ConfirmOrder loads only: { status, orderId }"
-
State Only For Validation: No unused fields in state
- "ShipOrderState has exactly: { status, orderId, paymentId } — all needed"
- CRITICAL: "ShipOrderState has: { status, orderId, paymentId, items[], total, address, ... } but only uses status and paymentId"
-
Per-Command State Shapes: Different interfaces for different commands
- ConfirmOrderState: { status, orderId }
- ShipOrderState: { status, orderId, paymentId } (DIFFERENT)
- CancelOrderState: { status, orderId, createdAt }
- CRITICAL: All use the same OrderState
Validation Verdict: If ANY command shares state with another command → REJECT MODEL - CRITICAL VIOLATION
7. Command & State Validation
- State-Based Decisions: Commands decide based on current state only
- Valid State Transitions: Document what state changes are allowed
Draft → Confirmed (ConfirmOrder)
Draft → Cancelled (CancelOrder)
Confirmed → Shipped (ShipOrder)
Confirmed ↛ Draft (invalid)
- Preconditions Clear: When can each command execute?
- "Can only confirm if state is Draft"
- "Can sometimes confirm"
- Error Handling: What happens if validation fails?
- "Reject with ValidationError, no events appended"
- "Append ErrorEvent and continue"
8. Projection Validation
- Read Models Separate from Command State: Read models are rich projections, NOT the command-validation state
- Read Models Optional: Are they needed or just convenience?
- Serve Real Queries: Each read model answers a specific question
- "OrderSummaryView serves 'get orders by customer' query"
- "OrderDetailsView duplicates all data from OrderState"
- Consistent Update: All relevant events update the read model
- Regenerable: Can be rebuilt from events at any time
9. Issues & Recommendations Report
Format findings as:
# Event Model Validation Report: [Domain]
## Issues Found
### Critical (Must Fix)
1. **Stream Root**: Order
**Issue**: No command for order cancellation
**Impact**: Cannot model cancellation requirement
**Fix**: Add CancelOrder command with OrderCancelled event
2. **Event**: PaymentProcessed
**Issue**: Missing payment method in event data
**Impact**: Cannot determine if card declined, etc.
**Fix**: Add paymentMethod, authCode fields
3. **State Design**: Order stream
**Issue**: ConfirmOrder loads full OrderState with items[], shipping, etc.
**Impact**: Violates minimal state principle
**Fix**: ConfirmOrder should load only: { status, orderId }
### Warnings (Should Consider)
1. **Stream Root**: Order
**Issue**: Events contain computed total instead of raw amounts
**Recommendation**: Store line item amounts in event, compute total in read models
**Rationale**: Events are facts, computations belong in projections
2. **ReadModel**: CustomerDashboard
**Issue**: Denormalizes data from 3 stream roots
**Recommendation**: Ensure dashboard is truly for querying, not command validation
**Rationale**: Read models support UI queries, not decision logic
## Completeness Analysis
| Aspect | Status | Details |
|--------|--------|---------|
| Stream Roots | Complete | 5 stream roots identified |
| Commands | 80% | Missing: ReactivateAccount |
| Events | Complete | 18 events cover all transitions |
| State Designs | 70% | Several have unnecessary fields for validation |
| Read Models | Complete | 4 views cover query needs |
## Event Sourcing Compliance
- Immutable Events: 100%
- Minimal Command State: 75%
- Stream Root Clarity: Clear identities
- Event Sourcing Pattern: Follows ES principles
- CQRS Separation: Command state vs Read models clear
## Validation Summary
**Overall Status**: Ready with recommendations
**Blockers for Implementation**: 0 critical issues
**Recommended Fixes**:
1. Add missing OrderCancelled event
2. Move PaymentMethod to its own minimal state projection
3. Document all implicit invariants explicitly
**Ready for Code Generation**: Yes, after implementing recommendations
## Next Steps
1. Review recommendations with domain expert
2. Update model with critical fixes
3. Proceed to code generation
Validation Scoring
Provide a score for each dimension (0-100):
- Completeness: Do all requirements have corresponding model elements?
- Consistency: Are all patterns applied uniformly?
- Correctness: Do business rules match domain expert knowledge?
- Clarity: Are invariants and constraints explicit?
- Projection/Command-State Compliance: Does every command use its own minimal state projection, with no command handler reading from a query/read model for validation?
Overall: Ready for Implementation if all critical issues are resolved.
Common Issues to Flag
| Issue | Pattern | Fix |
|---|---|---|
| Missing cancellation flows | No "Cancelled" events | Add compensation paths |
| Implicit invariants | "Obviously can't do X" | Make invariants explicit |
| Command state too broad | Shared state used by 2+ commands | Split into per-command minimal state projections |
| Orphaned events | Events no one listens to | Link to projections or commands |
| No read models | Commands reading query/read models for validation | Add separate query read models; keep command state minimal |
| Circular dependencies | Projection A depends on B, B on A | Redesign stream boundaries |
Key Principles for Event Sourcing
- Events are the source of truth: Everything else is derived from them
- Immutable event log: Events never change, only appended
- State is a projection: Current state is built by replaying events
- Commands are pure decisions: Validate against state, produce events or reject
- Projections are optional: Can be rebuilt at any time
- Stream per entity: Each entity has one append-only event stream
Success Criteria
Your event model validation is successful when:
- All requirements are captured in events
- Commands clearly trigger events
- Stream roots have clear, minimal boundaries
- Business rules are explicit invariants (not hidden assumptions)
- Read models serve actual query needs (not used by commands)
- Command state is minimal and command-specific (not shared across multiple commands)
- Events are immutable facts (past tense, no computed fields)
- State can be deterministically rebuilt from events
- All command-to-event mappings are documented
- Critical issues are resolved or documented as known limitations
A model is ready for code generation if:
- No critical issues remain
- All command state follows naming convention (e.g.,
[CommandName]State) - No state is shared between different commands
- All events are immutable facts
- All business rules are explicit
- A Role Catalog exists with all human roles and system actors
- Every command has explicit actor attribution from the Role Catalog
Quality Checklist
- All events are immutable facts (past tense)
- No computed fields stored in events
- State projection is deterministic from events
- Commands validate against current state only
- Each command either produces events or rejects (no silent failures)
- Event causality/command-event mapping is clear
- State transitions are documented
- No direct references between streams
- Projections serve specific query needs (or are removed)
- Everything can be rebuilt from the event stream
- Command state naming follows
[CommandName]Stateconvention - No state is shared between different commands
- All command state is minimal (only fields needed for validation)
- Role Catalog exists with human roles and system actors
- Every command attributed to a specific role/actor (no generic "User")
- Every human role has at least one command and one read model
- Permission boundaries from Role Catalog are respected
More from trogonstack/agentskills
diataxis-organize-docs
Reorganize documentation into the Diataxis framework structure. Splits existing docs into tutorials, how-to guides, reference, and explanation sections.
45datadog-design-dashboard
>-
33diataxis-gen-readme
Generate a README introduction following the Diataxis 4-paragraph structure for product documentation.
31nats-design-subject
>-
29gh-enrich-pr-description
Enrich GitHub PR descriptions with root-cause context, related issues/PRs, and CC mentions. Use when creating or editing a PR, when a PR has an empty or sparse description, or when the user asks to improve a PR description.
26ask-question
>-
23