We use cookies for site analytics. Accept to help us understand how the site is used. See our Privacy Policy for details.
Two-phase commit, sagas (choreography vs orchestration), TCC, idempotency keys, and the compensation logic that turns multi-service writes into something a customer-support agent can untangle.
Design a transaction protocol that spans multiple independent services - payments, inventory, shipping, notifications - where each owns its own database and you cannot use a single ACID transaction across them. The end-state must be consistent: either every service commits its piece, or every service rolls back. Failures (network blips, service crashes, duplicate retries) must not leave the system half-committed.
This is the question that separates "I read the saga blog post" from "I have run this in production". Strong candidates explain why 2PC is rare in practice, walk through a concrete saga (orchestration vs choreography), justify their idempotency story, and own the failure modes - what happens when a compensating action itself fails.
Asking these before diving into a solution is the difference between a "hire" and a "no signal" rating. Pick the questions whose answers would change your design.
Transaction log
Coordinator state
Idempotency cache
Compensation rate
Coordinator throughput per node
Latency budget for a 5-step orchestrated saga
The system layers a transaction coordinator on top of N participant services. The coordinator owns the durable transaction log; each participant owns its own database and exposes idempotent step + compensation APIs. The coordinator drives the state machine: start → step 1 → step 2 → ... → commit (or compensate-from-N → ... → compensate-from-1 → aborted).
The defining decisions: (1) coordinated (orchestration with a central saga executor) vs choreographed (each service publishes events and reacts), (2) compensating actions vs reservations (TCC), (3) where the idempotency boundary lives (coordinator-side keys vs participant-side dedup table), (4) what to do when a compensating action itself fails (escalation queue + human resolution).
The defining operational property: every transaction must reach a terminal state. A coordinator crash mid-saga is recoverable from the log; an unbounded "stuck" transaction is a bug, not a feature.
Stateless workers driving each transaction's state machine. Reads next step from the log, calls the participant, writes the outcome back. Sharded by transaction ID. The brain of the orchestration model.
Durable append-only record of every state transition (txn started, step started, step committed, compensation triggered). Source of truth for recovery. Kafka, DynamoDB streams, or a relational outbox table.
Owns its database. Exposes a step API (do-the-thing) and a compensation API (undo-the-thing). Both must be idempotent on the request key. Examples: payments service, inventory service, shipping service.
Maps (transaction_id, step_id) → outcome. Consulted on retry; populated on first ack. TTL'd at 30-90 days. Lives inside the participant or as a shared infra service.
When using choreography, services publish 'step completed' / 'step failed' events. Other services subscribe and act. Kafka or a managed pub/sub. Optional in pure orchestration setups.
When a step fails after others committed, the coordinator enqueues compensation jobs in reverse order. Jobs retry with backoff; persistent failures escalate to the human queue.
Background scanner finds transactions that have been in a non-terminal state past their SLA. Alerts on-call and surfaces them in the operator UI for manual resolution.
Read-only view of any transaction's state machine + per-step audit. Action buttons for force-compensate, force-commit, manual retry. Every action audit-logged.
For services that want atomicity between local DB write and event publish: write event row in same transaction as state change; background publisher reads the outbox and publishes to the bus. Defeats the dual-write problem.
The subsystems where the interview is actually decided. Skim if you're running short; own these if you want a strong signal.
Two-phase commit is the textbook answer and almost never the production answer. Understanding why is the interview signal.
2PC mechanics
Phase 1 (prepare): coordinator asks every participant "can you commit?". Each writes its intent durably and votes yes/no.
Phase 2 (commit/abort): if all voted yes, coordinator says "commit". If any voted no (or timeout), "abort". Participants apply and ack.
Why 2PC works on paper
ACID across N participants. Atomic - either all commit or none does. Strong consistency.
Why it falls over in production
3PC
Adds a "pre-commit" phase to remove the blocking case (in theory). In practice, 3PC requires synchronous network and bounded message delays - assumptions that don't hold on real networks. Almost no real system uses 3PC.
Where 2PC still makes sense
The interview answer
"2PC is the right model when you control all participants, they all support prepare/commit, and lock duration is sub-millisecond. For cross-service workflows in microservices or with third-party APIs, sagas with idempotency are the production answer."
The saga pattern: replace one big distributed transaction with a sequence of local transactions, each followed by a compensating action if a later step fails.
Choreography
Each service listens to events and reacts. No central coordinator. Service A commits step 1 → publishes "step 1 done" → service B sees it, commits step 2 → publishes "step 2 done" → ...
If a step fails, the failing service publishes a "saga failed at step N" event; earlier services subscribed to this event run their compensations.
Pros:
Cons:
Best for: <5-step sagas, small set of stable services, strong event-driven culture.
Orchestration
A central orchestrator (the saga executor) drives the state machine. It calls service A, waits for ack, calls service B, etc. On failure, it calls the compensations in reverse order.
Pros:
Cons:
Best for: 5+ step sagas, complex branching, multi-team environments where one team owns the workflow.
The hybrid
Many production systems use orchestration for the main flow + choreography for side effects. The orchestrator drives the critical path (charge → reserve inventory → ship); a "shipment created" event triggers downstream non-critical reactions (analytics, notifications, recommendations refresh).
Recommendation
Default to orchestration for new systems. The debuggability win dominates the architectural-purity loss. Choose tools (Temporal, AWS Step Functions, Cadence, Camunda) that codify the state machine and persist progress automatically.
Sagas trade ACID rollback for compensating actions. The compensation problem is harder than the commit problem.
Semantic vs literal undo
You don't undo "charge $100" by un-charging - you issue a refund. The refund is a new operation with a new ID. The customer sees a charge and then a refund on their statement. Literal time-reversal isn't available; semantic reversal is.
This affects every step:
Steps that have no compensation
Some actions are irreversible: sent SMS, sent push notification, fired-off webhook to a third party that isn't transactional. The saga design must either:
Writing the compensation
The compensation is itself a service operation. It must be:
Compensation failures
What if the compensation itself fails (network down, downstream rejecting refund)? Options:
The interview signal: candidates who treat "compensation always works" as an axiom score lower than candidates who design the operator queue for the cases where it doesn't.
The classic example - hotel + flight booking
Saga: book flight → book hotel → charge card → confirm.
Step 4 (confirm) fails (e.g., user changes mind, payment processor declines).
Compensations in reverse: refund card → cancel hotel → cancel flight.
Each cancellation may itself fail (hotel cancellation deadline passed, flight non-refundable). Real systems convert "cannot cancel" into a business policy: the customer is on the hook for the non-refundable component, the saga reaches a "partially compensated" terminal state, and a human reviews edge cases.
TCC is the saga pattern's pragmatic cousin. Each step splits into two phases: a Try (reserve) and a Confirm/Cancel (commit/release). Looks like 2PC, behaves like a saga.
The pattern
Phase 1 (Try): each service reserves the resource without making it user-visible. Inventory is decremented from "available" but moved to "reserved", not yet "sold". Card is authorized but not captured.
Phase 2 (Confirm or Cancel): once all Try steps succeed, coordinator calls Confirm on each. Reservations become real: reserved inventory becomes sold, auth becomes capture. If any Try fails, coordinator calls Cancel: reservations release, auths void.
Why this is better than naive saga + compensation
Why this is better than 2PC
Where TCC fits
Where TCC doesn't fit
Implementation gotchas
Comparison summary
Pick TCC when reservations are natural; saga otherwise.
Distributed transactions are at-least-once. Without idempotency, a single transient failure double-commits. Idempotency is not optional.
The contract
Every step request carries an idempotency key. The participant guarantees: for a given key, the operation is performed at most once. Subsequent requests with the same key return the prior result.
Where the key comes from
(transaction_id, step_id) as the key. Stable across retries.Stripe, Square, Adyen all expose Idempotency-Key headers - clients must generate stable keys to avoid double-charging.
Implementation: dedup table
Each participant maintains a table:
PK: idempotency_key
attributes: outcome (success/failure), result_payload, created_at
TTL: 30-90 days
On every request:
The atomic write is critical. Race condition: two requests both find no entry, both execute, both write. Now you've committed twice. Solutions:
if_not_exists, Postgres INSERT ... ON CONFLICT DO NOTHING RETURNING).TTL choice
The key must outlive the maximum retry window. Stripe defaults to 24h; orchestration platforms typically retry over hours; for human-in-loop workflows that can take days, extend the TTL. Past the TTL, the system reverts to non-idempotent behavior on duplicates - a known and documented limitation.
Idempotency at every hop
The coordinator, the participant, and any internal proxy/queue must all be idempotent on the same key. A queue that delivers at-least-once + a non-idempotent consumer = duplicates. Audit the entire request path.
The "I retried but my server already committed" pattern
Common bug: client times out, retries with the same idempotency key, server returns the cached "success" result from the first attempt. Looks fine. But the client's first attempt actually got a network failure on the response - the server committed, the client never knew. The retry is what surfaces success.
This is exactly the case idempotency is designed for. Clients must be willing to retry timeouts and trust that the idempotency key prevents double-execution.
Side effects in idempotency
Pure operations are easy. Operations with side effects (send email, push notification, write to a third-party API) need careful design:
The interview signal: candidates who own the at-least-once delivery model and design idempotency at every layer score higher than candidates who hand-wave with "exactly-once".
The most senior answer is sometimes "we don't need a distributed transaction here". Recognizing when scores higher than implementing one well.
The cost of distributed transactions
When the cost is worth it
When the cost isn't worth it
Patterns that avoid distributed transactions
The "single service per transaction" refactor
Sometimes the right answer is to consolidate ownership: move two services' state into one service so the multi-service transaction becomes a single-service transaction. This is anathema to microservice purity but often the most reliable fix. Stripe, Square, and Coinbase all have core "ledger" services that own all financial state precisely so they don't need cross-service transactions.
The hybrid in practice
Real systems use distributed transactions for the small core of operations that demand them (checkout, payment, transfer) and eventual consistency for everything else (notifications, analytics, derived views, search indexes). The interview signal: candidates who can articulate the line, not just defend one side.
2PC vs saga
2PC for shared-DB sharded transactions; saga for cross-service workflows. 2PC across services is a 2010 idea that didn't survive contact with cloud microservices.
Orchestration vs choreography
Orchestration for >3 steps and complex error handling; choreography for short event-driven flows where the "saga" is implicit. Default to orchestration; the debuggability dominates.
TCC vs compensation
TCC where reservations are natural (inventory, bookings, resource allocation); compensation where they aren't (irreversible actions, third-party calls). TCC has cleaner failure semantics; compensation is more flexible.
Coordinator-side vs participant-side idempotency
Coordinator-side keys (the orchestrator generates them) are easier to reason about; participant-side keys force every service to manage its own dedup table. Most systems do both - the coordinator owns the saga key, each participant owns its dedup table for the step.
Synchronous vs async sagas
Synchronous (orchestrator awaits each step) is easier to reason about and gives faster latency for happy paths. Async (events drive each step) scales better and survives orchestrator restarts more gracefully. Mature platforms (Temporal, Step Functions) hide the distinction.
Fast compensation vs forward recovery
Fast compensation (rollback everything on failure) is the default. Forward recovery (re-route through an alternative path) is more user-friendly but requires per-step alternatives - useful for high-value transactions (payment fallback to a secondary processor).
Strict vs lenient timeouts
Strict timeouts (compensate after N seconds) prevent stuck transactions but trigger compensation for transient delays. Lenient timeouts (give the long tail more room) let real failures linger. Match the timeout to user expectations and operator SLA.
Build vs buy a saga platform
Buy: Temporal, AWS Step Functions, Cadence, Camunda - they handle persistence, replay, monitoring, retries. Build: a custom orchestrator over Kafka. Below ~10 sagas, build feels lighter; above, the platform pays for itself in operability.
Eventual consistency vs distributed transaction
Default to eventual consistency. Reach for distributed transactions when money or compliance demands it. The senior signal is recognizing the cases where saga complexity isn't earning its keep.
Be ready for at least three of these. The first one is almost always asked.
Idempotency keys, double-spend prevention, the ledger model, and why eventual consistency is wrong for balances. The interview where ambiguity costs you money.
Raft leader election, log replication, snapshots - and the CAP theorem in operational practice. The substrate every other distributed system stands on.
Reading is the floor. The interview signal is in walking through this live with someone probing follow-ups. Use the AI mock interview to practice talking through requirements, architecture, and trade-offs out loud.
Start an AI mock interview →