# Event-Driven Architecture — Part 2: Outbox, Event Sourcing and CQRS

Part 1 defined events as the integration contract. Part 2 covers the three patterns that make event-driven systems **reliable** and that use events as more than notifications.

## 1. The Transactional Outbox

A service must update its database **and** publish an event. Doing both naively has the dual-write problem:

```ts
await db.orders.insert(order);                 // ✅ committed
await broker.publish("OrderPlaced", order);    // 💥 crash here → event lost forever
```

**Outbox pattern**: write the event to an `outbox` table in the **same transaction** as the business change; a separate **relay** publishes outbox rows to the broker and marks them sent.

```sql
BEGIN;
  INSERT INTO orders (id, customer_id, total) VALUES ('o-17', 'c-4', 4599);
  INSERT INTO outbox (event_id, aggregate_id, event_type, payload)
       VALUES ('01HZX…', 'o-17', 'OrderPlaced', '{"orderId":"o-17", ...}');
COMMIT;
```

Relay options: **polling** (`SELECT … FROM outbox WHERE sent_at IS NULL ORDER BY id LIMIT 100 FOR UPDATE SKIP LOCKED`) or **CDC** (Debezium tails the WAL and publishes outbox inserts). Either way the guarantee is **at-least-once** publishing — consumers dedup by `eventId`.

The mirror image on the consumer side is the **inbox**: record processed `eventId`s in the consumer's DB in the same transaction as the side effect.

## 2. Event Sourcing

Instead of storing current state and *also* emitting events, **store only the events**; current state is derived by replaying them.

```
stream "account-42":
  1 AccountOpened   { owner: "ana" }
  2 MoneyDeposited  { amount: 100 }
  3 MoneyWithdrawn  { amount: 30 }
  4 MoneyDeposited  { amount: 50 }
  ──────────────────────────────
  fold(events) → balance = 120
```

- **Commands** are validated against the current state (rebuilt from the stream), then produce new events appended to the stream.
- **Optimistic concurrency**: append with "expected version = 4"; if someone else appended first, the append fails and the command retries.
- **Snapshots** every N events avoid replaying long streams.

**Gains**: complete audit history; temporal queries ("balance on March 1st"); rebuild or add new read models by replaying; debugging by replay.
**Costs**: events are forever — schema evolution needs **upcasters**; GDPR erasure needs crypto-shredding or careful design; querying current state needs projections (CQRS); a steeper learning curve.

Use it where history *is* the domain (ledgers, carts, workflows, compliance), not everywhere.

## 3. CQRS — Command Query Responsibility Segregation

Separate the **write model** (validates commands, enforces invariants) from one or more **read models** (shaped for specific queries), kept in sync by events:

```
command ──► write model ──events──► projector ──► read model A (order list per customer, PostgreSQL)
                                    projector ──► read model B (search, Elasticsearch)
                                    projector ──► read model C (dashboard counters, Redis)
query ◄───────────────────────────────────────── read models
```

- Read models are **eventually consistent** with the write side (lag = projector delay).
- A read model can be **dropped and rebuilt** by replaying events from the beginning (Kafka retention/compaction or the event store).
- CQRS doesn't require event sourcing (you can project from outbox events), and event sourcing almost always needs CQRS.

## 4. Projections Must Be Idempotent and Ordered per Aggregate

A projector processes events at-least-once, so: track the last applied position/version per aggregate, skip anything older or equal, and make each update idempotent. That's what makes replay and redelivery safe.

## Under the Hood: Appending to an Event Store Safely

An event store needs exactly two guarantees: **append-only streams** and **atomic conditional append**. In PostgreSQL:

```sql
CREATE TABLE events (
  stream_id   TEXT    NOT NULL,
  version     INTEGER NOT NULL,          -- 1, 2, 3 … per stream
  event_type  TEXT    NOT NULL,
  data        JSONB   NOT NULL,
  global_pos  BIGSERIAL,                 -- total order for projectors
  PRIMARY KEY (stream_id, version)       -- ← the optimistic concurrency check
);

-- command handler loaded the stream at version 4, appends version 5:
INSERT INTO events (stream_id, version, event_type, data) VALUES ('account-42', 5, 'MoneyWithdrawn', '{"amount":80}');
-- a concurrent writer that also read version 4 tries version 5 → unique violation → reload and retry
```

The primary key turns "expected version" into a database-enforced check, so two concurrent commands can never both act on the same stale state (e.g. both withdraw the last €80). Projectors read by `global_pos`; note that with concurrent transactions a `BIGSERIAL` can commit out of order, so robust projectors either read only up to a safe position (no in-flight lower positions) or rely on a store that assigns positions at commit (EventStoreDB, Kafka offsets).

![A command updates the write model and inserts an outbox row in one transaction; a relay publishes the event to the broker, and idempotent projectors update several read models used by queries](diagrams/01-outbox-cqrs.svg)

## Common Pitfalls (Part 2)

1. **Dual writes** instead of an outbox — lost or phantom events.
2. **Event sourcing everything** — CRUD screens with no history needs don't benefit.
3. **Mutating stored events** — breaks replay; version and upcast instead.
4. **Non-idempotent projections** — rebuilds and redeliveries corrupt read models.
5. **Reading your own write from a lagging read model** — route that read to the write side or wait for the projection position.

## Quick Reference (Part 2)

| Pattern | Solves | Key mechanism |
|---|---|---|
| outbox | DB write + publish atomically | same-transaction insert + relay (poll/CDC) |
| inbox | consumer dedup | processed event ids in the same transaction |
| event sourcing | history as source of truth | append-only streams, expected-version append, snapshots |
| CQRS | different shapes for reads and writes | projections from events, eventually consistent |
