Data Consistency and Distributed Transactions Questions
Maintaining correctness of state across services and replicas: eventual consistency, conflict resolution (last-write-wins, CRDTs, vector clocks), the saga pattern, two-phase commit, and idempotency keys for exactly-once effects. Covers when to trade strict consistency for availability and how to reason about read-your-writes and monotonic guarantees. Focuses on the application/service layer rather than storage-engine internals.
Compare two-phase commit (2PC) and the saga pattern as approaches to coordinating a transaction that spans multiple services. Explain how each handles atomicity, availability, and isolation, and describe the kinds of business workflows where sagas are preferred over 2PC (and vice versa).
Sample Answer
Direct answer: 2PC gives you real atomicity and isolation across services at the cost of availability: every participant holds locks and blocks for the whole round-trip, and a coordinator failure can leave things stuck. Sagas give up that atomicity and isolation in exchange for availability: each step commits immediately and independently, and failures are corrected afterward with compensating actions rather than prevented upfront. Sagas are generally preferred for long-running, cross-service business workflows; 2PC (or a consensus-backed variant of it) is preferred when you genuinely need all-or-nothing atomicity over a short, bounded set of operations and can tolerate the availability cost.
Structured elaboration
Atomicity. 2PC is genuinely atomic: either every participant commits or every participant aborts, and no external observer ever sees a partial result, because nothing is applied until every vote is in. A saga is NOT atomic in that sense: each local transaction commits on its own, so a later step failing means the earlier steps' effects are visible until a compensation reverses them. The system passes through real intermediate states.
Isolation. 2PC participants hold locks from PREPARE through the final decision, so concurrent transactions touching the same data are serialized against each other for that whole window. Sagas provide no such isolation: two sagas can interleave their steps, so a saga step might read a state that a concurrent, not-yet-compensated saga has already changed (dirty reads/write-skew-like anomalies at the application level). This is why saga designs often need extra defenses (semantic locks, versioning, or designing steps to be commutative) to avoid anomalies 2PC would have prevented for free.
Availability. 2PC's blocking failure mode (a coordinator crash stranding in-doubt participants holding locks) is a direct availability cost that scales with how long the transaction takes and how many services are involved. Sagas never hold cross-service locks, so a slow or failed step degrades gracefully: earlier steps' work stays visible and gets compensated, rather than the whole system freezing.
When sagas are preferred over 2PC
- Long-running workflows (minutes to days: order fulfillment, travel booking) where holding locks the whole time is operationally unacceptable.
- Workflows that legitimately cross organizational or technology boundaries (different databases, sometimes different companies via APIs) where a shared 2PC coordinator isn't even possible.
- High-throughput systems where the extra round-trip and lock contention of 2PC would be a bottleneck.
- Cases where a business-level "undo" (refund, cancellation, apology) is an acceptable and well-understood outcome, most commerce and booking flows fit this.
When 2PC (or a consensus-backed equivalent) is still preferred
- Short, bounded operations across a small number of participants you fully control (e.g. a coordinator process and its own set of shards), where the blocking window is small and predictable.
- Cases with no sane compensating action, e.g. "send an email" can't be un-sent, so if a step is genuinely irreversible, a saga's whole safety model breaks down for that step and you need either strict ordering (do reversible steps first, irreversible steps last) or real atomicity upfront.
- Financial or regulatory contexts where a visible, even temporary, partial state is unacceptable regardless of eventual correction.
Trade-offs and pitfalls. The most common mistake in an interview answer here is treating this as "sagas are strictly better because they scale," without acknowledging that sagas give up something real: they push the correctness burden from the protocol onto every individual step's compensating-action design, and a badly designed compensation (one that isn't semantically correct, or that assumes an operation is reversible when it isn't) is a much harder bug to catch than a 2PC coordinator crash, because it corrupts business state rather than just blocking.
Design a CRDT-based multi-master replication scheme for user-profile objects replicated across regions. Which CRDT types would you choose for the different kinds of profile fields (counters, strings/text, sets), how would you handle deletions and tombstones, and how would you surface an unresolved semantic conflict to the application when a CRDT merge alone can't decide the right outcome?
Sample Answer
Direct answer: For user-profile objects replicated across regions, I'd use a G-Counter or PN-Counter for numeric fields (like a follower count), an LWW-Register (or a more careful conflict-preserving register) for single-valued fields like display name, and an OR-Set for multi-valued fields like a list of interests or tags, handling deletions with per-element tombstones and surfacing genuinely unresolvable semantic conflicts back to the application rather than silently picking a winner.
Structured elaboration
Per-field-type CRDT selection. A user profile isn't one homogeneous blob, different fields have different natural merge semantics, so the design applies a DIFFERENT CRDT per field type rather than forcing one structure onto the whole object. Counters (follower count, post count): PN-Counter, converges to the exact correct total regardless of which region incremented what. Single-valued text fields with no natural "combine" semantic (display name, bio): an LWW-Register (each write timestamped, last-write-wins per field), accepting that a genuine concurrent edit to the SAME field from two regions will silently keep only one, a reasonable trade for fields where a merge doesn't make sense anyway. Multi-valued fields (interests, tags, linked accounts): an OR-Set, so concurrent additions from different regions/devices all survive the merge, and removals only remove what was actually observed (not accidentally removing a concurrent, unrelated add of the same value).
Handling deletions and tombstones. An OR-Set's remove operation doesn't delete the underlying record, it marks the SPECIFIC observed instance(s) as removed (tracked via their unique add-IDs), while leaving room for a concurrent add of the "same" value (a different unique ID) to survive. Tombstones (records of what was removed) need periodic compaction (see metadata-growth discussion elsewhere in this topic) since they'd otherwise accumulate forever for a long-lived, frequently-edited profile.
Surfacing unresolved semantic conflicts. Some conflicts genuinely can't be resolved automatically by ANY generic merge rule, e.g. two regions concurrently setting a user's "primary email" to two DIFFERENT, both-valid-looking addresses, an LWW-Register would silently pick one, but that might not be what the user actually wants. For fields where a silent LWW resolution carries real risk of user confusion or harm, the design instead surfaces the conflict explicitly (both candidate values, with metadata about which region/device/time each came from) to the application layer, which can prompt the user to pick, rather than baking a silent, possibly-wrong resolution into the data layer.
Worked example. A user updates their bio from their phone while offline, and separately updates their location (a different field) from their laptop while also offline. On reconnect, these merge cleanly with no conflict at all (different fields, an LWW-Register per field means unrelated fields never interact). But if the SAME user, confused about which device has their latest edit, changes their bio on BOTH devices while both were offline with genuinely different text, the LWW-Register merge picks whichever write has the later timestamp, and the other bio edit is silently lost. If the product decides bio conflicts matter enough to protect against this, the field-level design would flag this specific case (same field, both devices, concurrent, per the register's own tracked metadata) and surface both candidate bios to the user on next sync rather than silently discarding one.
Trade-offs and pitfalls. Applying a single CRDT type uniformly across an entire heterogeneous object (treating the whole profile as one opaque LWW-Register, say) is a common shortcut that either loses information unnecessarily (for fields that could have merged cleanly, like the interests list) or, in the other direction, over-engineers simple fields with unnecessary OR-Set machinery, the per-field-type design costs more upfront modeling effort but avoids both failure modes.
Describe a zero-downtime migration strategy to change a service's consistency model from strong to eventual. Include feature flags, dual-writes, read-path toggles, monitoring to verify correctness, and a rollback path if anomalies appear. Explain how you would validate data correctness throughout the migration.
Sample Answer
Direct answer: A zero-downtime migration from strong to eventual consistency uses feature flags to control which consistency path is active, dual-writes to keep both models correct during the transition, a read-path toggle so you can validate the new (eventually-consistent) reads against the old (strong) ones before trusting them, ongoing monitoring to catch correctness regressions early, and a rollback plan that doesn't require another migration to execute.
Structured elaboration
Feature flags. A flag (ideally per-tenant or percentage-rollout capable, not a single global switch) controls whether a given request path uses the old strongly-consistent read/write logic or the new eventually-consistent one, letting you migrate gradually and limit the blast radius of any problem to whatever slice of traffic the flag currently covers.
Dual-writes. During the transition, writes go to BOTH the old strongly-consistent path and the new eventually-consistent path (e.g. writing synchronously to the primary datastore as before, while also publishing to whatever asynchronous replication or eventual-consistency mechanism the new path relies on). This keeps both models populated and comparable while neither is fully trusted alone yet.
Read-path toggle plus shadow reads. Before actually SERVING reads from the new eventually-consistent path, run it in shadow mode: for a sample of requests, read from both paths, compare the results, and log any divergence, without letting the new path's result affect the response the user actually sees. This surfaces correctness bugs (staleness beyond expected bounds, a merge bug, a missed write) before they're customer-visible.
Monitoring to verify correctness. Track divergence rate between the two paths (from shadow reads), staleness distribution on the new path (how far behind is a typical eventually-consistent read, and what's the tail), and business-relevant anomaly signals specific to the domain (e.g. for inventory, oversell events). These metrics are what tell you when it's actually safe to flip more traffic to the new path, not just elapsed time or gut feel.
Rollback path. Because writes are dual-written throughout the migration, rolling back is just flipping the feature flag back to serve reads from the still-current strongly-consistent path, no data migration or backfill needed, since that path was never stopped. This is why dual-writing throughout the transition (rather than a hard cutover) is the key enabler of true zero-downtime rollback.
Worked example. A product-catalog service migrates from a single strongly-consistent primary to a multi-region, eventually-consistent replicated store. Phase 1: dual-write catalog updates to both the existing primary and the new replicated store; reads still come exclusively from the primary. Phase 2: enable shadow reads for 5% of traffic, comparing primary reads against the new store's reads, this surfaces that replication lag occasionally exceeds 2 seconds during peak write bursts, more staleness than the product team is comfortable with, so the team adds a bounded-staleness check (reject a shadow-read comparison, and hold back cutover, if lag exceeds a threshold) before proceeding. Phase 3: once divergence and staleness metrics are within agreed bounds for a sustained period, gradually flip the read-path flag to serve real traffic from the new store, monitoring the same metrics the whole time, with the flag remaining flippable back to the primary for weeks afterward as a safety net.
Trade-offs and pitfalls. Skipping the shadow-read validation phase (going straight from dual-write to serving real traffic from the new path) is the most common shortcut that causes incidents, correctness bugs in an eventually-consistent path often only show up under real production write patterns and timing, not in staging, and shadow reads are what let you catch them without a customer-visible impact.
Describe an algorithmic approach to reconcile diverged replicas for a key-value store that uses last-writer-wins (LWW) with version vectors. Account for missing timestamps, partial updates, tombstones, and the goal of preserving monotonicity when possible. Explain operational steps an SRE should take to run reconciliation safely.
Sample Answer
Direct answer: Reconciling diverged replicas that use LWW with version vectors means comparing each replica's version vector for a key to determine whether one genuinely dominates the other (safe to just take the dominant one) or they're concurrent (genuine conflict, apply the LWW timestamp rule as the tiebreaker), while explicitly handling missing timestamps, partial updates, and tombstones as special cases that a naive "just compare timestamps" approach would get wrong.
Structured elaboration
Step 1: version-vector comparison first, LWW second. Before falling back to LWW's timestamp comparison, check whether the two replicas' version vectors for the key show a clear happened-before relationship (one vector dominates the other). If so, the dominant version is definitively the correct, more complete one, no ambiguity, no need for LWW at all, this case should never even consult timestamps. LWW timestamp comparison is only the right tool for the GENUINELY concurrent case (vectors incomparable), where there's a real conflict needing SOME resolution rule.
Step 2: missing timestamps. A replica that's never written a key locally may have received it only via replication, and might be missing a proper local timestamp for it (or have one that reflects RECEIPT time, not the original write time), the reconciliation algorithm needs to distinguish "this replica's copy has no meaningful timestamp of its own" from "this replica's copy is genuinely older," treating a missing timestamp as automatically losing (rather than defaulting to some arbitrary sentinel value that might accidentally win or lose incorrectly) is usually the safer default.
Step 3: partial updates. If an update only touched SOME sub-fields of a larger record (not a full replacement), reconciling at the whole-record level with LWW would incorrectly discard sub-field changes that a coarser-grained conflict resolution would drop, the algorithm needs to reconcile at the same GRANULARITY the writes actually happened at (per-field, if updates are per-field), not coarser, or it re-introduces exactly the "discard real information" problem LWW-at-the-wrong-granularity always causes.
Step 4: tombstones. A deleted key isn't simply absent, it needs its own tombstone record (with its own version vector and timestamp) so a reconciliation between a replica that has the delete and one that has a concurrent, unaware UPDATE can correctly determine whether the delete or the update should win (via the same version-vector-then-LWW logic), rather than the delete being invisible to the reconciliation process entirely (which would let a stale update silently "resurrect" a deleted key).
Preserving monotonicity where possible. Where the underlying application semantics allow it (e.g. a monotonically increasing "last known good state" concept), the reconciliation should prefer NOT to move a value backward even when a raw LWW comparison might otherwise suggest it, worth flagging as an explicit design choice (monotonicity as an added constraint layered ON TOP of the version-vector-then-LWW logic, not something either mechanism provides automatically on its own).
Operational steps for running reconciliation safely. Run reconciliation as a controlled, rate-limited background process (not blocking live traffic), sampling or scanning divergent keys via a cheap detection mechanism (checksums or hash comparison per key range) before doing the more expensive full version-vector comparison only on keys that actually show divergence, log every reconciliation decision (which version won, why, for later audit if a resolution turns out to have been wrong), and support a manual override path for cases the automated algorithm can't confidently resolve (e.g. two updates with missing timestamps on both sides, genuinely no safe automatic answer).
Worked example. Replica A has key K with version vector {A:2, B:1}, value "v2", timestamp 100. Replica B has K with version vector {A:1, B:2}, value "v3", timestamp 105. Comparing vectors: {A:2,B:1} vs {A:1,B:2}, neither dominates (A is ahead on its own axis, B on its own), genuinely concurrent. Falls to LWW: B's timestamp (105) is later, B's value "v3" wins. The reconciled version vector becomes the MERGE of both ({A: max(2,1)=2, B: max(1,2)=2}), not just B's original vector, since the resolution needs to reflect that BOTH replicas' prior history has now been accounted for, even though only B's VALUE was kept.
Trade-offs and pitfalls. A common bug is updating the value from the LWW-winning replica but forgetting to merge the version vectors themselves (just copying the winner's vector instead of merging both), which can cause a FUTURE genuinely-new update to be incorrectly classified as "already seen" or vice versa, the value and the causal-history metadata need to be reconciled together, not independently.
Design a scalable approach to support atomic increments for a counter that's sharded across many keys (for example, a global 'likes' count). Compare a few approaches (per-shard counters with periodic aggregation, CRDT counters, a central counter service, optimistic CAS-based increments) on accuracy, throughput, read latency, and reconciliation cost.
Sample Answer
Direct answer: For a sharded counter that needs to keep incrementing correctly under high concurrency without coordination, a CRDT counter (a PN-Counter, sharded by key with one counter object per key) gives exact eventual convergence with no central bottleneck; per-shard counters with periodic aggregation trade a small aggregation delay for simpler infrastructure; a central counter service gives immediate global consistency at the cost of becoming a write bottleneck; and optimistic CAS (compare-and-swap)-based increments work for moderate contention but degrade under very high concurrent write rates on the same key.
Structured elaboration
CRDT counter (PN-Counter per key). Each region/replica maintains its own local increment count for a given counter key; merging (element-wise max plus sum) converges to the exact correct total with no coordination required at write time. Accuracy is eventually exact (once all increments have propagated and merged, the total is precisely correct, nothing is approximated or lost). Storage overhead is one entry per replica per counter key, and merge/read cost scales with the number of replicas, not the number of increments, since increments to the same replica's entry just add to that entry rather than each needing its own record.
Per-shard counters with periodic aggregation. Instead of a CRDT's continuous merge, each shard just accumulates its own local count independently, and a periodic batch job sums all shards' counts to produce the displayed total. Simpler to build (no CRDT library or merge logic needed), but the displayed total is only as fresh as the last aggregation run, real-time reads see a stale, under-counted total between aggregation cycles, an explicit trade-off, not a bug, if the use case tolerates it.
Central counter service. A single service (backed by its own strongly-consistent store) owns the counter and serializes all increments through it. Gives immediate, always-accurate reads with no aggregation delay, but throughput is capped by that single service's write capacity, and it becomes a single point of contention (and, without careful design, a single point of failure) as write volume grows, exactly the scaling problem sharding elsewhere in the system was meant to avoid.
Optimistic CAS-based increments. Each increment reads the current value, computes the new value, and writes it back with a compare-and-swap (only succeeding if the value hasn't changed since the read); a losing CAS retries. Works well at moderate contention (few concurrent writers per key), but retry rate grows sharply as concurrent writers to the SAME key increase, under very high contention (many writers hammering one popular key, a "hot key"), this can degrade into a large fraction of writes retrying repeatedly, hurting both latency and throughput.
Comparison
| Approach | Accuracy | Throughput under high concurrency | Read latency | Reconciliation needed |
|---|---|---|---|---|
| CRDT (PN-Counter) | Eventually exact | High, no coordination on write | Read = merge of current replica states, cheap | None; merge IS the reconciliation |
| Per-shard + periodic aggregation | Exact as of last aggregation, else stale | Very high, purely local writes | Fast but stale between cycles | The aggregation job itself |
| Central counter service | Always exact, immediately | Bounded by the service's own capacity | Fast, single source of truth | None needed, but scaling requires sharding the service itself eventually |
| Optimistic CAS | Always exact, immediately | Degrades under high same-key contention | Fast when uncontended | None; but retry storms under contention are a real operational risk |
Worked example. A "post likes" counter under a viral post might receive thousands of concurrent increments per second from a single popular key. A central counter service or CAS-based approach would see heavy contention specifically on that one hot key; a PN-Counter sharded across regions handles it gracefully since each region's writers only contend with OTHER writers in the SAME region (much lower local contention), converging the true global total asynchronously with no single bottleneck.
Trade-offs and pitfalls. Teams sometimes default to a central counter service for simplicity and only discover the bottleneck once a specific key goes viral or otherwise becomes hot, worth explicitly asking during design "what's the expected max increment rate on a SINGLE key," not just the aggregate system-wide rate, since that's what determines whether central/CAS approaches will hold up.
Unlock Full Question Bank
Get access to all Data Consistency and Distributed Transactions interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.