eventmodeling-designing-event-models

Installation
SKILL.md

Designing Event Models

Interview Phase (Optional)

When to Interview: Skip if the user has specified: stream identity strategy, command-specific state needs, and read model requirements. Interview when stream boundaries or state design are unclear.

Interview Strategy: Establish stream identity and per-command state boundaries before designing. Ambiguous boundaries are the primary cause of the DDD aggregate anti-pattern appearing in event-sourced models.

Critical Questions

  1. Stream Identity (Impact: Determines how events are grouped into streams)

    • Question: "What's the entity that owns events? (e.g., orderId, reviewId, customerId) What's the lifetime? Single transaction or years?"
    • Why it matters: Wrong stream identity causes design problems; correct identity keeps streams appropriately scoped
    • Follow-up triggers: If multiple candidates → ask "Which entity's identity would you use to load events for a single command decision?"
  2. Minimal State vs Bundled State (Impact: Prevents DDD aggregate anti-pattern)

    • Question: "Will different commands need different state? Or does every command need the same full state?"
    • Why it matters: Each command should have minimal, command-specific state—not bundled DDD aggregates
    • Follow-up triggers: If "same full state" → walk through two commands and ask what each actually reads during validation

Interview Flow

Conditional Entry:

If user has provided:
  - Stream identity (which entity ID anchors the stream)
  - AND at least two commands with explicitly different state needs documented
  - AND read model requirements (what queries the UI or processors make)

Then: Skip interview, proceed directly to design

Else: Conduct interview

Phase 1: Stream Boundaries (Question 1)

  • Confirm which entity anchors the stream
  • Establish stream lifetime expectations
  • Identify whether multiple candidate roots exist and resolve them

Phase 2: State Design (Question 2)

  • Confirm per-command state isolation
  • Identify whether DDD aggregate thinking is present upfront
  • Establish minimal state shapes for at least two commands

Capturing Interview Findings

Append findings to the project's event modeling file:

File: .trogonai/interviews/[project-name]/EVENTMODELING.md

Use Write tool to add/update this section:

## Designing Event Models (eventmodeling-designing-event-models)

### Stream Identity
[From Q1: Which entity? What identity key? Lifetime?]

### Per-Command State Decisions
[From Q2: Which commands need different state? Initial minimal state shapes?]

### Design Decisions
- Stream root: [entity name] identified by [id field]
- State isolation: [confirmed / DDD pattern caught and corrected]

Update Interview Trail:

| Design | eventmodeling-designing-event-models | Done | Stream identity, per-command state shapes |

Core Architectural Rule

NEVER use a "DDD Aggregate Root" (bundled state) for command validation Every command handler has its own minimal state projection. What DDD calls an "aggregate root" is actually a read model, not command-validation state.

 WRONG: Using DDD Aggregate as command state
OrderAggregate { orderId, customerId, items[], total, status, paymentId, address, shippedAt, cancelledAt, ... }
    ↑ This is a READ MODEL, not command state
    ↓ NEVER use for command validation
  handleConfirmOrder(OrderAggregate)
  handleShipOrder(OrderAggregate)
  handleCancelOrder(OrderAggregate)

 CORRECT: Minimal state per command
ConfirmOrderState { status, orderId }
  handleConfirmOrder(ConfirmOrderState)

ShipOrderState { status, orderId, paymentId }
  handleShipOrder(ShipOrderState)

CancelOrderState { status, orderId, createdAt }
  handleCancelOrder(CancelOrderState)

OrderSummaryView { orderId, customerId, items[], total, status, paymentId, ... }
    ↑ This is for UI queries, NOT command validation

Purpose

Converts domain analysis into the event sourcing architecture pattern:

UI/ProcessorCommand → [Command State Read Model] → EventQuery Read Models - UI/Processor: Entry points that trigger intent

  • Command: Intent data (can be rejected)
  • Stream Root: Logical grouping of immutable events (NOT a DDD aggregate bundle)
  • Command State Read Model: Minimal projection derived from events, optimized for this specific command's validation. Each command gets its own read model interface. Different commands = different shapes. (Categorized as "Command State" for semantic clarity)
  • Event: Result of successful command (immutable fact: command data + implicit context)
  • Query Read Models: Rich projections of events optimized for UI/Processor queries. Separate from command state. (Categorized as "Query Models" for semantic clarity)

Workflow

Given a domain analysis, design a complete event-sourced model:

1. Design Event Streams

Events are the immutable source of truth. Each stream holds facts about one entity:

  • Stream Name: Entity type + identity (Order:order-123)
  • Event Type: What changed? (past tense: Created, Confirmed, Shipped)
  • Event Data: Combines command input + implicit stream state facts
  • Causality: Triggered by which command?

Format:

Stream: Order:order-123

Events (chronological):
1. OrderCreated
   Triggered by: CreateOrder command
   Data: customerId, items[], total, shippingAddress, createdAt
   (from command: customerId, items[], shippingAddress)
   (implicit: total calculated from items)

2. OrderConfirmed
   Triggered by: ConfirmOrder command
   Data: paymentId, confirmedAt
   (from command: paymentId)
   (implicit: orderId from stream, previous status verified)

3. OrderShipped
   Triggered by: ShipOrder command
   Data: shipmentId, shippedAt
   (from command: shipmentId)
   (implicit: orderId, confirmed status verified)

Key Rules:

  • Events are immutable facts from successful commands
  • Event data = command input + implicit stream state context
  • Stream identity is explicit (Order:order-123)
  • Event order matters for state reconstruction
  • Never modify or delete events
  • Events only exist if command succeeded

2. Design Command State Read Models (Minimal Per-Command)

Critical Rule: Each command must have its own read model (command state). NEVER share read models between commands. Naming Convention (for Automation):

  • [CommandName]State = Implemented command state read model
  • [CommandName]StateToDo = Planned command state read model (marked for implementation)

Examples:

  • PublishReviewState = implemented
  • EditReviewStateToDo = planned, needs implementation
  • SellerRespondState = implemented

Semantic Categorization: These are read models, but categorized as "Command State" based on their purpose (command validation, not UI queries).

Command state read models are derived from events and minimal:

  • Read only events needed for a specific command's decision
  • Build state by replaying only relevant events
  • ENFORCEMENT: Different commands = different read model interfaces. Period.
  • Each command handler defines what state projection it needs (and ONLY what it needs)
  • Projection can be regenerated from events at any time

Example for Order stream with separate command state read model for EACH command:

## ConfirmOrder Command (IMPLEMENTED)
State interface: ConfirmOrderState { status, orderId }
Builder: buildConfirmOrderState(events)
Naming: [CommandName]State = implemented
- OrderCreated event → Set status='Draft'
- OrderConfirmed event → Set status='Confirmed'
(SKIP: items, total, shipping - not needed for this command)

## ShipOrder Command (IMPLEMENTED)
State interface: ShipOrderState { status, orderId, paymentId }
Builder: buildShipOrderState(events)
Naming: [CommandName]State = implemented
(DIFFERENT from ConfirmOrderState)
- OrderCreated event → (skip)
- OrderConfirmed event → Set status='Confirmed', set paymentId
- OrderShipped event → Set status='Shipped'

## CancelOrder Command (PLANNED - NOT IMPLEMENTED)
State interface: CancelOrderStateToDo { status, orderId, createdAt }
Builder: buildCancelOrderStateToDo(events) [STUB - TODO]
Naming: [CommandName]StateToDo = planned, needs implementation
(DIFFERENT from both above)
- OrderCreated event → Set status='Draft', createdAt
- OrderCancelled event → Set status='Cancelled'

Enforcement Rule:

  • ConfirmOrderState used ONLY by handleConfirmOrder
  • ShipOrderState used ONLY by handleShipOrder
  • NEVER share state between commands
  • NEVER create a single "OrderState" for all Order commands

This is NOT a full aggregate state bundle—it's minimal, command-specific state access.

3. Design Commands

Commands are intent data from UI or Processor:

  • Represent what user/system wants to do
  • Can be rejected (validation failure)
  • Only UI or Processor can issue commands
  • Load current stream state for validation
  • Produce events if valid, or reject if invalid

Format:

Command: ConfirmOrder
Source: UI or Processor (only these can issue)
Input: orderId, paymentId

Processing:
    1. Load current state from Order:orderId stream
    2. Validate preconditions:
       - state.status === 'Draft' (reject: already confirmed)
       - paymentId is valid (reject: invalid payment)
    3. If all valid:
       - Produce: OrderConfirmed event
         - Data: paymentId, confirmedAt
         - Implicit: orderId (from stream), previous status (from state)
    4. If any validation fails:
       - Reject: return error (no event created)

Outcomes:
     Success: OrderConfirmed event appended to stream
     Rejection: Error returned, no event created

Key Rules:

  • Only UI or Processor can issue commands (entry points)
  • One command per UI/Processor action
  • Commands validate against stream state
  • Successful command → Event(s) created
  • Failed validation → Command rejected, no event
  • Commands are synchronous decision logic (pure)

4. Design Read Models

Read models are projections of events for UI/Processor queries:

  • Built from events (only source is events)
  • Optimized for specific query patterns
  • Consumed by UI or Processor (for display/decision)
  • Can be regenerated from events anytime

Format:

ReadModel: OrderSummaryView
Purpose: UI displays customer order list, Processor checks order status

Subscribed to events:
  - OrderCreated
  - OrderConfirmed
  - OrderShipped
  - OrderCancelled

Data (optimized for queries):
  {
    orderId: string
    customerId: string
    total: number
    status: string
    createdAt: Date
    confirmedAt?: Date
    shippedAt?: Date
  }

Update from events:
  - OrderCreated → Insert row (id, customer, total, status='Draft')
  - OrderConfirmed → Update status='Confirmed', set confirmedAt
  - OrderShipped → Update status='Shipped', set shippedAt
  - OrderCancelled → Update status='Cancelled'

Consumed by:
  - UI: displays list of orders
  - Processor: checks if order can be shipped

5. Document Event Causality

Show how events relate to each other:

Command Flow:
CreateOrder command
    → OrderCreated event
       ↓ (may trigger external process)
ConfirmOrder command (reads OrderCreated state)
    → OrderConfirmed event
       ↓ (may trigger)
ShipOrder command (reads OrderCreated + OrderConfirmed state)
    → OrderShipped event

6. Document State Transitions

Show valid state transitions:

Order Stream State Transitions:

Initial state: (empty stream)
CreateOrder → OrderCreated
State: Draft

Draft state:
  → ConfirmOrder → OrderConfirmed → State: Confirmed
  → CancelOrder → OrderCancelled → State: Cancelled

Confirmed state:
  → ShipOrder → OrderShipped → State: Shipped
  → CancelOrder (rejected - already confirmed)

Shipped state:
  → No more transitions allowed

Output Format

Present complete model as:

# Event Model: [Domain]

## Event Streams

### Stream: Order

**Identity**: orderId

**Events**:
- OrderCreated: Initial event creating the order
Data: customerId, items[], total, shippingAddress

- OrderConfirmed: Payment confirmed
Data: paymentId, confirmedAt

- OrderShipped: Order shipped
Data: shipmentId, shippedAt

- OrderCancelled: Order cancelled
Data: cancelledAt, reason

**State Projection (Human Example)**:
For the ConfirmOrder command, we need minimal state:
```text
ConfirmOrderState:
  - orderId: 'order-123'
  - status: 'Draft'

For the ShipOrder command, we need different data:

ShipOrderState:
  - orderId: 'order-123'
  - status: 'Confirmed'
  - paymentId: 'payment-456'

Commands

Command: CreateOrder

  • Input: customerId, items[], shippingAddress
  • Validation: Items valid, customerId exists
  • Events produced: OrderCreated
  • Possible outcomes: Success (OrderCreated) or Validation error

Command: ConfirmOrder

  • Input: orderId, paymentId
  • Validation: Order in Draft status, payment validated
  • Events produced: OrderConfirmed
  • Possible outcomes: Success or "Already confirmed" error

Read Models (Optional)

ReadModel: OrderSummaryView

  • Purpose: Quick lookup of order status
  • Events: OrderCreated, OrderConfirmed, OrderShipped, OrderCancelled
  • Queries served: GetOrder(orderId), ListOrdersByCustomer(customerId)

Implementation Notes

  • All state is derived from events
  • Commands validate against derived state
  • No transaction across streams
  • Events are source of truth
  • Read models can be rebuilt from events

## Key Event Sourcing Principles

1. **Events are Facts**: Events describe what happened, not what might happen
2. **Immutable Event Log**: Events are appended, never modified
3. **State is Minimal and Command-Driven**: State is built by replaying events, but ONLY for what a specific command needs to validate. Not all stream fields are needed for all commands.
4. **Not DDD Aggregates**: Stream roots group events logically, but aren't bundles of related data like DDD aggregates. State is determined per-command, not designed upfront for the whole stream.
5. **Commands are Pure**: No side effects, just decision logic against minimal state
6. **Read Models are Separate**: Read models (projections) are separate from command-validation state. Read models can have rich data; command state stays minimal.
7. **Event Causality**: Commands → [minimal state] → Events → [read models]

## Design Patterns

### Compensation Pattern
Handle errors by appending compensation events:
```text
Command: ProcessPayment failed
  → PaymentFailed event
    (triggered by external error)
  → OrderCancelled event (compensation)
    (or retry logic)

Temporal Queries

Answer "what was the state at time T?":

Replay events up to timestamp T
  → Get historical state

Best Practices for Event Model Design

1. Design Minimal State Per Command

Each command handler only loads the state it needs:

  • "LoadOrderState loads { id, items, total, shipping, customer, payment, status, ... }"
  • "ConfirmOrderState loads { status, orderId }"
  • "ShipOrderState loads { status, orderId, paymentConfirmed }"

2. Separate Command State from Query Models

Keep command-validation state and read models strictly separate:

  • Command State (minimal): Used by handlers to validate commands
  • Query Models (rich): Used by UI/Processor to display/query data
  • Never share between them

3. Name State Interfaces by Command

Use the pattern [CommandName]State to make the relationship explicit:

  • PublishReviewState for PublishReview command
  • EditReviewState for EditReview command
  • ReviewState (ambiguous - which command?)

4. Document State Transitions Clearly

Show what state changes trigger what commands:

  • Include initial state
  • Show all valid transitions
  • Mark impossible transitions (and why)
  • Document rejection conditions

5. Make All Constraints Explicit

Transform "obvious" business rules into documented invariants:

  • "Obviously can't ship an unconfirmed order"
  • "ShipOrder validation: requires status='Confirmed' with paymentId"

6. Keep Event Data Factual

Events record facts, not derived values:

  • "OrderCreated { items, total }" (total is computed from items)
  • "OrderCreated { items[] with unitPrice, shippingAddress }" (total computed in projection)

Quality Checklist

  • All events are immutable facts (past tense)
  • Events contain only captured data, no computed fields
  • State projection is deterministic from events
  • Each command validates against current state
  • Commands either produce events or reject
  • Event causality is clear
  • State transitions are documented
  • No references between streams in events
  • Read models are optional, not required
  • All logic is state → events (pure functions)
Related skills
Installs
19
First Seen
Apr 13, 2026