A Read Model Is
a DENORMALIZED view pre-shaped for specific queries:
write model (normalized):
users(id, name) orders(id, user_id, total) ...
read model "user_order_history":
{
user_id, user_name,
orders: [{id, total, status, placed_at}, ...],
lifetime_value: 4310.50,
last_order_at: ...
}
one query serves the profile page — zero joins at read time.
joins happen ONCE during projection updates,
not on every page load. that's the whole trick.
Building Projections
from events (or CDC):
handler(OrderPlaced e):
upsert user_order_history[e.user_id]:
push order {e.id, e.total, ...}
lifetime_value += e.total
last_order_at = e.at
properties to design deliberately:
□ IDEMPOTENT handlers (redelivery!): upserts by event id,
or track last-processed offset per projection
□ ORDERED per key: sequence/version guards against
stale-event overwrites
□ REBUILDABLE: drop + replay from source = migration tool,
bug-recovery path, new-view creation mechanism
the rebuild property is what makes aggressive iteration safe:
views are DISPOSABLE; the stream is truth.
One Query Shape → One View
anti-pattern: god-view serving every endpoint.
pattern: purpose-built views:
view | store | shape
--------------------|--------------|------------------
search | elasticsearch| inverted index
profile page | postgres json| document blob
counters/badges | redis hashes | key-counters
recommendations | feature store| vectors/lists
each view: its own lag tolerance, its own scaling curve,
its own rebuild pipeline. independence is the feature.
cost honesty: N views = N pipelines to monitor/operate.
consolidate where shapes genuinely overlap.
Serving With Staleness
read models lag writes by sync latency:
UX patterns for eventual reads:
- OPTIMISTIC UI: show expected state immediately;
reconcile when view catches up ("Order confirmed ✓")
- WRITE-THROUGH HYBRID: response of the command RETURNS
the new state (client renders it; view catches up behind)
- READ-YOUR-WRITES routing: this-user queries hit write
store briefly after their mutation; everyone else sees views
- STALENESS INDICATORS: "updated 30s ago" where precision matters
pick per VIEW: cart needs read-your-writes;
recommendation feeds don't care.
Interview Framing
“Profile pages take 800ms with 12 joins — fix it” scored answer: recognize read-shape mismatch, propose purpose-built read model updated via CDC (not request-time caching!), design the projection handler idempotently, staleness policy stated (read-your-writes post-mutation), rebuildability mentioned as safety net. The “build the read model, not a cache” distinction is precisely what’s being tested.
Premium Content
Unlock Read Models and all premium lessons with a subscription.
All premium lessons
Ad-free experience
Priority support
From ₹199.99/year — See plans