
Introduction
Event Sourcing and CQRS are two patterns that often go hand in hand, helping to solve complex problems of data consistency, audit trails and performance optimization in microservices.
1. Event Sourcing
1.1 Concept
Instead of saving the current state, save the entire sequence of events that have occurred:
Traditional (State-based):
┌──────────────────────────────┐
│ Orders Table │
│ id: O-001 │
│ status: shipped ← Chỉ biết state hiện tại
│ total: 500,000 │
│ updated_at: 2026-03-31 │
└──────────────────────────────┘
Event Sourcing:
┌──────────────────────────────────────────────────────────┐
│ Event Store (append-only) │
├────┬──────────────────┬──────────────┬──────────────────┤
│ # │ Event Type │ Data │ Timestamp │
├────┼──────────────────┼──────────────┼──────────────────┤
│ 1 │ OrderCreated │ {items, ...} │ 10:00:00 │
│ 2 │ PaymentReceived │ {amount} │ 10:01:00 │
│ 3 │ ItemsReserved │ {items} │ 10:01:05 │
│ 4 │ OrderShipped │ {tracking} │ 10:30:00 │
└────┴──────────────────┴──────────────┴──────────────────┘
Current State = replay(events) → Order{status: "shipped"}
1.2 Event Store
Đặc điểm:
├── Append-only: Không bao giờ update hoặc delete events
├── Immutable: Events là facts đã xảy ra, không thể thay đổi
├── Ordered: Events có thứ tự rõ ràng (sequence number)
└── Stream: Events được nhóm theo aggregate (ví dụ: order-O-001)
Implementation options:
├── EventStoreDB (purpose-built, recommended)
├── PostgreSQL + events table
├── Apache Kafka (log-based)
└── DynamoDB Streams (AWS)
PostgreSQL Event Store:
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
stream_id VARCHAR(255) NOT NULL, -- "order-O-001"
version BIGINT NOT NULL, -- sequence number
event_type VARCHAR(255) NOT NULL, -- "OrderCreated"
data JSONB NOT NULL, -- event payload
metadata JSONB DEFAULT '{}', -- traceId, userId, ...
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(stream_id, version) -- đảm bảo ordering
);
CREATE INDEX idx_events_stream ON events(stream_id, version);
1.3 Rebuilding State
def get_order(order_id: str) -> Order:
events = event_store.get_events(stream_id=f"order-{order_id}")
order = Order() # empty state
for event in events:
order.apply(event) # replay từng event
return order # current state
class Order:
def apply(self, event):
match event.type:
case "OrderCreated":
self.id = event.data["id"]
self.status = "created"
self.items = event.data["items"]
case "PaymentReceived":
self.status = "paid"
case "OrderShipped":
self.status = "shipped"
self.tracking = event.data["tracking"]
1.4 Snapshot Optimization
When the stream has too many events (thousands) and slow replay → use Snapshot:
Event Stream cho order-O-001:
Event 1: OrderCreated
Event 2: ItemAdded
...
Event 500: ItemRemoved
──── Snapshot at version 500 ────
{ status: "processing", items: [...], total: 1000000 }
Event 501: PaymentReceived
Event 502: OrderShipped
Rebuild: Load snapshot (v500) + replay events 501-502
→ Nhanh hơn nhiều so với replay 502 events
1.5 Advantages and disadvantages
✅ Ưu điểm:
├── Complete audit trail (ai làm gì, lúc nào)
├── Time travel: Rebuild state tại bất kỳ thời điểm
├── Event replay: Fix bug rồi replay events để sửa data
├── Natural fit cho event-driven architecture
└── Debug: Hiểu chính xác điều gì đã xảy ra
❌ Nhược điểm:
├── Complexity: Khó hơn CRUD đáng kể
├── Query: Không thể query trực tiếp (cần CQRS)
├── Schema evolution: Thay đổi event format phức tạp
├── Storage: Nhiều events → nhiều storage
└── Learning curve: Team cần thời gian adapt
2. CQRS — Command Query Responsibility Segregation
2.1 Concepts
Separate model for write (Command) and model for read (Query):
Traditional:
Client ──CRUD──▶ Same Model ──▶ Same Database
CQRS:
┌─────────────────────────────────────┐
│ API Layer │
└──────────┬───────────────┬───────────┘
│ │
┌──────────▼──────┐ ┌──────▼──────────┐
│ Command Side │ │ Query Side │
│ (Write Model) │ │ (Read Model) │
│ │ │ │
│ - CreateOrder │ │ - GetOrderDetails│
│ - CancelOrder │ │ - ListOrders │
│ - UpdateStatus │ │ - SearchOrders │
└────────┬────────┘ └────────▲─────────┘
│ │
┌────────▼────────┐ ┌───────┴─────────┐
│ PostgreSQL │ │ Elasticsearch │
│ (Write DB) │ │ (Read DB) │
│ Normalized │ │ Denormalized │
└────────┬────────┘ └─────────────────┘
│ ▲
└───── Events ──────┘
(sync read model)
2.2 Why separate Read and Write?
Write: Read:
├── Ít operations hơn ├── Nhiều operations hơn (10:1 ratio)
├── Cần ACID consistency ├── Eventual consistency OK
├── Normalized schema ├── Denormalized, pre-joined
├── Complex validation ├── Simple query, fast response
├── Scale: moderate ├── Scale: aggressive (caching, replicas)
└── PostgreSQL (optimal) └── Elasticsearch/Redis (optimal)
2.3 Synchronizing Read Model
Option 1: Domain Events (khuyến nghị)
Write DB ──event──▶ Kafka ──▶ Read Model Updater ──▶ Read DB
Option 2: Change Data Capture (CDC)
Write DB ──Debezium──▶ Kafka ──▶ Read Model Updater ──▶ Read DB
Option 3: Dual Write (KHÔNG khuyến nghị)
Service ──write──▶ Write DB
──write──▶ Read DB ← Có thể inconsistent!
2.4 CQRS + Event Sourcing
Combine both patterns:
Command Flow:
Client ──CreateOrder──▶ Command Handler
│
Validate
│
Append event to Event Store
│
Publish event to Kafka
│
▼
Event Store (source of truth)
Query Flow:
Kafka ──OrderCreated──▶ Projection Handler
│
Update Read Model (Elasticsearch)
│
Client ──GetOrder──▶ Query Handler ──▶ Read from Elasticsearch
2.5 Eventual Consistency
Timeline:
T0: Client tạo order (write to Event Store)
T1: Event published to Kafka (~5ms)
T2: Projection handler updates Elasticsearch (~50ms)
T3: Read model available (~100ms after T0)
Giữa T0 và T3: Read model chưa có data mới = Eventual Consistency
Giải pháp UX:
├── Optimistic UI: Client hiển thị ngay sau write, không đợi read model
├── Read-your-writes: Sau write, query write DB cho user đó
├── Polling/WebSocket: Client poll cho đến khi read model updated
└── Inbox pattern: Return 202 Accepted + polling endpoint
3. When to use Event Sourcing / CQRS?
3.1 You should use Event Sourcing when
- ✅ Need full audit trail (finance, healthcare, legal)
- ✅ Need time travel (rebuild state at any time)
- ✅ Domain has complex state transitions (order workflow, booking)
- ✅ Need debug production issues (replay events)
- ✅ Event-driven architecture is already the foundation
3.2 When should CQRS be used?
- ✅ Read/Write ratio big difference (10:1 or more)
- ✅ Read and Write need different scales
- ✅ Read model that needs denormalized/pre-computed
- ✅ Complex queries need search engine (Elasticsearch)
3.3 Should NOT be used when
- ❌ Simple CRUD application
- ❌ Team does not have event-driven experience
- ❌ Simple domain, few state transitions
- ❌ Consistency requirement = strong consistency everywhere
- ❌ Urgent deadline, need to ship quickly
4. Summary
| Pattern | Key Point |
|---|---|
| Event Sourcing | Save events instead of state, append-only, audit trail |
| Event Store | Immutable log of events, source of truth |
| Snapshots | Optimize rebuild using periodic snapshots |
| CQRS | Separate read/write model, scale independently |
| Eventual Consistency | Read model updates after writing (ms-level delay) |
| Projection | Process events → update read model |
Next article: Saga Pattern — Handling distributed transactions when each service has its own database.