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.
Explain last-write-wins (LWW) conflict resolution in plain terms. Describe two scenarios where it's an acceptable resolution strategy and two where it's dangerous.
Sample Answer
Direct answer: Last-write-wins (LWW) resolves a conflict between two concurrent updates by simply keeping whichever one has the later timestamp and discarding the other, no merging, no user involvement, just pick one. It's acceptable when losing the discarded update silently is a genuinely low-stakes outcome; it's dangerous when the discarded update carried information that mattered and shouldn't have been thrown away.
Structured elaboration
Mechanism. Every write carries a timestamp (or a similar monotonically-comparable value). When two replicas' values for the same key disagree, whichever has the LATER timestamp is kept as the resolved value; the other is discarded entirely, with no trace and no attempt to combine the two.
Where LWW is acceptable. A user's "last active device" field, where the whole point IS to reflect the most recent update and there's no meaningful sense in which an older value should be preserved or merged. A cache of a frequently-refreshed external value (like a cached stock price snapshot), where an older concurrent write being discarded in favor of a newer one is exactly the desired behavior, not data loss. Any field where two concurrent writes represent genuinely REPLACING information rather than two pieces of information that both need to be kept.
Where LWW is dangerous. A shopping cart's item list: if LWW is applied naively to the whole cart object, two devices concurrently adding DIFFERENT items results in one device's additions being silently discarded entirely, real data loss a user would notice and be frustrated by. A counter or accumulator: LWW applied to "total items sold" would drop one of two concurrent increments rather than correctly summing them, an outright correctness bug, not just a UX nuisance. Any field relying on wall-clock timestamps for ordering when clock skew between replicas is a realistic risk, since LWW's correctness depends entirely on timestamps accurately reflecting real recency, a replica with a clock running even a few seconds fast can have ALL its writes incorrectly "win" against genuinely more recent writes from a correctly-clocked replica.
Worked example. A collaborative task list applies LWW at the WHOLE-LIST level (a common early design mistake): user A, offline, adds "buy milk"; user B, also offline and unaware of A's edit, adds "call dentist" to their own copy. On reconnect, LWW compares the two list versions' timestamps and keeps only ONE user's entire list, silently discarding the other user's addition entirely, "buy milk" or "call dentist" is gone, not because of a bug, but because that's exactly what LWW is designed to do: pick one and discard the other. The fix isn't a "better" LWW, it's recognizing this field needed a different merge strategy entirely (an OR-Set, in this case, which would have kept both additions).
Trade-offs and pitfalls. LWW's appeal is its simplicity, no merge logic to design or reason about, but that simplicity is exactly what makes it a trap when applied by default to a field that actually needed real merge semantics; the discipline worth internalizing is asking, for every field, "if two concurrent writes to this happen, is discarding one of them actually an acceptable outcome," before defaulting to LWW rather than after a customer reports lost data.
An enterprise needs eventual consistency between service A and service B using events. Design an idempotent event processing and reconciliation strategy that guarantees convergence and supports replays, while preserving ordering where necessary.
Sample Answer
Direct answer: To make eventual consistency between service A and B idempotent and reconciliation-friendly, service A publishes events with a stable event ID (or a monotonic sequence number per entity), service B's consumer deduplicates on that ID before applying any change, and a periodic reconciliation job independently compares A's and B's views to catch and repair anything that slipped through despite the idempotency guarantees.
Structured elaboration
Idempotent event processing on the consumer side. Every event from A carries a stable identifier; B's consumer checks (atomically, alongside applying the event) whether that ID has already been processed, using the same "dedup record plus the actual state change in one transaction" discipline as any idempotent write. This is what makes at-least-once delivery (which any reasonable messaging setup between A and B will actually provide) safe: redelivery is a no-op rather than a duplicate application.
Preserving ordering where necessary. If events for the same entity must be applied in order (e.g. "created" before "updated" before "deleted"), B's consumer needs either a strictly-ordered delivery channel per entity (partition by entity ID) or an explicit sequence number in each event that B checks against the last-applied sequence for that entity, rejecting or buffering an out-of-order arrival rather than applying it prematurely.
Supporting replays. Because B's state can, despite everything, still drift from A's (a bug, an extended outage, a schema-migration mistake), the design should support REPLAYING A's full event history into B from scratch (or from a checkpoint) to rebuild B's view, which requires A to retain (or be able to regenerate) its event history for at least as long as any realistic replay window, and requires B's apply logic to be safe to run repeatedly over the same events (which it already is, by the idempotency design above).
Reconciliation as the safety net, not the primary mechanism. A periodic job independently compares A's and B's data (via checksums, row counts, or a full diff on a schedule appropriate to the data's size and criticality) and either auto-repairs small, well-understood divergences or flags larger ones for human review. This is deliberately a SEPARATE mechanism from the event-driven sync path, its job is to catch failures of that path (a dropped event no retry ever recovered, a bug in the consumer's apply logic), not to be the primary way B stays in sync (that would defeat the point of event-driven propagation in the first place).
Worked example. Service A (an Orders service) publishes OrderUpdated{order_id, sequence, payload} events. Service B (a search index) consumes them, checking (order_id, sequence) against the last sequence it applied for that order, skipping (as an idempotent no-op) any event with a sequence it's already seen or older, and buffering (briefly) any event that arrives out of order, applying it once the gap is filled or timing it out into a "request full replay for this order_id" fallback if the gap doesn't close. Nightly, a reconciliation job compares a sample (or full set, for smaller datasets) of orders between A's source of truth and B's index, flagging any order where B's data doesn't match A's for investigation, this is how the team discovered a bug where B's consumer was silently dropping events during a brief scaling event, well before any customer noticed stale search results.
Trade-offs and pitfalls. Skipping the reconciliation job because "the event pipeline is reliable" is a common and risky shortcut, event-driven consistency mechanisms fail in ways that are often invisible until reconciliation (or a customer complaint) surfaces them, since a missed event usually produces no error, just quietly stale data.
Explain the difference between read-after-write guarantees and causal consistency. Give a concrete multi-service example where causal consistency is required for correctness, and describe how you would design for and verify it in production.
Sample Answer
Direct answer: Read-after-write is a promise scoped to one client about its OWN writes: after you write something, your next read reflects it. Causal consistency is broader: it preserves the order between any operations that are causally related (one happened because of, or in response to, another), even across different clients, so a reply is never visible before the comment it's replying to, regardless of who's reading.
Structured elaboration
Read-after-write, precisely. Guarantees a SINGLE client sees its own writes reflected in its own subsequent reads. Says nothing about what a DIFFERENT client sees, or about the ordering between two DIFFERENT clients' operations, even if those operations are causally related from a human's point of view.
Causal consistency, precisely. Guarantees that if operation B was causally dependent on operation A (B happened after seeing A's effect, e.g. a reply written after reading the comment it replies to), then EVERY client that sees B must also see A (and see it first). Operations with no causal relationship (concurrent, independent writes) have no ordering guarantee between them, which is what distinguishes causal consistency from full strong consistency, it's ordering-preserving only where a real causal link exists, not globally.
Why this distinction matters for correctness across services. Read-after-write alone is insufficient in a multi-user, multi-service system: it says nothing about user B seeing user A's action in the order A actually did things. A concrete example: user A posts a comment, then user A ALSO likes their own comment. With only read-your-writes, user A sees both actions reflected in their own view fine, but a THIRD user, B, viewing the thread might, due to independent replication paths, see the "like" appear before the comment it's attached to even exists, from B's point of view, a like on nothing, a causally impossible ordering. Causal consistency specifically prevents this: since the like causally depends on the comment (you can't like a comment you haven't made), the system guarantees anyone who sees the like has already seen the comment.
Concrete example needing causal consistency. A comment-reply thread: user A posts "What time is the meeting?", user B, having READ that comment, replies "3pm". A third user, C, must never see B's reply without also seeing A's original comment, that would be a nonsensical, causally backward view (a reply to a question no one asked, from C's perspective). Read-after-write alone doesn't prevent this (it only governs what B sees of B's OWN writes); causal consistency specifically governs what C sees, given the causal link between A's comment and B's reply.
How you'd design for and verify it in production.
Design. Track causal dependencies explicitly, most commonly via vector clocks or a simpler "the client includes the version/ID of the data it read, as part of the metadata attached to its subsequent write" (a causal token, similar to the read-your-writes token, but propagated to the SERVER as part of the write, not just used by the client for its own reads). The replication/delivery layer then withholds delivering a causally-dependent write to any reader until the write(s) it depends on have also been delivered to that reader.
Verification in production. Since this is hard to observe directly (an anomaly is the ABSENCE of a causal ordering violation, not a positive signal), verification usually combines: synthetic canary transactions (deliberately create a known causal chain, e.g. write A then a dependent write B from a controlled test client, then check from multiple regions/replicas whether B ever appears without A), and passive monitoring using the causal metadata itself (if every write carries its causal dependencies, an anomaly detector can flag any observed read that shows a write without its declared dependency also present).
Trade-offs and pitfalls. A common design mistake is assuming read-your-writes is "basically the same as" causal consistency because both involve "seeing things in the right order," they solve genuinely different problems (one client's own view, versus cross-client causal ordering), and a system can have one without the other, most systems that offer only read-your-writes are explicitly NOT protecting against the cross-user causal-ordering anomaly described above.
Discuss the trade-offs between throughput and consistency when designing a service that requires high write throughput. What metrics would you collect to quantify the trade-off, and what patterns let you move some operations to eventual consistency while preserving correctness on the critical paths?
Sample Answer
Direct answer: The throughput-consistency trade-off shows up as added latency, coordination overhead, and reduced write concurrency the stronger your consistency guarantee gets; the metrics that quantify it are write latency (p50/p99), achievable write throughput per shard/partition, and lock/contention wait time, and the pattern for reclaiming throughput is to selectively relax consistency on the paths that can tolerate it while keeping strong guarantees only where correctness genuinely requires them.
Structured elaboration
Why the trade-off exists mechanically. Strong consistency requires coordination, a single leader serializing writes, or a quorum of replicas confirming before a write is acknowledged, and coordination costs time (a network round-trip, at minimum) and limits how many writes can be in flight concurrently without conflicting. Eventual consistency skips that coordination: a write is accepted locally and propagated asynchronously, no round-trip wait, no serialization bottleneck, dramatically higher achievable throughput, at the cost of the staleness and conflict-resolution concerns covered elsewhere in this topic.
Metrics to collect. Write latency distribution (not just average, the P99/P999 tail is usually where coordination overhead shows up most painfully, since a quorum write's latency is bounded by its SLOWEST required replica, not the average one). Achievable write throughput per partition/shard under the current consistency model (directly comparable before/after a consistency-relaxation change). Contention/lock-wait time specifically (for a strongly-consistent single-writer model, how much time writes spend WAITING for a lock or leader slot, a direct signal of how much headroom exists before the coordination bottleneck becomes the limiting factor). Replication lag (for the eventually-consistent path, needed to know the ACTUAL cost being paid in staleness in exchange for the throughput gained, not just a theoretical estimate).
Patterns to move operations to eventual consistency while preserving critical-path correctness. Identify which specific writes are on a genuinely correctness-critical path (the small subset discussed in the checkout/inventory example elsewhere in this topic) versus the majority that aren't, and apply the SAME per-operation consistency-tagging approach as a hybrid-consistency API design: keep the critical subset strongly consistent, move everything else to an eventually-consistent, asynchronously-replicated path. Batch and buffer non-critical writes (accumulate several eventually-consistent writes and apply them together, amortizing coordination overhead, where a strongly-consistent alternative would pay that overhead per-write). Shard more aggressively for the strongly-consistent subset specifically, since sharding reduces per-shard write contention directly, letting you keep strong consistency WITHIN a shard while still scaling overall throughput across shards.
Worked example. A write-heavy service is bottlenecked on a fully strongly-consistent, single-leader-per-shard model, where each write waits for a quorum round-trip before being acknowledged. Profiling shows 90% of writes are low-stakes telemetry-adjacent updates that don't actually need strong consistency (a "last seen" timestamp, an activity counter), only the remaining 10% (account-balance-affecting operations) genuinely need it. For a coordination-bound write path, achievable throughput scales roughly inversely with per-write coordination latency (fewer, shorter waits per write means more writes fit in the same window); moving the 90% onto a locally-accepted, asynchronously-replicated path removes the quorum round-trip from those writes entirely, replacing it with a purely local acknowledgment. The DIRECTION and SHAPE of the win are what's derivable and defensible here (a large, multiplicative throughput increase on the relaxed 90%, since a local write is fundamentally faster than one that waits on a network round-trip to other replicas), the exact multiplier depends on the specific coordination latency and replica topology being replaced, and would need to be measured on the real system rather than assumed. The critical 10% keeps its unchanged strong-consistency guarantee and latency profile throughout, since it was never touched by the change.
A related judgment call: per-tenant consistency in a multi-tenant SaaS product. The same throughput-vs-consistency reasoning applies at the tenant level, not just the operation level: a multi-tenant platform might reasonably guarantee strong consistency for a tenant's configuration changes (critical, low-volume, and where staleness would be confusing and hard to explain support-wise) while running analytics and usage-metrics writes for the same tenants under eventual consistency (high-volume, tolerant of a short delay), the same per-operation-criticality logic from the applied-scenario answers elsewhere in this topic, applied here as the lens for sizing a specific throughput-consistency trade-off decision rather than an unrelated concern.
Trade-offs and pitfalls. A common mistake is measuring throughput improvement without ALSO measuring and monitoring the staleness cost being paid on the relaxed path, a throughput win that's actually causing user-visible staleness problems nobody's watching for isn't a clean win, it's a trade that was made implicitly rather than deliberately and monitored.
You are on-call: users report duplicate charges even though requests include idempotency keys. Provide a prioritized debugging checklist to find the root cause: logs/traces to inspect, DB constraints, idempotency key collisions or TTL expiry, races in key insertion, message replay sources, third-party gateway callbacks, and immediate remediation steps.
Sample Answer
Direct answer: With duplicate charges despite idempotency keys being present, the systematic debugging path is: confirm the SAME key was actually reused across the duplicate charges (not two different keys for what the client thought was one action), check for a race between concurrent requests with that key, check for a TTL (time-to-live) that expired and let a "retry" outlive the dedup record, and check whether the duplication happened upstream (client generating a new key per retry) or downstream (the payment gateway itself, or a message replay bypassing the key check).
Structured elaboration
Step 1: confirm it's actually the same idempotency key. Pull the logs/traces for both charges and compare their idempotency keys directly. If they're DIFFERENT keys, the bug isn't in the idempotency mechanism at all, it's in the CLIENT generating a new key per retry instead of reusing one, the single most common root cause of "idempotency keys didn't work" reports. This check should be first because it redirects the entire investigation if it's true.
Step 2: rule out an idempotency-key collision. Separately from a race, check whether the key itself has enough entropy and uniqueness, was it a properly generated UUID/random token, or was it derived from mutable or low-cardinality business fields (customer ID plus rounded timestamp, for example) that two DIFFERENT orders could plausibly produce the same value for? Also check whether an internal layer (a datastore column, a cache key, a hashing step) truncates or re-hashes the client's key in a way that could map two distinct client keys onto the same stored dedup record. A genuine collision more often shows up as a MISSED charge (the second, different order gets silently treated as a duplicate of the first) rather than a duplicate one, so it's a less likely direct cause here, but it's cheap to rule out early because its fix (widen the key's entropy, stop truncating/re-hashing it internally) is entirely different from a race or TTL fix.
Step 3: check for a key-insertion race. If it IS the same key, check whether both charge attempts' timestamps are close enough together to suggest they raced: did the second request's dedup-check run BEFORE the first request's transaction committed? This points to a missing or incorrectly-scoped database constraint (the unique-key insert and the charge creation weren't actually in the same atomic transaction), or a check-then-act pattern in application code instead of relying on the database's own uniqueness enforcement.
Step 4: check TTL and expiry. If the two charges are far apart in time (hours or more), check whether the idempotency-key record had already been cleaned up (expired past its TTL) by the time the "retry" arrived, an overly aggressive cleanup job can inadvertently let a legitimate late retry through as if it were a brand-new request. This is a config/policy bug, not a logic bug, worth distinguishing because the fix (adjust the TTL) is different from a code fix.
Step 5: check for replay sources. If the request path involves a message queue or webhook relay upstream of the idempotency check, confirm whether the SAME idempotency key made it into two separate messages (a bug in whatever's producing those messages) versus the idempotency-key check itself being bypassed on a replayed message (e.g. if replay happens at a layer that doesn't go through the same dedup code path as normal requests).
Step 6: check third-party gateway callbacks. If the payment gateway itself sends a callback/webhook confirming the charge, and that webhook is ALSO wired to trigger some local state change, verify the webhook handler is idempotent independently, a duplicate WEBHOOK delivery (common; most gateways explicitly warn about this) can cause a duplicate downstream effect even if the original charge request itself was correctly deduplicated.
Immediate remediation. Once the specific duplicate charges are confirmed (via the gateway's own transaction records, not just internal logs, since internal logs could themselves be the buggy layer), refund the duplicate charge and notify the affected customer proactively rather than waiting for them to notice, and flag the affected account for a manual review pass to catch any other duplicates from the same root-cause window.
Worked example. Investigation reveals: same idempotency key on both charges (step 1 clean), timestamps 4 hours apart (step 2 not applicable, too far apart to be a race), and the TTL policy is configured to clean up keys after 1 hour (step 3 hit). The retry, which happened after a genuinely long client-side outage, arrived after the key had already been purged, and was processed as if it were a brand-new request. Fix: extend the TTL to a value that comfortably covers realistic retry windows (informed by actual client retry-timeout configuration), and add a safety check that flags (rather than silently allowing) a charge request whose amount and customer exactly match a recently-expired key, as a defense-in-depth measure.
Trade-offs and pitfalls. Jumping straight to "the idempotency implementation is broken" without first confirming step 1 (same key, actually reused) wastes investigation time on the wrong layer; the debugging order above is deliberately structured to rule out the cheapest, most common causes first.
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.