Event Sourcing & CQRS · CQRS and Projections

Building Projections

Turn event streams into query-optimized read models reliably.

A projection consumes events (from the global log or a subscription) and updates a read model. Make projections idempotent (upserts keyed by entity, track processed positions), store the checkpoint (last processed position) in the same transaction as the read-model update, handle events in order per stream, and design read models per query (denormalize freely). Build several read models from the same events: SQL tables, search indexes, caches, analytics.

Many read models, one event log

Read model Built for Store
Order summaries Customer order history page SQL table
Order search Support agents searching orders Elasticsearch/OpenSearch
Daily revenue Finance dashboard Analytics DB / warehouse
Inventory available Product pages Redis / SQL
async def run_order_summary_projection(db, store):
    async with db.transaction():
        checkpoint = await db.fetchval("SELECT position FROM checkpoints WHERE name = 'order_summaries'") or 0
    async for batch in store.read_all(after=checkpoint, batch_size=500):
        async with db.transaction():
            for e in batch:
                if e.type == "OrderPlaced":
                    await db.execute('''INSERT INTO order_summaries (id, customer_id, total, status, placed_at)
                                        VALUES ($1,$2,$3,'placed',$4) ON CONFLICT (id) DO NOTHING''',
                                     e.data["order_id"], e.data["customer_id"], e.data["total"], e.recorded_at)
                elif e.type == "OrderShipped":
                    await db.execute("UPDATE order_summaries SET status='shipped' WHERE id=$1", e.data["order_id"])
            await db.execute('''INSERT INTO checkpoints (name, position) VALUES ('order_summaries', $1)
                                ON CONFLICT (name) DO UPDATE SET position = EXCLUDED.position''', batch[-1].position)

Common pitfalls

  • Checkpoint saved separately from the read-model write.
  • Non-idempotent projection logic.
  • Projections that call external services (slow, non-replayable).

Key points

  • Idempotent updates
  • Checkpoint in the same transaction
  • Read model per query

Further reading: Azure Architecture: Materialized View pattern

Practice questions

Why store the projection checkpoint in the same transaction as the read-model update?

Answer

So a crash can't leave the read model updated without advancing the checkpoint (duplicates) or the checkpoint advanced without the update (lost events).

Why should projections avoid calling external services?

Answer

Projections must be fast, deterministic and replayable; external calls make rebuilds slow, non-reproducible and side-effecting.

Open in the app to mark lessons complete, take notes and track your progress.