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.
You need a repair system capable of rewinding and reapplying events across replicas to fix a logical bug introduced in the event-processing code. Design the mechanism to snapshot state, replay events in a safe order, ensure idempotence during reapply, and minimize user-visible disruption. Provide rollback and verification steps.
Sample Answer
Direct answer: Rewinding and safely reapplying events to fix a bug that was introduced by buggy event-processing code requires snapshotting state before the reapply, replaying events in a deterministic, verifiable order, ensuring the reapply logic is itself idempotent (since a reapply attempt might itself need to be retried), and minimizing user-visible disruption by doing the repair on a copy or in a way that doesn't block live traffic.
Structured elaboration
Snapshot before touching anything. Before starting any repair, capture a full snapshot of the current (buggy) state, this is both your rollback point if the repair itself goes wrong, and your baseline for verifying the repair actually fixed what it was supposed to fix (diff the pre- and post-repair state against expectations).
Determine the correct replay starting point. Identify the earliest point where the buggy logic could have produced incorrect results (typically, the deployment time of the bug, or earlier if the bug could have affected historical processing retroactively), and confirm the FIXED event-processing logic is what will be used for the replay, replaying with the same buggy code obviously reproduces the same bug.
Replay in a safe order. Events must be replayed in the same relative order they were originally processed (per aggregate/entity, at minimum; global ordering isn't usually necessary), out-of-order replay can produce a DIFFERENT, also-incorrect result even with correct processing logic, since many state transitions are order-dependent.
Idempotence during reapply. The replay process itself needs to be safe to interrupt and resume: if the repair job crashes partway through, restarting it should not double-apply events already reprocessed. This typically means tracking a replay checkpoint (a per-entity or per-partition high-water mark of what's already been redone) and being able to resume from there, exactly the same idempotency discipline used elsewhere in this topic, applied to a repair job instead of live traffic.
Minimizing user-visible disruption. Where possible, run the replay against a SEPARATE copy of the state (a shadow store) first, validate the result there, and only then cut over (or apply the diff) to the live system in a controlled way, rather than replaying directly against production data while it's serving live traffic, which risks exposing partially-repaired, inconsistent state to users mid-repair.
Verification. After the replay, compare the repaired state against independent expectations (a known-correct sample, an invariant check like "total conserved" for a financial domain, or a diff against what the CORRECT logic would have produced from scratch on a subset), before considering the repair complete and before removing the snapshot taken at the start.
Worked example. A bug in an inventory-adjustment event processor was double-counting a specific event type for three weeks before being caught. Repair: snapshot current inventory state; identify the affected event types and the time window; build a shadow copy of the inventory read-model and replay ALL events from before the bug window using the now-fixed processor logic; verify the shadow copy's totals against a small set of manually-audited SKUs known to be correct; once verified, apply the diff between the shadow copy and the live (buggy) state as a single, logged, auditable correction to production, rather than replacing the live state wholesale (which would risk losing any legitimate changes that happened AFTER the replay's snapshot point but before the correction is applied).
Trade-offs and pitfalls. Replaying directly against the live, currently-serving state (skipping the shadow-copy step) is the highest-risk shortcut here: any bug in the REPLAY logic itself (not just the original processing bug) is now live and user-visible immediately, with no isolated environment to catch it first.
Architect a transactional outbox pattern for reliably publishing domain events when committing a write to the primary database. Include the outbox table schema, the reader design (and how you'd compare a polling reader against a change-data-capture-based reader), ordering guarantees, idempotency handling on the consumer side, and how you would scale the reader for high throughput.
Sample Answer
Direct answer: A production-grade transactional outbox service has three pieces: the application's database with an outbox table written in the same local transaction as the business change, a relay/reader that publishes unpublished rows to the broker (polling or CDC-based, CDC meaning change-data-capture), and enough retry/dedup discipline on both sides that the whole thing delivers at-least-once without ever silently losing an event.
Structured elaboration
Outbox table schema. At minimum: id (primary key, also usable as an idempotency/dedup token), aggregate_id (what business entity this event is about, often used as the broker partition key to preserve per-entity ordering), event_type, payload (serialized event body), created_at, and published (boolean, or a published_at timestamp, null until published). An index on (published, created_at) supports the "give me the oldest unpublished rows" query the reader needs.
Reader design: polling vs streaming. A polling reader periodically queries for published = false rows, simple to build and reason about, but adds latency bounded by the poll interval and puts a steady, small query load on the database. A CDC-based (streaming) reader instead taps the database's write-ahead log or replication stream (e.g. via Debezium) to be notified of new outbox rows as they're written, giving near-real-time delivery with no polling load, at the cost of running and operating CDC infrastructure and a slightly more complex failure story (the CDC connector itself can fall behind or need to resume from a checkpoint).
Ordering guarantees. If per-entity ordering matters (e.g. all events for order 501 must be delivered in the order they were created), the reader must publish using aggregate_id as the partition/routing key, so a partitioned broker (like Kafka) preserves order within that key even while allowing parallelism across different keys. Global ordering across all entities is usually not worth the throughput cost and isn't needed by most consumers.
Idempotency handling on the consumer side. Since the relay guarantees only at-least-once delivery, every event's payload includes a stable ID (the outbox row's id, or a domain-level event ID) that consumers use to deduplicate, typically via a small "seen event IDs" store with a bounded retention window matched to how long redelivery could realistically be delayed.
Scaling the reader for high throughput. A single polling reader eventually becomes a bottleneck; horizontal scaling requires either sharding the outbox table (e.g. by a hash of aggregate_id) with one reader instance per shard, or, for a CDC-based reader, letting the CDC connector's own partitioning handle parallelism. Batching reads and publishes (rather than one row per query/publish round-trip) is usually the first and cheapest throughput lever before reaching for sharding.
Worked example. An orders platform doing 2,000 writes/sec needs its outbox reader to keep pace. A single polling reader batching 200 rows per poll every 200ms caps out at 200 / 0.2s = 1,000 rows/sec, below the required rate regardless of how the rest of the system is tuned. Moving to a CDC-based reader (Debezium tailing the outbox table's WAL, write-ahead log) removes the poll-interval ceiling entirely, since new rows are picked up as they're written to the log rather than on a fixed cadence, at the cost of adding a Kafka Connect cluster and its own on-call burden that didn't exist with the simpler polling design; the actual sustained throughput and delivery latency the CDC path achieves would need to be measured against the real database's write-ahead log volume and Kafka Connect's own configured parallelism, not assumed from the pattern alone.
Trade-offs and pitfalls. Teams often under-invest in the outbox table's own cleanup: published rows accumulate forever unless there's a retention/archival job, which eventually degrades both the outbox query performance and, for CDC-based readers, the size of the write-ahead log the CDC connector has to process on any resume-from-checkpoint scenario.
Design a saga coordinator that supports long-running transactions which include human approvals and manual compensations. Describe the state machine, durable state storage, visibility for operators, timeout and escalation policies, and how to ensure auditability and idempotent retries of compensating steps.
Sample Answer
Direct answer: A saga coordinator that supports long-running transactions with human approvals needs an explicit, durable state machine (not just a sequence of automated steps) with a distinct "awaiting approval" state, timeout and escalation policies for approvals that never come, and the same durable-state-plus-idempotent-compensation discipline as any other saga, extended to cover a human-in-the-loop step that can take hours or days rather than milliseconds.
Structured elaboration
State machine design. Beyond the usual step states (pending, in_progress, completed, compensating, compensated), a human-approval step introduces awaiting_approval as a first-class state, entered when the automated part of a step completes and a human decision gates the next one (e.g. a large refund needing manager sign-off). The saga sits in this state, potentially for a long time, without any automated retries firing.
Durable state storage. The full saga state (current step, all prior steps' outcomes, any approval requests issued and their IDs) is persisted to a durable store on every transition, exactly like an automated saga, so an orchestrator restart doesn't lose track of a saga that's been sitting in awaiting_approval for two days.
Visibility for operators. Since a human has to act on the saga, there needs to be a queryable view (a dashboard or queue) listing every saga currently awaiting_approval, who it's assigned to or eligible to act on it, how long it's been waiting, and enough context (what's being approved, why) for the approver to decide without digging through logs.
Timeout and escalation policies. An approval request that sits unanswered past a threshold should escalate, notify a different approver, page an on-call, or auto-escalate to a higher authority level, rather than silently stalling forever. Some designs also define an explicit "default" outcome for a timed-out approval (auto-deny is usually safer than auto-approve for anything consequential).
Auditability and idempotent retries of compensating steps. Every approval decision (who approved, when, any notes) is recorded immutably alongside the saga's state, both for compliance and so a later dispute can be traced. Compensating actions triggered after a rejected approval must be idempotent exactly like any other saga compensation, since an operator might reject the same request twice (e.g. a UI double-click), or a retry might be triggered by an infrastructure blip rather than a genuine second action.
Worked example. A saga for a $50,000 vendor payment reaches step "approve large payment," transitions to awaiting_approval, and creates an approval request visible in an approvals queue with the amount, vendor, and originating order context. Two approvers are eligible; the saga waits. After 24 hours with no response, an escalation job fires: it notifies a backup approver and pages a manager. The manager approves at hour 30; the saga transitions to approved and proceeds to the payment-execution step, using the approval-request ID as part of the idempotency key for that step so a duplicate approval click doesn't trigger two payments.
Trade-offs and pitfalls. A common design mistake is treating the approval step like any other automated step with a short timeout and automatic retry, which either spams the approver with duplicate notifications or, worse, silently drops the request if a retry policy assumes failure after a short window. Human-in-the-loop steps need their OWN timeout scale (hours/days, not seconds) and their own explicit escalation path, not an automated saga's default retry/backoff policy.
Compare orchestration versus choreography when implementing sagas across microservices. Produce a decision matrix covering coupling, observability, error handling, and versioning, and give criteria for when you would recommend each approach.
Sample Answer
Direct answer: Choreography fits a small number of loosely-coupled services where each step's failure handling is simple, since there's no central process to build or operate; orchestration fits workflows with more than a handful of steps, complex or conditional failure handling, or a need for centralized visibility into where every saga instance currently stands.
Structured elaboration
Decision matrix
| Dimension | Choreography | Orchestration |
|---|---|---|
| Coupling | Low: each service only needs to know which events to listen for and emit, not the identity or existence of other services | Higher: services expose commands the orchestrator calls directly, and the orchestrator needs to know about every participant |
| Observability | Hard: the workflow's state is implicit, spread across every service's event log; understanding "where is saga X right now" requires correlating events across services | Easy: the orchestrator holds the saga's explicit state machine in one place, straightforward to query and dashboard |
| Error handling | Gets tangled quickly as the number of steps grows, each service has to know what to do on every relevant failure event, and cyclic or conditional failure logic becomes hard to express cleanly | Centralized: the orchestrator's state machine can express arbitrary branching and compensation logic explicitly, in one place |
| Versioning | Changing the workflow (adding a step, changing order) means coordinating event-contract changes across every affected service | Changing the workflow is mostly a change to the orchestrator's logic; participant services just implement the same command/compensation interface |
| Number of participants | Scales well with few services (2-4); each new participant multiplies the event-contract surface every existing service might need to know about | Scales better as participant count grows, adding a step is adding one more call from the orchestrator, not touching every existing participant |
When to recommend choreography. A 2-3 service flow with straightforward, largely linear failure handling (if step 2 fails, always compensate step 1, no branching), where teams value not having a shared piece of orchestration infrastructure everyone depends on, and where the participating services are otherwise fully independent (no team already owns cross-service workflow logic).
When to recommend orchestration. More than about 4-5 services, conditional or branching compensation logic (different failure handling depending on WHICH step failed or WHY), a compliance or operational need to see "where is this specific transaction right now" without correlating distributed logs, or a workflow that changes often enough that touching every participating service's event contract on each change would be a real cost.
Worked example. A 2-service "reserve inventory then send confirmation email" flow is a reasonable choreography candidate: Inventory service reserves and publishes Reserved, Notification service (subscribed to Reserved) sends the email; if reservation fails, Inventory publishes ReservationFailed and nothing else needs to react. A 6-service order-fulfillment flow (Order, Inventory, Payment, Fraud-check, Shipping, Notification) with different compensation logic depending on which step failed (a fraud rejection needs different handling than a payment decline) is a stronger orchestration candidate: expressing that branching logic as a web of event subscriptions across 6 services becomes hard to reason about and debug.
Trade-offs and pitfalls. Teams sometimes start with choreography for its lower initial coupling and later regret it as the workflow grows past 3-4 services and failure handling gets more conditional, at which point migrating to orchestration means introducing a new piece of shared infrastructure AND unwinding event contracts multiple teams already depend on. It's usually cheaper to start orchestrated for anything expected to grow past a handful of steps, even though it means one more service to build up front.
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.
Unlock Full Question Bank
Get access to all 46 Data Consistency and Distributed Transactions interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.