# 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.

<!--
Guidance: How to use this template
- Complete section 1 honestly. Event sourcing adds real cost; use it where
  the event history is itself valuable, not as a default persistence style.
- The event catalogue (section 2) is the contract. Name events in the past
  tense in domain language, and review them with domain experts.
- Decide versioning, idempotency and erasure rules before the first event
  reaches production. Events are permanent; mistakes in them are too.
- Keep read models disposable: any projection must be rebuildable from the
  event store, and the rebuild must be tested.
-->

| 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

<!-- Guidance: Justify event sourcing and CQRS for this context. Answer: is the audit trail or temporal query a business need? Are reads and writes very different in shape or scale? Can the team operate it? -->

| 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

<!-- Guidance: List aggregates with their invariants, then every event they emit. Each event has one producer aggregate. Include the data each event carries and who consumes it. -->

### 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

<!-- Guidance: Define the envelope, the payload format and how events evolve. Upcasting lets old events be read as the current version without rewriting history. -->

Every event is stored with a common envelope:

```json
{
  "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

<!-- Guidance: Describe the technology, stream layout, concurrency control and retention. Optimistic concurrency on stream version is the usual approach. -->

| 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

<!-- Guidance: List each read model, the events it consumes, where it is stored and how fresh it must be. Each projection should be owned and rebuildable. -->

| 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

<!-- Guidance: State what is strongly consistent, what is eventual, and how ordering is guaranteed. Explain how the UI handles stale reads. -->

- **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:* `CartCheckedOut`
  triggers the order process manager to issue `PlaceOrder`.
- **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
  only `global_position` order is guaranteed in the store; the broker
  partitions by `stream_id` so per-aggregate order is preserved for
  consumers.

## 7. Snapshots, replay and rebuild

<!-- Guidance: Snapshots are a performance optimisation, never the source of truth. Define when they are taken and how projections are rebuilt safely. -->

| 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

<!-- Guidance: With at-least-once delivery every handler will see duplicates. Explain how commands and event handlers stay idempotent. -->

| 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

<!-- Guidance: Events are immutable, but personal data must be erasable on request. Plan this up front. Crypto-shredding is the usual technique: encrypt personal fields with a per-subject key and delete the key to erase. This is a design pattern, not legal advice; confirm obligations with your data protection officer. -->

| 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

<!-- Guidance: Show one representative command from request to updated read model, including the outbox and the projection. -->

The sequence shows a customer adding an item to a cart and the cart
view being updated.

```mermaid
sequenceDiagram
    participant UI as Web client
    participant API as Command API
    participant AGG as Cart aggregate
    participant ES as Event store and outbox
    participant BR as Event broker
    participant PR as Cart view projection
    participant RM as Cart read model
    UI->>API: AddItem (Idempotency-Key)
    API->>ES: Load stream cart-6c2a
    ES-->>API: Events v1..v6
    API->>AGG: Rehydrate and handle AddItem
    AGG-->>API: ItemAddedToCart
    API->>ES: Append at expected version 6
    ES-->>API: OK, version 7
    API-->>UI: 202 Accepted, version 7
    ES->>BR: Outbox relay publishes event
    BR->>PR: ItemAddedToCart
    PR->>RM: Update view, checkpoint
    UI->>RM: Query cart (min version 7)
    RM-->>UI: Cart with new item
```

## 11. Operations and testing

<!-- Guidance: List the operational signals and the tests that protect this design. -->

| 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.
