skip to content

In the index table pattern, a write to a base entity needs to also update a separate index table keyed by a non-key attribute of that entity. Describe two different strategies for keeping the two tables consistent with each other, and what each one trades off in terms of latency and consistency guarantees.

level: middleimportance: must knowfreq 65%

answer

  1. scoped transaction (same partition) vs async propagation
  2. change feed / CDC / outbox / queue
  3. atomic but narrow scope vs decoupled but eventually consistent
  4. read-after-write gap
  5. reconciliation job for drift

basics

~20 s

You can either write to both tables together as one atomic operation when the store allows it, or write to the base table first and update the index table afterward in the background. The first is safer but slower and not always possible; the second is faster but can leave the index briefly out of date.

solid answer

~50 s

Two common strategies: (1) Transactional/batched dual write — if the store supports a scoped multi-item transaction (e.g., DynamoDB TransactWriteItems, Azure Table Storage entity-group transactions within one partition), write the base row and index row in the same atomic operation, so either both succeed or both fail. This gives strong consistency but is constrained to whatever scope the store's transaction API allows (often same-partition only) and adds latency/throughput cost from the transaction overhead. (2) Asynchronous propagation — write the base row first as the durable source of truth, then propagate the index update via a change feed, CDC stream, outbox table, or queue consumed by a separate process. This decouples the two writes, tolerates partial failures more gracefully via retries, and scales better, but it means the index table is eventually consistent — there's a window where a just-written entity won't show up in index-table queries.

go deeper

for a junior

Should know that there's more than one way to do the second write and that doing it 'at the same time' vs 'a bit later' has different guarantees, in plain terms.

for a middle

Should name at least one concrete transactional mechanism and one concrete async mechanism, and state the core trade-off (strong consistency + narrow scope/cost vs. eventual consistency + decoupling).

for a senior

Should discuss the read-after-write implications for real query paths and know how to reconcile drift, not just how to create the split-write pipeline.

for a principal

Should be able to design a hybrid policy — deciding per-index-table which strategy to use based on the staleness tolerance of its query paths, and account for the operational cost (monitoring, dead-letter handling) of the async pipeline.

## The gap the store leaves Keeping an index table faithful to its base table is the central operational challenge of this pattern, because the store that forces you to build the index table in the first place is usually also a store that doesn't give you a native way to update two tables atomically as a single unit — that's exactly the feature ('do this write and this other write together, or neither') a relational database gives for free via multi-statement transactions and secondary indexes give for free via engine-internal index maintenance. Two broad strategies fill that gap, and the choice between them is a genuine trade-off, not a matter of one being categorically better. ## Strategy one — the transactional dual write The **first strategy** is a transactional or batched dual write, used wherever the underlying store offers some scoped multi-item atomicity. - DynamoDB's `TransactWriteItems` lets you submit up to 100 item writes across multiple tables as a single all-or-nothing operation. - Azure Table Storage's entity group transactions let you batch up to 100 operations against entities that share the same partition key within one table. If your index table entry and base table entity can be expressed as operations inside that scope, you get true atomicity: either the base row and the index row both land, or neither does, and there is no window where an external reader can observe one without the other. The cost is real, though. Transactional writes typically: - cost more — DynamoDB bills transactional writes at roughly double the capacity units of a plain write; - carry higher latency, because the store has to coordinate the commit across the involved items; and - are, **critically**, usually scoped narrowly. Azure's entity-group transaction requires everything in the batch to share a partition key, which frequently doesn't align with the index table's natural partition key (the indexed attribute) versus the base table's partition key (the entity's own ID); you often can't get both tables into the same transaction scope at all, which is why in Azure's own documented guidance for this pattern, true single-transaction consistency between a base table and an index table is often not achievable, and the pattern is explicitly described as offering eventual consistency in the general case. ## Strategy two — asynchronous propagation The **second strategy** is asynchronous propagation: the base table write happens first, alone, as the single durable source of truth, and the index table update is derived from it afterward by a separate process. That derivation can be driven: - by a **change feed** or change-data-capture stream native to the store (Cosmos DB's change feed, DynamoDB Streams); - by an application-level **outbox pattern**, where the base write also writes a small 'pending index update' record in the same transaction as the base write and a background worker drains that outbox; or - simply by a **message queue** the writer publishes to after the base write succeeds. This decouples the two writes completely: the base write path stays fast and simple, retries and failures in the index-update path don't block or fail the caller's primary operation, and the propagation mechanism can batch, dedupe, and back off under load far more gracefully than a synchronous transaction could. The cost is that the index table becomes eventually consistent. There's a real, measurable window — milliseconds under DynamoDB Streams in the common case, potentially much longer under load, backpressure, or an outage of the propagation worker — during which a caller who writes an entity and immediately queries for it via the index table will get a miss. Any code that reads its own writes through the index path has to be written with that window in mind (read-after-write consistency isn't guaranteed), and the propagation pipeline itself becomes a new component that needs monitoring, dead-letter handling, and a repair/reconciliation job for the cases where an event is dropped entirely. ## Combining the two in production In production, the two strategies are frequently combined by scope: use transactional dual writes wherever the store's transaction scope happens to cover both tables cheaply (small, same-partition updates), and fall back to async propagation, with an explicit staleness SLA communicated to consumers, for everything else. A common real-world instance of this hybrid is a DynamoDB table pattern where a single-item update uses a conditional/transactional write to update both the item and a co-located attribute atomically, while a separate DynamoDB Streams-triggered Lambda repairs any cross-table index tables that couldn't be included in that transaction, reconciling drift on a lag measured in seconds. The engineering judgment call is picking, per index, whether the query paths that read through it can tolerate that lag. | Index | Tolerance | |---|---| | a login-by-email index | probably can't tolerate more than sub-second staleness | | a 'products in this price tier' browsing index | usually can |

  • What happens to a reader who queries the index table during the eventual-consistency window in the async strategy?
    They get a false negative — the entity exists in the base table but the index update hasn't propagated yet, so the query returns no match even though the data is really there. Applications that can't tolerate this either need to read through the base table for recency-sensitive paths or accept and design around a documented staleness bound.
  • Why can't you just always use the transactional strategy if the store supports it?
    Because the transaction scope the store offers is usually narrower than 'any two arbitrary tables' — commonly limited to entities sharing a partition key, or capped in item count, or priced and rate-limited such that using it for every write would blow through both latency and cost budgets. It's a tool for the writes that fit its scope, not a universal answer.
  • How would you detect and fix index-table entries that drifted out of sync under the async strategy?
    Run a periodic reconciliation job that scans (or streams) the base table and re-derives what the index table should contain, comparing against what's actually there and repairing mismatches — orphaned index entries get deleted, missing ones get inserted. This is typically run on a schedule or triggered off a dead-letter queue from the propagation pipeline, not run synchronously on every read.

It's like updating a company's HR system and its printed org chart on the wall. If both are small enough to update in the same afternoon meeting, you can make them agree instantly (transactional). If the org chart is printed and mailed to another building, you accept it'll be a day behind and just make sure someone eventually corrects any mistake (asynchronous).

saying these in an interview costs you the question

  • Claims the two tables are always kept perfectly in sync with no discussion of consistency model
  • Doesn't know any concrete mechanism for async propagation (change feed, streams, outbox, queue) beyond a vague 'update it later'
  • Assumes transactional writes are free/unlimited in scope and cost
  • Can't explain what a caller experiences during the eventually-consistent window
  • Treats 'eventual consistency' as a synonym for 'broken' rather than a deliberate, boundable trade-off

context