Event Sourcing and CQRS Design Template
Event-sourced service design: fit check, event catalogue, versioning and upcasting, store, projections, idempotency, crypto-shredding
사용법: Use when a bounded context will store state as events with separate read models. Start with the fit check and the event catalogue, reviewed with domain experts. The main pitfall is deciding versioning, idempotency and personal-data erasure after events are already in production.
미리보기
Event Sourcing and CQRS Design: [Bounded context or service name]
Design document for a service that stores state as a sequence of domain events (event sourcing) and separates the write model from read models (Command Query Responsibility Segregation, CQRS): when to use it, aggregates and the event catalogue, event schema and versioning, the event store, projections, consistency and ordering, snapshots, replay, idempotency, GDPR erasure and the main interaction flow. Based on common industry practice. Replace every
[bracketed]value and every row marked Example; the examples follow a shopping cart and order context.
| Field | Value |
|---|---|
| Document ID | [ES-CQRS-001] |
| Bounded context | [e.g. Ordering] |
| Version | [0.1] |
| Status | [Draft / In review / Approved] |
| Owner | [@tech-lead] |
| Reviewers | [@architect], [@data-protection-officer], [@sre] |
| Last updated | YYYY-MM-DD |
| Related | [Domain model], [Event API specification], [ADR-NNN on event sourcing] |
1. When to use and decision
| Question | Answer for this context |
|---|---|
| Is a complete, trustworthy history a business requirement? | Example: Yes. Customer disputes need the exact sequence of cart and order changes |
| Do we need to answer "what was the state at time T"? | Example: Yes, for pricing audits |
| Do read and write workloads differ greatly? | Example: Yes. 50 reads per write; several read shapes (customer view, warehouse view, analytics) |
| Is eventual consistency of read models acceptable? | Example: Yes, within 2 seconds, except order confirmation which reads its own write |
| Does the team have experience or support? | Example: Two engineers have run an event-sourced system before |
| Simpler alternative considered | Example: CRUD with an audit table; rejected because audit tables drift from behaviour |
Decision: [Use event sourcing with CQRS / Use CQRS only / Do not use]. Recorded in [ADR-NNN].
Avoid this pattern for: [list]. Example: reference data such as the product catalogue and user preferences, which stay CRUD.
2. Aggregates and event catalogue
2.1 Aggregates
| Aggregate | Identity | Invariants enforced | Typical stream length |
|---|---|---|---|
| Example: Cart | cart_id | Max 100 lines; quantity 1-99 per line; no changes after checkout | 5-40 events |
| Example: Order | order_id | Cannot ship before payment captured; cancellation only before shipment | 4-15 events |
| [Aggregate] | [ID] | [Rules] | [Events] |
2.2 Event catalogue
| Event | Aggregate | Trigger (command) | Key data | Schema version | Consumers |
|---|---|---|---|---|---|
CartCreated | Cart | CreateCart | cart_id, customer_id, currency | 1 | Cart view |
ItemAddedToCart | Cart | AddItem | sku, quantity, unit_price | 2 | Cart view, analytics |
ItemRemovedFromCart | Cart | RemoveItem | sku | 1 | Cart view, analytics |
CartCheckedOut | Cart | Checkout | cart_id, order_id, total | 1 | Order process manager |
OrderPlaced | Order | Process manager | order_id, lines, total, shipping_address_ref | 1 | Order view, warehouse view, email |
PaymentCaptured | Order | CapturePayment | payment_ref, amount | 1 | Order view, finance |
OrderShipped | Order | ConfirmShipment | carrier, tracking_no | 1 | Order view, email |
OrderCancelled | Order | CancelOrder | reason_code | 1 | Order view, finance, warehouse |
| [EventName] | [Aggregate] | [Command] | [Fields] | [n] | [Consumers] |
Naming rules: past tense, domain language, no technical verbs such as "Updated" for a whole aggregate.
3. Event schema and versioning
Every event is stored with a common envelope:
{
"event_id": "0f8b6c1e-4a1d-4a63-9a7e-2b1f7f0c9d21",
"stream_id": "cart-6c2a",
"stream_version": 7,
"event_type": "ItemAddedToCart",
"schema_version": 2,
"occurred_at": "2026-10-21T09:14:03.120Z",
"correlation_id": "req-91a2",
"causation_id": "cmd-55f0",
"tenant_id": "t-001",
"payload": { "sku": "SKU-1042", "quantity": 2, "unit_price": { "amount": "19.90", "currency": "EUR" } }
}
| Change type | Allowed? | Approach |
|---|---|---|
| Add optional field | Yes | Consumers ignore unknown fields; default when missing |
| Rename or change meaning of a field | No in place | New schema version plus upcaster |
| Remove a field | Only with a new version | Upcaster supplies a derived value or default |
| Split or merge events | Rarely | New event types; upcaster or copy-and-transform migration |
Upcasting: when an old event is read, a chain of upcasters converts it
step by step to the current version before it reaches the aggregate or a
projection. Example: ItemAddedToCart v1 stored price as a number in
cents; the v1 to v2 upcaster converts it to unit_price with amount and
currency taken from CartCreated.
| Event | From | To | Upcaster | Test |
|---|---|---|---|---|
ItemAddedToCart | 1 | 2 | Example: ItemAddedV1toV2 | Golden events from production samples |
| [Event] | [n] | [n+1] | [Name] | [Test] |
4. Event store
| Aspect | Design |
|---|---|
| Technology | Example: PostgreSQL table events (append-only) with an outbox for publishing; or a dedicated event store product |
| Stream layout | One stream per aggregate instance: {aggregate}-{id} |
| Concurrency | Optimistic: append succeeds only if expected stream_version matches; unique key on (stream_id, stream_version) |
| Global order | Monotonic global_position for projections that read all streams |
| Publishing | Transactional outbox to the event broker; at-least-once delivery |
| Retention | Permanent for business events; see section 9 for personal data |
| Backups | Point-in-time recovery; restore tested quarterly |
| Expected volume | Example: 3 million events per day, average 600 bytes, about 650 GB per year |
5. Projections and read models
| Read model | Purpose | Events consumed | Store | Freshness target | Owner |
|---|---|---|---|---|---|
| Example: Cart view | Show current cart to customer | Cart*, Item* | Key-value store | < 500 ms | [@team-cart] |
| Example: Order history | Customer order list | Order*, PaymentCaptured | Document database | < 2 s | [@team-order] |
| Example: Warehouse pick list | Orders ready to ship | PaymentCaptured, OrderCancelled, OrderShipped | Relational table | < 5 s | [@team-fulfilment] |
| Example: Sales analytics | Revenue by day and product | All order events | Warehouse | < 1 h | [@data-team] |
| [Read model] | [Purpose] | [Events] | [Store] | [Target] | [@name] |
Each projection records its checkpoint (global_position last
processed) in the same transaction as its writes, so restarts resume
exactly where they stopped.
6. Consistency and ordering
- Within an aggregate: strongly consistent. Commands load the stream, check invariants and append with optimistic concurrency.
- Across aggregates: eventually consistent, coordinated by process
managers (sagas) reacting to events. Example:
CartCheckedOuttriggers the order process manager to issuePlaceOrder. - Read models: eventually consistent. Commands return the new stream version; the UI can wait until the read model reaches that version ("read your own writes") for order confirmation.
- Ordering: guaranteed per stream by
stream_version. Across streams onlyglobal_positionorder is guaranteed in the store; the broker partitions bystream_idso per-aggregate order is preserved for consumers.
7. Snapshots, replay and rebuild
| Topic | Design |
|---|---|
| Snapshot trigger | Example: Every 100 events for Order streams; none for Cart (short streams) |
| Snapshot storage | Separate table, keyed by stream and version; may be deleted at any time |
| Snapshot versioning | Snapshot schema version stored; on mismatch, ignore and replay from start |
| Projection rebuild | Build a new projection version alongside the old one, replay from position 0, switch reads when caught up, then drop the old one (blue-green) |
| Rebuild duration | Example: About 6 hours for one year of events (1.1 billion) at 50,000 events/s |
| Side effects during replay | Disabled: emails, payments and external calls are only triggered by live processing |
8. Idempotency
| Point | Mechanism |
|---|---|
| Command submission | Example: Client sends Idempotency-Key; command handler stores key and result for 24 h |
| Event append | Deterministic event_id per command; duplicate append rejected by unique constraint |
| Projection handler | Skip events with global_position at or below checkpoint |
| External side effects | Record event_id in a processed-events table before calling out; external API called with the same idempotency key |
9. GDPR erasure and personal data
| Rule | Design |
|---|---|
| Keep personal data out of events where possible | Example: Store shipping_address_ref, not the address; the address lives in a CRUD store that supports deletion |
| Personal fields that must be in events | Encrypted with a per-customer data key held in a key management service |
| Erasure (crypto-shredding) | Delete the customer's key; events remain but personal fields can no longer be decrypted |
| Projections | Erasure event triggers deletion or anonymisation in every read model |
| Backups | Keys are not in event-store backups, so restored events stay unreadable |
| Evidence | Erasure log with request ID, key ID and timestamp |
10. Main interaction flow
The sequence shows a customer adding an item to a cart and the cart view being updated.
11. Operations and testing
| Area | Practice |
|---|---|
| Monitoring | Example: Projection lag in seconds and positions; outbox backlog; concurrency conflict rate; upcaster errors |
| Alerts | Projection lag > 30 s for 5 min; outbox backlog > 10,000 |
| Aggregate tests | Given past events, when command, then new events (or error) |
| Projection tests | Given events, then read model state |
| Upcaster tests | Stored golden samples of every historical schema version |
| Rebuild drill | Full rebuild of one projection in staging every quarter |
Standard and licence note
Based on common industry practice for event sourcing and CQRS, as described in public writing by Greg Young, Martin Fowler and others. Original text written for this collection; examples are illustrative. The GDPR section describes a technical pattern and is not legal advice. Template licence: MIT.
관련 템플릿
Event-Driven API Specification Template (AsyncAPI 3)
Async event contract: brokers, channels, operations, CloudEvents envelope, ordering, delivery, DLQ, schema evolution, AsyncAPI YAML
아키텍처 및 시스템 설계EventStorming Workshop Summary Template
Record an EventStorming session: events timeline, commands, actors, policies, read models, hotspots, context candidates
아키텍처 및 시스템 설계Architecture Decision Record Template (Tyree-Akerman, Y-Statement)
Detailed ADR – Tyree & Akerman fields (assumptions, constraints, positions, argument, implications, related items) plus Y-statement