Production Data Systems Handbook / Chapter 24
Modeling Reads: Query APIs, Read Models, and Freshness
Design read paths as product contracts with explicit freshness, authorization, pagination, fallback, latency, ownership, and telemetry.
Preparing audio…
Audio edition
Modeling Reads: Query APIs, Read Models, and Freshness
One Screen, Several Clocks
At 10:05 a support agent opens order O-731, the cancellation from the previous chapter. The header says cancellation_pending. Search still describes the order as active. The activity feed shows that fulfillment bought a shipping label, but not that the carrier has collected the parcel. The billing panel says “void requested.” A usage panel was last refreshed at 10:00.
Which of those answers is wrong?
Perhaps none of them. Each may be an honest view of a different authority at a different time. The danger is the screen’s implication that they form one current snapshot. The agent may tell the customer that shipment has stopped, attempt a second payment reversal, or restore a permission from a stale entitlement view. A read path changes no stored fact, yet it can still cause money movement, disclosure, or a bad command.
A production read is therefore a contract about what the answer permits its caller to believe. The contract includes shape, authority, age, ordering, authorization, latency, and failure behavior. Choosing a query or read model comes after those promises are known.
Begin With the Consequence of a Bad Answer
The support workspace is not one read. Its panels support different decisions, and the harm of a bad answer differs.
The current cancellation and entitlement states can authorize consequential actions. They need current authority or a conspicuous pending result. The activity feed helps the agent reconstruct what happened; it can tolerate projection delay if the delay is visible. Usage is explanatory context, so a five-minute-old aggregate may be entirely adequate. Search helps discovery, but every result, snippet, and facet must respect the agent’s tenant and permissions.
Before selecting a store, ask what the caller will do and then disturb the answer. What happens if it is stale, incomplete, duplicated, absent, or drawn from different moments? A stale display name is usually cosmetic. A stale revocation can disclose protected data. A missing activity event can send an investigation down the wrong path. A duplicate row in an export can corrupt a total. The failure consequence tells the designer how much freshness, coherence, and fallback the read deserves.
The API should then name the product view rather than expose a storage accident. get cancellation status, list account activity, and search support notes leave room to change tables and indexes. “Select arbitrary order columns with any filter” freezes physical names, private joins, default ordering, and unbounded cost into a public interface. Useful flexibility still needs allowed fields, query budgets, result limits, supported sorts, authorization policy, usage telemetry, and an owner.
This is also where compatibility begins. Adding an optional field may be harmless. Changing what pending means, the default sort, a cursor’s lifetime, the freshness guarantee, or the behavior on timeout can break a consumer while the JSON shape remains valid. Those behaviors belong in the contract and in version decisions.
Give Each Part of the Answer a Serving Model
The order header can read the transactional authority directly. Direct reads fit small, bounded lookups where the latest committed fact controls an important decision: permission checks, administrative actions, payment state, or the result of a command just accepted. They concentrate load on the authority and couple latency to it, so broad lists and expensive joins do not become direct merely because current data is desirable.
A read replica can take bounded operational traffic or place reads nearer a region, but it introduces another clock. If the agent refreshes immediately after cancelling, routing to a lagging replica can move the screen from cancellation_pending back to ready_to_pack. The product can route recent sessions to the primary, carry a commit position and wait until the replica reaches it, or decline to promise read-your-writes. Whichever choice it makes, replica lag needs a limit and a response when the limit is exceeded.
The activity feed is better served by a projection shaped for traversal. It can combine committed order, fulfillment, payment, and support events into a denormalized timeline without making the live request join four systems. That convenience creates obligations: identify every source of truth, preserve source identities and versions, measure projection lag, reject stale updates, compare the projection with its sources, and retain a rebuild path. A projection that cannot be rebuilt or challenged has become an authority by accident.
Search is another derived model, but its job is discovery rather than merely faster filtering. Its contract includes analyzers, ranking, deletion propagation, ACL filtering, safe snippets, facets, and reindexing. The support page must not reveal the title of a private note in an autocomplete suggestion while correctly hiding the note itself.
A cache can protect a hot dependency or reuse an expensive answer. The cache key must include every dimension that changes the safe result: tenant, actor or permission scope, locale, query version, and relevant policy version. TTL alone is not an invalidation policy for a security-sensitive entitlement. The design must say whether revocation invalidates the entry, whether uncertainty fails closed, how concurrent misses avoid a stampede, and whether stale data may be served during dependency failure.
Usage belongs on an analytical serving path or precomputed aggregate. That path trades immediate currency for stable grain, definitions, lineage, and efficient scans. Showing “updated at 10:00” is more truthful and useful than forcing operational stores to perform wide aggregation for the appearance of immediacy.
Composition is not a defect. The workspace can use a direct read for the current cancellation, a projection for the timeline, a search index for notes, a cache for low-risk product metadata, and an analytical dataset for usage. The contract has to prevent that composition from pretending to be one atomic snapshot. Decision-critical actions recheck their authority; other panels expose age or degraded state where it changes interpretation.
Freshness Needs a Clock and a Response
“Eventually consistent” says nothing about when the answer becomes unsafe. A useful promise names the observation the caller receives and how the system proves it.
Read-your-writes means that after a command returns as committed, that caller can observe the new fact. Primary routing, session affinity, commit-position tokens, or catch-up waits can provide it. It does not mean every derived panel has caught up, so the support page can show the cancellation as pending while the activity projection is still processing its event.
Monotonic reads prevent a caller from seeing an older version after a newer one. A session that has observed order version 418 should not later accept version 417 from another replica. The client or gateway can carry the observed version, and the serving path can meet it, wait, or return a retryable response.
Bounded staleness permits lag inside a stated window. The usage panel may promise data no more than five minutes old. That promise needs a source timestamp or processed position measured at the point that serves the response—not merely proof that a scheduler started recently.
Best effort offers no useful age guarantee. It can be reasonable for low-risk content, but it should not be disguised as bounded staleness in billing, security, fulfillment, or incident response.
A composed response has several clocks. Record or expose them at the level at which they change a decision. One global “last updated” label is false if the entitlement is current, the feed is forty seconds behind, and usage is five minutes old. Either make the mixture safe through rechecks or let the caller see the relevant ages.
Traverse a Set That Keeps Moving
At 10:06 the agent opens page two of the activity feed while three new events arrive. Offset pagination asks the store to skip a number of rows. Inserts ahead of the offset shift the boundary, so an event may appear twice or disappear. Large offsets also become costly, and total counts may be expensive, stale, or sensitive.
A cursor resumes from an ordered position. For the activity feed it might contain the last ingestion sequence and event id, bound to the tenant, filters, direction, and query version. The event id breaks ties. The token can be signed or opaque so it does not disclose raw identifiers or permit callers to alter scope.
The ordering still expresses a policy. Event time tells the business story, but a late event can belong on a page the agent already passed. Ingestion sequence provides stable traversal, but it is not business chronology. The workspace can traverse by ingestion position, display event time, and mark late arrivals. If the requirement is a coherent export rather than a live feed, a fixed snapshot boundary may be the better contract.
Permission changes between pages require an explicit rule. Newly forbidden records should be filtered, and a token must never restore their visibility. Depending on the product, traversal may continue with shorter pages or restart at a safe boundary. Page size, counts, timing, and gaps must not become side channels for hidden records.
Authorization Must Reach Every Copy
Checking the agent before running the first query is only the beginning. The main object, individual fields, related rows, counts, facets, snippets, suggestions, thumbnails, cursor contents, exports, cache entries, logs, and metrics labels can all disclose facts.
Every derived model needs an authorization strategy that matches the risk. Tenant partitioning and service credentials may establish a broad boundary. Row or field policy may depend on relationship, classification, geography, workflow state, legal hold, or purpose. Search can use per-tenant indexes, indexed permission tokens, query-time filters, or separate sensitivity tiers. These choices trade update speed, query cost, complexity, and blast radius; none removes the need to test deletion and permission propagation.
A revocation is itself a freshness event. When access is removed, caches, projections, search indexes, exports, and analytical copies need propagation targets proportionate to the harm. For confidential notes, an uncertain policy evaluation or an overdue ACL update should fail closed. A public catalog can often tolerate a slower update.
Investigation telemetry should identify the actor, tenant, policy version, target, decision, and correlation id where appropriate. It should not copy forbidden snippets or sensitive payloads into logs. Observability that creates a second disclosure path has failed the same contract it was meant to prove.
A Fallback Must Preserve Meaning
Suppose the activity projection stops updating at 10:07. Returning its last page with “feed delayed by 90 seconds” may still help support. If the entitlement authority times out, serving yesterday’s cached permission as if current would be dangerous. If billing cannot confirm the void, the screen should say that confirmation is pending and prevent another payment action—not turn absence into failure or success.
Choose fallback from the decision, not from the dependency. Low-risk public content may use stale cache. A dashboard can return clearly marked partial panels. Search may omit facets while retaining authorized results. A decision that moves money, grants access, controls fulfillment, or guides an incident may need a source recheck, a retryable error, or a fail-closed response.
Silent degradation changes the product without review. Track cache age, replica and projection lag, index delete lag, partial responses, primary-read escapes, stale-while-revalidate use, authorization failures, and time spent on each fallback. If a stale path serves traffic for three days, it is no longer a brief fallback; it is the production behavior.
Operate the Answer
During an incident, an owner should be able to reconstruct one response: which models served it, the source or policy versions used, the age of each consequential fact, the cursor and filters applied, the fallback chosen, and the latency and query cost contributed by each dependency.
That requires telemetry at the read-contract boundary. Measure latency and timeout rate by endpoint and bounded query shape; result sizes and empty results; cache hit and age; replica, projection, index, and analytical lag; authorization denials; cursor errors; fallback rates; query-plan or scan warnings; drift checks; and rebuild progress. High-cardinality identifiers do not belong indiscriminately in metric labels; use logs or traces with suitable access and retention when response-level investigation needs them.
Derived models need named owners for serving schema, build logic, access policy, freshness objective, backfill, drift detection, rebuild, and incident response. Upstream owners remain responsible for their facts and publication guarantees. A composed read also needs one serving owner who decides what the user sees when those responsibilities meet.
Write the Contract as a Failure Conversation
Before approving an important read, write a short record that another engineer could use during an outage:
Caller and decision:
Answer shape, limits, filters, and compatibility promise:
For each consequential field or panel:
Serving model and authoritative source:
Latency target and query budget:
Freshness promise and measurement point:
Authorization rule and revocation behavior:
Ordering, cursor, tie-breaker, and snapshot rule:
Allowed stale, partial, retry, or fail-closed result:
Owner, drift check, rebuild path, and telemetry:
Contradictions the caller may observe:
Action that must recheck current authority:
Evidence that would force a different design:
Apply it to the support workspace, then break the design. Delay the activity projection. Revoke the agent’s access between pages. Insert three events with the same timestamp. Miss a note deletion in search. Let the analytical build fail. Timeout after the order authority has returned version 418 once.
For every break, decide what the agent sees and which actions remain available. Then replace offset pagination with a cursor and state the new invariants: deterministic order, stable tie-breaker, token scope, permission behavior, late-arrival policy, and snapshot or expiry rule. The exercise is complete only when an on-call engineer can identify the source, age, policy, fallback, and repair path without guessing.
The cancellation in Chapter 23 left several honest facts in flight. A good read path does not flatten them into a reassuring answer. It shows current authority where a decision requires it, admits bounded delay where delay is acceptable, and refuses to let an old view authorize a new consequence. Once that behavior is explicit, the next question is who may expose the data and through which service boundary.
Continue reading
Full table of contents