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.
Design a conflict detection and resolution strategy for a piece of user data that may be updated concurrently at multiple regions (for example, user preferences, or a shopping cart with offline mobile edits). What options would you weigh, and how would you present a merge conflict to the user when automatic resolution isn't confidently correct?
Sample Answer
Direct answer: For data that can be updated concurrently at multiple regions, the right conflict-resolution strategy depends on what the data actually represents: last-write-wins for genuinely replace-semantic fields, CRDTs for anything where both concurrent contributions should survive automatically, version vectors when you need to DETECT a conflict precisely but resolution requires more context than a generic rule can provide, and application-level merge (informed by user intent) when the data has domain-specific meaning a generic rule would get wrong.
Structured elaboration
Last-write-wins. Simplest option; correct choice when the field has a genuine "replace" semantic and losing an older concurrent write is a non-issue (see the LWW discussion elsewhere in this topic for where this holds and where it doesn't).
CRDTs (state-based or op-based). Correct choice when the data type has a well-defined, generic merge function that preserves both concurrent contributions automatically (counters, sets, sequences), no application-specific judgment needed at merge time, the merge function itself IS the resolution.
Version vectors (as a detection mechanism, not a resolution mechanism on their own). Useful when you need to reliably DETECT that a conflict exists (distinguishing "genuinely concurrent" from "one clearly happened after the other") but the actual resolution needs more context than a generic rule can supply, version vectors tell you THAT there's a conflict; something else (LWW, application logic, a human) has to decide WHAT to do about it.
Application-level merge based on user intent. The right choice when the data is domain-specific enough that no generic structural rule (LWW, CRDT, or otherwise) captures the correct resolution, the application needs custom logic that understands what the data MEANS. A concrete case: two concurrent edits to a user's notification preferences, where the "correct" merge might depend on business rules like "the more restrictive (less permissive) setting wins" for a privacy-sensitive toggle, a domain judgment call, not something a generic CRDT or LWW rule would know to apply.
Worked example: user preference updates. A user changes their notification preferences from their phone (enabling email notifications) while, concurrently and offline, changing them from their laptop (disabling ALL notifications, a broader privacy-oriented change). A naive LWW resolution would pick whichever device's write has the later timestamp, potentially resulting in email notifications being enabled even though the user's OTHER, more recent-feeling intent (from their perspective) was to turn things off. An application-level merge rule, informed by the specific business/privacy semantics of THIS field ("prefer the more restrictive setting on conflict" is a reasonable, defensible product decision here), produces a result that matches what the user probably actually wanted, better than either LWW or a generic CRDT structural rule would, since neither of those understands that "disable all" should take precedence over "enable one channel" for a privacy-sensitive toggle.
Presenting merge conflicts to users when automatic resolution isn't confident. For genuinely ambiguous cases where even a domain-informed rule can't confidently decide (e.g. two concurrent, materially different, both-plausible edits to a text field), the interface can show both candidate values with metadata about their origin (which device, when) and let the user explicitly choose or merge manually, rather than the system guessing silently, this converts a potential silent-data-loss bug into a small, transparent moment of user friction, generally the better trade for anything where getting it wrong silently would be worse than asking.
Handling a conflict storm. These strategies also need to hold up under volume, not just for one isolated conflicting pair. After a large network partition heals, many keys can surface divergent updates all at once, a conflict storm, and the resolution strategy above needs an operational playbook for that scale: triage which conflicts are high-value (revenue-affecting, security-relevant) versus low-stakes and can auto-resolve via the default rule; apply automated mitigation for the low-stakes majority immediately; and have a safe rollback path (freezing further writes to the affected keys, or serving from a known-good snapshot) if the automated resolution itself turns out to be producing bad results, with clear communication to stakeholders about the scope and expected resolution time while it's ongoing.
Operational and correctness trade-offs. Application-level merge logic is the most POWERFUL option (it can encode exactly the right domain behavior) but also the most bespoke and hardest to get uniformly correct, every new field with custom merge logic is new code that needs its own tests and its own review for correctness, unlike a CRDT or LWW rule that, once implemented, generalizes safely to every field that uses it.
Design the UX and engineering approach to expose eventually-consistent data to end users while minimizing confusion and incorrect actions. Walk through a concrete example: what should the interface actually show while the data might still be catching up, and what should the ACTING user's own experience look like versus everyone else's?
Sample Answer
Direct answer: Expose eventually-consistent data to users by combining read-your-writes for the user's own actions (so their own changes always look correct to them), causal or version metadata carried through the UI so the client can detect and gracefully handle stale reads, and explicit UI reconciliation (a subtle "updating..." indicator or a merge prompt) rather than either hiding staleness entirely or confusing the user with an unexplained flicker.
Structured elaboration
Read-your-writes as the baseline. Whatever else the design does, the ACTING user should always see their own action reflected immediately, this alone eliminates the most common and most confusing eventual-consistency symptom (a user does something and it looks like it didn't work). Implemented via the mechanisms discussed elsewhere in this topic (sticky routing, version tokens).
Causal/version metadata. Every piece of eventually-consistent data the UI displays carries a version or timestamp the client can use, both to detect when a NEWER version has become available (prompting a lightweight refresh rather than the user having to guess something changed) and to avoid the UI regressing to an OLDER value if a later request happens to be served by a more-stale replica (a monotonic-reads violation from the user's point of view, and a real UX bug if not guarded against).
UI reconciliation patterns. Rather than silently showing possibly-stale data as if it were definitely current, or blocking the UI until a strongly-consistent read completes (defeating the purpose of using eventual consistency in the first place), the interface signals uncertainty appropriately to the situation: a subtle "syncing..." indicator for data that's expected to catch up within a second or two, an explicit "this may not reflect the latest changes" note for longer-lag scenarios, or, for genuinely conflicting concurrent edits, a merge-conflict prompt letting the user see and choose between divergent versions.
Order-status example, concretely. An order-status page shows "Processing" immediately after checkout (this status write is the user's OWN action, read-your-writes applies, no lag here). As the order progresses through downstream services (payment confirmation, inventory allocation, shipping label creation), each of those services' writes propagate to the status page ASYNCHRONOUSLY, the status page might briefly show "Processing" for a few seconds after payment has actually already been confirmed elsewhere. The design mitigates this with: an optimistic UI update the moment the user's own action (placing the order) is confirmed (read-your-writes), a lightweight polling or push-based refresh (not the user having to manually reload) that updates the displayed status as soon as new information propagates, each status carrying a timestamp shown subtly to the user ("updated 3s ago") so the display doesn't implicitly claim to be instantaneous, and, if the status hasn't updated within an expected window (e.g. no change after 30 seconds when a change was expected), a fallback to an explicit strongly-consistent check rather than leaving the user staring at a potentially-stale status indefinitely.
Testing user-facing correctness. Beyond the backend correctness tests discussed elsewhere in this topic (convergence, monotonicity), UI-level testing specifically injects realistic replication delay into a staging environment and verifies: the acting user's own view never shows a regression (their own action always reflected, per read-your-writes), the staleness indicator accurately reflects actual lag (not a hardcoded or misleading value), and the fallback-to-strong-check path actually fires and resolves correctly when the expected update doesn't arrive within the design's own stated window.
Trade-offs and pitfalls. A common design mistake is either fully hiding staleness (presenting eventually-consistent data with the same visual confidence as strongly-consistent data, which then produces a confusing "it says X but actually Y" moment when a user compares notes with someone else or refreshes at the wrong time) or over-communicating it (a persistent, anxiety-inducing "this data might be wrong" banner on every page, which erodes trust even when the actual staleness window is small and rarely matters), the better middle ground calibrates the UI signal to the actual, monitored staleness distribution for that specific data, not a blanket policy either way.
Design a saga orchestrator that guarantees exactly-once external side effects (like sending notification emails or charging cards) in the presence of retries and orchestrator crashes. Include durable command logs, deduplication of outbound commands, transactional outbox patterns, compensating actions, idempotency tokens for external systems, and reconciliation mechanisms for failures.
Sample Answer
Direct answer: To guarantee exactly-once external side effects (like sending an email or charging a card) despite retries and orchestrator crashes, the orchestrator needs a durable command log recording every side-effecting call it has made, keyed so a retried or resumed saga recognizes "I already issued this command" and skips re-issuing it, combined with idempotency tokens the external system itself can use to deduplicate on its side.
Structured elaboration
Durable command log. Before calling an external system (charge card, send email), the orchestrator writes a durable record: (saga_id, step, command_id, status=pending). It only sends the actual external call after that write succeeds. If the orchestrator crashes after the write but before getting a response, on recovery it finds the pending record and must resolve the ambiguity, not by blindly retrying (the original call may have already succeeded on the external system's side), but by checking status first where possible, or retrying with the SAME command_id as an idempotency token so the external system can recognize the duplicate.
Idempotency tokens for external systems. Any external side effect worth protecting this way needs to accept an idempotency key from the caller (most payment gateways and transactional email providers do): the orchestrator generates a stable command_id once per logical command and reuses it on every retry of that same command, so the external system either applies it once and returns the same result on retries, or explicitly rejects a duplicate.
Deduplication of outbound commands. Before issuing any command, the orchestrator checks its own log for an existing record with that command_id, if status=applied already, it skips the call entirely (no network round-trip needed, we already know the outcome); if status=pending, it either polls the external system's status endpoint (if one exists) or safely retries using the same idempotency token.
Transactional outbox pattern for writing the command log. When the decision to issue a side-effecting command is itself derived from a change the orchestrator is making to its own database (e.g. "saga step N just completed, therefore issue the notification-email command"), writing the (command_id, status=pending) row to the command log and committing the saga's own state change must happen atomically, otherwise you get the same dual-write problem the command log exists to prevent, just one level up: the saga state could commit while the command-log row is lost, or vice versa. The transactional outbox pattern is how this is made atomic: the pending command-log row is inserted in the SAME local database transaction as the saga's state update, and a separate relay (polling or change-data-capture based) is what actually issues the external call and marks the row applied, rather than the orchestrator calling the external system synchronously inline. This decouples "decide to send the command, durably" from "actually send it," and gives the orchestrator a natural resume point after a crash: any outbox row still pending is a command the relay has not yet confirmed was issued.
Compensating actions and reconciliation. If a step later in the saga fails and this side effect needs to be "undone" (refund a charge, can't literally un-send an email), the compensating action is itself logged and issued the same way, with its own idempotency token. A periodic reconciliation job cross-checks the command log against the external system's own record of what was applied (where the external system offers a lookup API), to catch and alert on any command stuck in pending for longer than expected, evidence of a bug or an external outage rather than a normal race.
Worked example. Saga step "charge card" generates command_id = S-991-charge. Orchestrator logs (S-991, charge, S-991-charge, pending), then calls the payment gateway's charge API with Idempotency-Key: S-991-charge. The call times out with no response (ambiguous: could have succeeded server-side). Orchestrator does NOT immediately retry blindly assuming failure; it either calls the gateway's "look up by idempotency key" endpoint if available, or safely retries the SAME charge call with the same key, the gateway either recognizes the duplicate and returns the original result, or, if the first attempt genuinely never reached it, applies it now. Either way exactly one real charge happens, and once the orchestrator gets a definitive applied response it updates the log to status=applied and proceeds.
Trade-offs and pitfalls. The recurring failure mode is generating a NEW command_id on every retry instead of reusing the same one, that defeats the whole point, since the external system can no longer tell a retry from a genuinely new request, and you're back to the double-charge/double-email risk this design exists to prevent. The command_id must be stable per LOGICAL command (tied to the saga+step), generated once and reused for every retry attempt of that specific command.
Design API semantics and a contract to allow clients to retry complex 'create-with-side-effects' operations safely (for example: create-order that triggers inventory reservation and payment). Define idempotency key structure, client responsibilities, server guarantees (at-most-once vs idempotent-create), visibility of side-effects to users, and error semantics.
Sample Answer
Direct answer: For a create-with-side-effects operation like create-order (which triggers inventory reservation and payment), the API contract needs a client-generated idempotency key that scopes the ENTIRE multi-step operation, not just the first HTTP call, so a client retry after a timeout re-resolves to the same underlying order and side effects rather than triggering a second one; the server tracks the operation's progress against that key and returns a consistent result regardless of how many times the client retries.
Structured elaboration
Idempotency key structure. The client generates a key once per logical intent (e.g. a UUID generated when the "place order" button is clicked, reused on every retry of that same click, never regenerated on retry). The server stores this key alongside the operation's current state and final result once known, exactly the shape used for a single-endpoint idempotent write, but here the "operation" spans multiple internal steps (order creation, inventory reservation, payment) rather than a single database write.
Client responsibilities. Generate the key once and persist it (e.g. in local state) before the first attempt, reuse the SAME key on every retry of the same logical action, and never reuse a key for a genuinely different order (e.g. a second, separate purchase needs its own key).
Server guarantees: idempotent-create vs at-most-once. The contract promises idempotent-create semantics: retrying with the same key is guaranteed to return the result of the FIRST successful attempt (or the current in-progress status), never trigger a second inventory reservation or a second charge, this is stronger than plain at-most-once (which would just refuse to retry at all) and is what actually lets clients retry safely after an ambiguous failure like a timeout.
Visibility of side effects to users. Because the operation spans multiple steps that don't all complete instantly, the API returns a status field alongside the order/operation ID: pending (steps still in flight), completed (all steps succeeded), or failed (a step failed and compensations, if any, have run). A client polling on the same idempotency key (or the returned operation ID) gets a consistent, current view rather than each poll re-triggering work.
Error semantics. A definitive rejection (e.g. inventory genuinely unavailable) returns a failed status with a reason, and the idempotency key is now permanently associated with that failed outcome, a retry with the SAME key returns the same failure rather than re-attempting (since re-attempting the same operation with the same inputs would fail again anyway); a genuinely NEW attempt requires a new key, communicating the client's intent to try again as a fresh action, which matters if the failure was due to a transient condition the client wants to retry past.
Worked example. Client calls POST /orders with Idempotency-Key: 8f3c... and the order payload. The server hasn't seen this key before, so it starts the underlying saga (create order, reserve inventory, charge payment), stores status: pending against the key, and returns 202 Accepted with an operation ID. A network blip means the client never sees this response, and retries the SAME POST /orders call with the SAME idempotency key. The server recognizes the key, sees the saga is still pending, and returns the current status (again 202, same operation ID) WITHOUT starting a second saga. The client can now poll GET /orders/{operation_id} (itself naturally idempotent, a read) until it sees completed or failed.
Trade-offs and pitfalls. A common design mistake is treating the idempotency key as scoping only the first HTTP request rather than the whole underlying operation, if the key is discarded once the initial 202 response is sent, a later retry re-triggers the ENTIRE saga from scratch, defeating the purpose. The key must remain valid and checked for the full lifetime of the operation it protects, not just the initial acknowledgment.
Explain session guarantees: read-your-writes, monotonic reads, monotonic writes, and write-follows-reads. Propose an implementation strategy for a client SDK to provide these guarantees against a multi-region replicated datastore, including how you'd persist the necessary metadata across devices and handle token expiry.
Sample Answer
Direct answer: Session guarantees are a set of promises a system can make to a SPECIFIC client about what it will see across its own sequence of reads and writes, weaker than full strong consistency (which promises something to every observer, not just one client's own session), but strong enough to avoid the most confusing user-facing anomalies. The four common ones: read-your-writes (a client always sees its own prior writes), monotonic reads (a client never sees data go backward in time on successive reads), monotonic writes (a client's writes are applied in the order it made them), and write-follows-reads (a client's write is guaranteed to be ordered after any write it has already observed).
Structured elaboration
Read-your-writes. After a client successfully writes something, every subsequent read BY THAT CLIENT reflects that write (or something newer), even if the underlying system is eventually consistent for OTHER clients. Without it: a user updates their profile, refreshes, and sees the old value, confusing even though the system is "eventually" correct.
Monotonic reads. Once a client has read a value at some point in time, it will never later read an OLDER value, even if it's routed to a different, lagging replica on a subsequent request. Without it: a user refreshes a page twice and the second refresh shows STALER data than the first, which reads as the system going backward.
Monotonic writes. A client's own writes are applied in the order the client issued them, even if they're processed by different replicas or arrive out of order over the network. Without it: two edits from the same user could be applied out of order, silently losing the later edit's effect if the earlier one "wins" by arriving second.
Write-follows-reads. If a client reads value V and then makes a write based on what it read, that write is guaranteed to be applied AFTER (causally ordered after) the write that produced V, even on a different replica. Without it: a user could see a comment, reply to it, and have their reply become visible to others before the original comment they're replying to is, a causally backward-looking result.
Implementation strategies for a client SDK
Sticky sessions (server-side). Route a given client's requests to the SAME replica for the duration of a session, so its own reads trivially see its own writes (since they're the same node). Simple, but limits load-balancing flexibility and doesn't survive the sticky replica failing over.
Client-side version tokens / causal tokens. The server returns a version token (a timestamp, a vector clock, or a simple monotonic counter) with every response; the client includes the LATEST token it has seen with every subsequent request; the server serving that request ensures the replica it reads from is at least as current as that token (waiting briefly, or routing to a sufficiently up-to-date replica) before responding. This works across server restarts and load-balancer changes, at the cost of a small amount of extra state the client (or an SDK on its behalf) needs to carry and the server needs to check against.
Metadata persistence and token expiry. For a client SDK, the causal token is typically stored alongside whatever session state the client already keeps (in-memory for a single session, or persisted, e.g. in a mobile app's local storage, if the guarantee needs to survive an app restart). A token needs no explicit expiry for correctness (an old, stale token just means the server might do a bit more waiting to catch up, never incorrect behavior), though in practice you might cap how large it can grow (e.g. compacting a vector-clock-style token) for efficiency.
Persisting across devices. Storing the causal token only in a single device's local storage (as described above) loses the guarantee the moment a user switches devices, a phone-issued token never reaches the laptop the user opens next, so the laptop's first read looks like a fresh, tokenless session and can show stale data relative to a write the user just made on the phone. To make the guarantee follow the USER rather than the device, the server should also persist the client's latest known token keyed by the authenticated user's account (not just return it to the device and forget it), and the SDK should fetch that stored token as part of session bootstrap on login, merging it with (or preferring it over) whatever token the local device already has, before issuing its first read. This costs a small amount of server-side storage per user and an extra lookup at login, but it's what keeps read-your-writes intact across a device switch instead of silently resetting.
Trade-offs and pitfalls. Confusing "the system is eventually consistent" with "session guarantees don't matter" is a common design mistake, an eventually-consistent system with NO session guarantees produces a genuinely confusing user experience (data appearing to go backward, a user not seeing their own actions reflected), while the same system WITH session guarantees can feel completely normal to users despite being eventually consistent under the hood for everyone else.
Unlock Full Question Bank
Get access to all 41 Data Consistency and Distributed Transactions interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.