From trogonstack-eventmodeling
Designs event-sourced domain models. Maps business processes to immutable events and state projections. Events are the source of truth; state is derived from events for command validation. Use when designing event streaming architectures from domain analysis. Do not use for: brainstorming events from scratch (use eventmodeling-brainstorming-events), optimizing stream sizing or snapshotting (use eventmodeling-optimizing-stream-design), or translating external system events (use eventmodeling-translating-external-events).
How this skill is triggered — by the user, by Claude, or both
Slash command
/trogonstack-eventmodeling:eventmodeling-designing-event-modelsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
**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.
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.
Stream Identity (Impact: Determines how events are grouped into streams)
Minimal State vs Bundled State (Impact: Prevents DDD aggregate anti-pattern)
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)
Phase 2: State Design (Question 2)
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 |
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
Converts domain analysis into the event sourcing architecture pattern:
UI/Processor → Command → [Command State Read Model] → Event → Query Read Models - UI/Processor: Entry points that trigger intent
Given a domain analysis, design a complete event-sourced model:
Events are the immutable source of truth. Each stream holds facts about one entity:
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:
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 = implementedEditReviewStateToDo = planned, needs implementationSellerRespondState = implementedSemantic 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:
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:
This is NOT a full aggregate state bundle—it's minimal, command-specific state access.
Commands are intent data from UI or Processor:
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:
Read models are projections of events for UI/Processor queries:
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
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
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
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'
## 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)
Answer "what was the state at time T?":
Replay events up to timestamp T
→ Get historical state
Each command handler only loads the state it needs:
Keep command-validation state and read models strictly separate:
Use the pattern [CommandName]State to make the relationship explicit:
PublishReviewState for PublishReview commandEditReviewState for EditReview commandReviewState (ambiguous - which command?)Show what state changes trigger what commands:
Transform "obvious" business rules into documented invariants:
Events record facts, not derived values:
Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub trogonstack/agentskills --plugin trogonstack-eventmodeling