Lesson 14 / 25

Read Models, Projections and Eventual Consistency

Build read models from events and handle the delay in the UI.

Keeping read models up to date

A projection is code that listens to events (or change-data-capture records) and updates a read model: insert a row on OrderPlaced, update the status on OrderShipped, increment a counter on ProductViewed. One stream of events can feed many projections: a customer order list, a warehouse picking view, a search index, a revenue dashboard. Projections must be idempotent and able to be rebuilt from scratch, by replaying events into a fresh table, which also lets you add new read models later. The cost is eventual consistency: just after a command succeeds, the read model may not show the change yet. Handle it in the UI: return the new ID and show the result optimistically, poll or push an update, display "processing", or read from the write model for the user's own just-created item.

A projection for the customer orders view

Upserts make it safe to replay or receive duplicates.

def project(event, db):
    t, d = event["type"], event["data"]
    if t == "OrderPlaced":
        db.execute(
            "INSERT INTO customer_orders_view (order_id, customer_id, placed_at, item_summary, status) "
            "VALUES (%s, %s, %s, %s, 'PLACED') ON CONFLICT (order_id) DO NOTHING",
            (d["orderId"], d["customerId"], event["time"], summarise(d["items"])),
        )
    elif t == "OrderShipped":
        db.execute(
            "UPDATE customer_orders_view SET status = 'SHIPPED', eta = %s WHERE order_id = %s",
            (d.get("eta"), d["orderId"]),
        )
    elif t == "OrderCancelled":
        db.execute(
            "UPDATE customer_orders_view SET status = 'CANCELLED' WHERE order_id = %s",
            (d["orderId"],),
        )

The railway departure board

The departure board is a read model: it is updated from the control room's decisions, usually within seconds. Occasionally a train has already left before the board catches up, so the station staff plan for that delay.

Quick check: A user creates an order and is immediately redirected to "My orders", but the new order is missing. What is the most likely cause?

  • The command failed silently
  • The database lost the write
  • The schema registry rejected the event
  • The read model has not processed the event yet (eventual consistency)
Answer

The read model has not processed the event yet (eventual consistency) — Projections lag slightly behind the write side; the UI must account for it.