Treat materialized views and CQRS projections as operationally maintained copies with offsets, versions, lag, replay, and reconciliation—not magic synchronized tables.

Materialized Views, Projection Tables, CQRS Read Models, and Eventually Consistent Derived Data

Read models are valuable because a query can consume a shape designed specifically for it. That convenience introduces an operational system: a source log or owner state, a projector, offsets, duplicate handling, ordering assumptions, freshness indicators, rebuild procedures, and alerting when the copy diverges.

Intermediate–Advanced100–130 minutesMechanism-first modeling labPython 3.13+ · standard libraryVendor-neutral · free/local mandatory pathLast reviewed: August 2026

Learning outcomes

Read models are valuable because a query can consume a shape designed specifically for it. That convenience introduces an operational system: a source log or owner state, a projector, offsets, duplicate handling, ordering assumptions, freshness indicators, rebuild procedures, and alerting when the copy diverges.

01

Distinguish logical views, persisted materialized views, projection tables, and CQRS read models.

02

Process duplicate and out-of-order change events without double-counting or regressing state.

03

Expose consumer offset/source versions so freshness is measurable to operators and clients.

04

Prove a derived projection can be discarded and rebuilt from authoritative history.

Implementation snapshot

Mandatory work uses Python 3.13+ standard library only in one local process. No MongoDB, Cassandra, Redis, PostgreSQL server, cloud service, Docker image, paid feature, network manipulation, or destructive failure injection is required. Optional implementation references are PostgreSQL 18, MongoDB 8.3.8 (current released patch in the 8.3 stable series as of August 29, 2026), Apache Cassandra 5.0.9, and Redis Open Source 8.10. Product-specific transaction, indexing, partition, quota, security, and licensing behavior is illustrative rather than universal.

1. Derived read models are state machines

A materialized view stores the result of a query or transformation rather than recomputing it for every read. PostgreSQL 18 provides a concrete relational example: the materialized result is persisted and can be refreshed from its defining query. A projection table is a more general application-maintained copy. Command Query Responsibility Segregation (CQRS) separates the model used to accept commands from one or more read models optimized for queries. These concepts differ in implementation, but operationally they share one truth: derived state must be populated, refreshed, validated, and rebuilt.

2. Change delivery can duplicate, reorder, or pause

At-least-once delivery means a consumer can receive the same event more than once. Cross-partition streams can also interleave independent aggregates. A robust projector therefore needs an idempotency key such as event_id, an ordering rule such as per-aggregate sequence or version, and a policy for gaps. Global ordering is often unnecessary; preserving the order required by each invariant is enough. The projection should publish its source offset/version and oldest backlog age so “fresh” becomes measurable.

3. AtlasMart lab: duplicate/out-of-order delivery and rebuild

The broken projector below assumes delivery order is authoritative and counts duplicate messages. The corrected projector deduplicates by event ID, buffers per-order gaps, advances only contiguous sequence numbers, and proves rebuildability from the authoritative log.

python · AtlasMart deterministic simulation
from collections import defaultdict

# Authoritative event log. event_id is globally unique; seq is per order.
events = [
    {"event_id":"e1","order_id":"o1","seq":1,"type":"OrderPlaced","customer":"c1","amount":120},
    {"event_id":"e2","order_id":"o1","seq":2,"type":"OrderPaid","customer":"c1","amount":120},
    {"event_id":"e3","order_id":"o2","seq":1,"type":"OrderPlaced","customer":"c1","amount":80},
]
# Delivery contains a duplicate and an out-of-order message.
delivery = [events[1], events[0], events[2], events[0]]

# Broken projector: assumes delivery order is truth and counts duplicates.
bad = defaultdict(lambda: {"orders":0,"paid":0,"last_seq":0})
for e in delivery:
    row = bad[e["customer"]]
    if e["type"] == "OrderPlaced": row["orders"] += 1
    if e["type"] == "OrderPaid": row["paid"] += 1
    row["last_seq"] = e["seq"]
print("broken projection:", dict(bad))

# Safer projector: idempotent by event_id and order-aware sequence buffering.
seen = set()
next_seq = defaultdict(lambda: 1)
pending = defaultdict(dict)
good = defaultdict(lambda: {"orders":0,"paid":0})

def apply(e):
    row = good[e["customer"]]
    if e["type"] == "OrderPlaced": row["orders"] += 1
    if e["type"] == "OrderPaid": row["paid"] += 1

for offset, e in enumerate(delivery, start=1):
    if e["event_id"] in seen:
        continue
    seen.add(e["event_id"])
    pending[e["order_id"]][e["seq"]] = e
    while next_seq[e["order_id"]] in pending[e["order_id"]]:
        n = next_seq[e["order_id"]]
        apply(pending[e["order_id"]].pop(n))
        next_seq[e["order_id"]] += 1

print("correct projection:", dict(good))
print("consumer delivery offset:", len(delivery))
print("unique events applied:", len(seen))
print("authoritative events available:", len(events))

# Rebuild proof: discard the projection and replay authoritative log in sequence order.
rebuilt = defaultdict(lambda: {"orders":0,"paid":0})
for e in sorted(events, key=lambda x:(x["order_id"],x["seq"])):
    row = rebuilt[e["customer"]]
    if e["type"] == "OrderPlaced": row["orders"] += 1
    if e["type"] == "OrderPaid": row["paid"] += 1
print("rebuild matches:", dict(rebuilt) == dict(good))
print("freshness signal: projection offset/version must be observable, not guessed")
Expected evidence

The broken projection double-counts one order and leaves a misleading last sequence. The corrected projection produces two orders and one paid order for customer c1, applies only three unique events, and a full replay after deleting the read model reproduces exactly the same state.

4. Freshness belongs in the contract

A derived read model should expose source version, stream offset, event timestamp, or computed lag. Clients can then decide whether a 30-second-old search result is acceptable while a support dashboard waits for a newer order version. A success status from the projector process is insufficient. Alert on increasing lag, stuck offsets, poison events, repeated retries, dead-letter volume, reconciliation mismatches, and rebuild duration.

5. Rebuild and reconciliation are different tools

Replay/rebuild discards derived state and recomputes it from a source of truth. Reconciliation compares existing derived state with authority to detect or correct drift. Rebuild requires retained source history or a source snapshot plus subsequent changes. Reconciliation is essential when historical events may not encode the entire current state. Both procedures need capacity planning because backfills can compete with production traffic for CPU, I/O, network, caches, and write throughput.

6. Production judgment and bridge

Use a derived read model when query shape/frequency justifies duplication and when staleness is acceptable or can be gated by source version. Protect it with tenant-aware projection code, least privilege, replayable change capture, idempotency, lag SLOs, reconciliation, and a tested rebuild. The final lesson combines the chapter by taking one normalized schema and deriving document, wide-column, and key-value physical models from explicit workloads.

Wrong approach: assume “the queue preserves order” and increment counters on every delivery

Ordering guarantees are scoped: a transport may preserve order only within a partition/key, and retries can duplicate messages. A projector that needs stronger ordering must encode that requirement in its key/sequence model. Idempotency plus per-aggregate sequence handling repairs the demonstrated double-count and stale-sequence failure.

Verification, cleanup, and production checklist

Verification is the deterministic program output plus the reasoning checks below. Cleanup is simply deleting the local lesson4.py file because the lab creates no external service or persistent database. In production, repeat the design with representative cardinality/skew, tail-latency, failure injection, tenant-isolation tests, restore/rebuild drills, migration rollback, and cost/capacity evidence before committing to a physical model.

Check your understanding

  1. What makes a read model derived rather than authoritative?
  2. Why track event IDs?
  3. Why can per-aggregate ordering be enough?
  4. What should a freshness signal contain?
  5. Why test rebuilds before an incident?
Review the answers

1. Its facts can be reconstructed from another owner/source of truth and it is not the accepted writer for those business facts.

2. To make repeated delivery idempotent and prevent duplicate side effects such as double-counted aggregates.

3. Many invariants only require ordered changes for the same entity/aggregate, avoiding unnecessary global coordination.

4. A source version/offset or lag measure that lets clients/operators compare projection state with authority.

5. A theoretical replay path may fail because history is incomplete, schemas evolved, capacity is insufficient, or projection code is not backward compatible.

References

Foundational statements are kept vendor-neutral. Version-sensitive examples use current official documentation and are labeled as examples rather than definitions.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.