Design a Distributed Transaction System from scratch

Not "use 2PC or Sagas to keep services consistent." Instead: two Principal Architects trading precise, checkable arguments in a design review — where nothing is allowed to be a definition, and every mechanism has to earn its place by defeating the decision before it.

15 stages Problem → Constraint → Trade-off → Design Decision → New Problem
How this works: Architect A is extremely skeptical and never accepts a buzzword — "atomicity," "eventual consistency," "rollback" — without demanding the exact mechanism underneath. Architect B is you, designing from first principles, grounded in a real fintech stack — mutual fund order flow spanning payment, allotment, and NSDL demat; LAMF lien-marking and disbursement; PolicyBazaar premium collection. Read A's challenge, form your own answer, then reveal to compare against the reference dialogue.
A Interviewer — skeptical, never satisfied with a buzzword
B You — designing the system from zero, every claim must survive scrutiny
Stage 0

Don't say "distributed transaction"

Architect A

I don't want a distributed transaction system. Put payment, NSDL lien marking, and loan disbursement in one database, one transaction, done. Convince me you can't.

✎ think first

Payment runs through a PSP. NSDL is a separate, external registry you don't operate. What does "one transaction" even mean when two of the three systems involved aren't yours to open a transaction on?

Reveal the dialoguereveal
B

A single database transaction requires a single database. The PSP is a separate company's system. NSDL is a separate, regulated registry. Neither exposes "join my transaction" as an API — they expose HTTP calls that either succeed or fail, on their own clock, with no shared commit log.

ONE DB TRANSACTION:                   REALITY:
  BEGIN
  debit balance                        Our ledger  -- our DB
  mark lien                            NSDL        -- their registry, HTTP API
  disburse loan                        PSP         -- their gateway, HTTP API
  COMMIT (all or nothing, one disk)    THREE separate systems, THREE clocks,
                                        no shared commit log between them
A

Fine — call them one after another in your own service code. Isn't that a transaction in every sense that matters?

B

No — it's a sequence with no undo. If the lien-marking call to NSDL succeeds and the loan-disbursement call to the bank then times out, the customer's units are now frozen and no money moved. Nothing automatically reverses the first step because there was never a shared transaction to roll back.

A

So name the actual problem. Not "coordinating multiple services" — the precise mechanical gap.

B

The gap is this: the moment a business operation crosses a network boundary, there is no longer any mechanism, by default, that guarantees all its steps happen or none of them do. That guarantee has to be built — it doesn't come free the way it does inside one database engine.

Where this leaves us: a distributed transaction system isn't a fancier database — it's the machinery that has to be built from scratch to restore a guarantee that a single database gave away for free. Every stage from here is an argument about how much of that guarantee is actually recoverable, and at what price.
Engineering Philosophy

Before you design a distributed transaction, check if you actually need one. True atomicity across services is expensive. Sometimes the right answer is to accept eventual consistency and build compensation.

Stage 1

What are we optimizing?

Architect A

Pick exactly one: speed, availability, or atomicity. Not all three. Defend the one you pick with a consequence, not a preference.

✎ think first

Compare "the operation is slow but never leaves a half-finished state" against "the operation is fast but can strand a customer's money mid-flight." Which failure would you rather explain to a regulator?

Reveal the dialoguereveal
B

Atomicity. Not speed, not availability. A slow success is an inconvenience. A half-finished money movement — units locked, cash not moved, or the reverse — is an incident that needs a human, a reconciliation team, and possibly a regulatory disclosure.

A

Careful — "atomicity across systems you don't control" might be a promise you can't keep. Are you claiming true ACID atomicity, or something weaker dressed up in the same word?

B

Something precise, not weaker: a guarantee that the system will always know, and eventually enforce, that every step either completed or was undone — even if "undone" isn't instant. I'm not promising NSDL and the bank commit in lockstep. I'm promising the overall business operation never gets stuck half-true.

Where this leaves us: atomicity-as-a-goal is now defined precisely enough to be checkable — not "nothing ever fails," but "failure is always eventually resolved to a known, complete state." That precise definition is what the next stage's naive attempt gets to fail against.
Stage 2

The naive fix: wrap it in try/catch

Architect A

Call NSDL, call the bank. If either call throws, catch the exception and call the "undo" API for whatever already succeeded. That's a rollback. Why isn't that enough?

✎ think first

The undo call is itself a network call to another system. What happens if the undo call fails too?

Reveal the dialoguereveal
B

Then you're stuck with exactly the same unresolved-state problem, one level deeper — except now it's hidden inside a catch block that everyone assumes already "handled" it.

TRY:
  mark lien on NSDL         -- SUCCEEDS
  disburse loan via bank    -- FAILS, times out
CATCH:
  undo lien on NSDL         -- also FAILS (network blip)
  -- now what? the catch block has no plan B.
A

So retry the undo in a loop until it works?

B

Retrying blindly, in-process, dies the moment the service itself crashes or restarts mid-retry — the loop, and the memory of "I still owe an undo," disappears with the process. Whatever tracks "this operation is incomplete" has to survive the failure of the code that's trying to fix it.

A

So the real gap isn't the rollback logic. It's that nothing durable remembers the operation is unfinished.

B

Exactly — try/catch treats failure as a momentary exception to handle inline. What we actually need is a durable record of "this multi-step operation is in progress," one that outlives the process, so something can pick it back up no matter what crashes.

Where this leaves us: in-memory rollback logic can't survive the failure of the very process running it. Any real solution needs a durable, external record of progress — which is the first honest requirement for a coordination protocol, not an app-code pattern.
Stage 3

Two-phase commit — force agreement before anyone commits

Architect A

Design the protocol. Not the name — the actual mechanism that forces every participant to either all commit or all abort.

✎ think first

If you ask every participant to commit in one single round, and one of them says no after another has already committed, can you undo the one that already committed? What does that force you to do instead?

Reveal the dialoguereveal
B

Split it into two rounds. Round one — PREPARE — ask every participant "could you commit this, if asked?" without actually committing. Each one locks its resources and replies yes or no, but takes no irreversible action yet.

PHASE 1 -- PREPARE:
  Coordinator --> NSDL:  "could you mark this lien?"  --> NSDL: "yes, locked"
  Coordinator --> Bank:  "could you disburse this?"   --> Bank: "yes, locked"
  Nothing IRREVERSIBLE has happened yet on either side.
A

And if one of them says no?

B

Then round two never becomes COMMIT — it becomes ABORT, sent to everyone, and every participant releases its lock. Nothing was ever committed, so there's nothing to undo. That's the entire trick: defer the irreversible step until every participant has already agreed.

PHASE 2, all said YES:              PHASE 2, one said NO:
  Coordinator --> ALL: COMMIT          Coordinator --> ALL: ABORT
  every participant commits           every participant releases locks
  for real, now                       nothing irreversible ever happened
A

What's the cost of holding every participant's resource locked through the whole first round?

B

Every other operation waiting on that same lien or that same account balance is now blocked, for the full round-trip time of the slowest participant. We've bought atomicity by spending availability — and that trade is about to get much worse.

Where this leaves us: two-phase commit earns real atomicity by making everyone wait for everyone else's answer before anyone acts. That waiting is not a minor inefficiency — it's the seed of the next stage's proof that this protocol cannot avoid blocking.
Stage 4

The unavoidable proof: 2PC cannot be non-blocking

Architect A

NSDL has said "yes, prepared." The bank has said "yes, prepared." Both are now holding locks, waiting for the coordinator's final word. The coordinator's network connection drops before it sends COMMIT or ABORT. What does NSDL do?

✎ think first

NSDL cannot unilaterally commit — the bank might have said no and NSDL never heard about it. NSDL cannot unilaterally abort — the bank might have already committed. What third option does NSDL actually have?

Reveal the dialoguereveal
B

None. That's the honest, uncomfortable answer. NSDL is stuck holding the lock, unable to safely guess either direction, until the coordinator comes back — however long that takes.

NSDL, prepared, waiting:
  Guess COMMIT --> if bank actually said NO, we just committed alone. WRONG.
  Guess ABORT  --> if bank actually already COMMITTED, we're now inconsistent. WRONG.
  Only safe move: WAIT. Lock stays held. Nobody else can touch this resource.
A

So say it plainly, without hedging: two-phase commit can block a resource indefinitely on a single coordinator failure.

B

Yes. That's not an implementation bug I can engineer away — it's a structural property of the protocol. The instant you require unanimous agreement before anyone acts, you've made every participant's progress depend on hearing back from one single coordinator. There's no clever fix that removes that dependency while keeping true atomicity.

A

Then why does anyone still use 2PC?

B

Inside a single data center, with a fast, reliable network and a coordinator that restarts quickly from a durable log, the blocking window is small and rare enough to tolerate. Across an external registry like NSDL and an external bank, on the open internet, that same blocking window becomes routine, not rare — which is exactly why it stops being the right answer here.

Where this leaves us: 2PC isn't wrong, it's mismatched — it was built for a world of fast, trusted, nearby participants, and we're operating in a world of slow, external, sometimes-hostile ones. The next stage has to ask what happens in the specific failure this stage just proved was inevitable.
Stage 5

The hardest question in 2PC: does the coordinator's log actually save you?

Architect A

You'll say "the coordinator writes its decision to a durable log before crashing, so it can recover." Fine — the coordinator crashes after writing COMMIT to its log but before telling any participant. What do NSDL and the bank experience?

✎ think first

The durable log protects the coordinator's own memory of the decision. Does it do anything at all for the participants who are still sitting there, blocked, with no way to read that log themselves?

Reveal the dialoguereveal
B

Nothing changes for them in the moment. The durable log guarantees the coordinator, once it recovers, will remember and correctly re-announce COMMIT to everyone. But until that recovery happens, NSDL and the bank are exactly as blocked as before — the log protects the coordinator's memory, not the participants' wait time.

Coordinator log: "DECISION = COMMIT"  -- written to durable disk
Coordinator crashes.
NSDL:  still blocked, holding lock, no idea the decision exists
Bank:  still blocked, holding lock, no idea the decision exists
Coordinator recovers, reads its own log, NOW tells everyone.
The gap between crash and recovery was pure blocked time.
A

So the durable log is solving the coordinator's amnesia problem, not the participants' blocking problem. Say that difference out loud — people conflate them constantly.

B

They're genuinely different problems. Amnesia is "does the coordinator remember what it decided." Blocking is "how long do participants wait to find out." A durable log fixes the first completely. It does nothing for the second — recovery time is still whatever it takes to restart the coordinator process and reconnect to every participant.

Where this leaves us: even a perfectly engineered 2PC coordinator, one that never loses its own memory, still leaves participants blocked for the coordinator's full recovery window. That's the real, final verdict on 2PC for this use case — the fix isn't a better coordinator, it's not requiring unanimous agreement before acting at all.
Engineering Philosophy

Two-phase commit can block forever if the coordinator crashes at the wrong moment. This isn't a bug you can fix — it's mathematically unavoidable. Any system using 2PC has accepted this risk.

Stage 6

Give up atomicity on purpose: the saga

Architect A

If waiting for unanimous agreement is the actual disease, stop waiting for it. What do you do instead — and what do you honestly give up?

✎ think first

If each step commits immediately, for real, on its own — no prepare phase, no lock held for anyone else — what happens the moment a later step fails, and what has to exist to fix it?

Reveal the dialoguereveal
B

A saga: a sequence of independent local transactions, each one committing for real, immediately, with no cross-service lock ever held. If a later step fails, you don't roll back the earlier ones in the database sense — you run a separate compensating action that semantically undoes them.

SAGA (no locks held across steps):
  Step 1: mark lien on NSDL      --> COMMITS immediately, for real
  Step 2: disburse loan via bank --> FAILS
  Step 3 (compensation): release lien on NSDL --> a NEW, separate transaction
  No lock was EVER held waiting on step 2's outcome.
A

You just said the quiet part — "release the lien" is not the same operation as "the lien never happened." What did you actually give up by choosing this?

B

Isolation, and true rollback. Between step 1 committing and the compensation running, the lien is real and visible — anyone checking that customer's units during that window sees a locked state that will, shortly, be reversed. A saga trades "never visibly inconsistent" for "never permanently stuck."

Where this leaves us: the saga isn't a free upgrade over 2PC — it's the opposite trade. 2PC hides inconsistency behind blocking; sagas expose brief, real inconsistency in exchange for never blocking at all. Whether that trade is acceptable depends entirely on whether "compensation" is actually the same as "undo" — which is the next stage's problem.
Engineering Philosophy

A saga trades atomicity for availability. Each step must be designed to be undoable, even if the undo isn't perfect. That's a product decision before it's an engineering one.

Stage 7

Compensation is not undo — and sometimes it can't be

Architect A

Step 3 was "send a confirmation email to the customer that their loan was approved." Step 4 fails. What's the compensating action for "the customer already read the email"?

✎ think first

Some actions are reversible in the database — releasing a lock, refunding a charge. Some are irreversible in the real world. Which category does a notification fall into, and what does that force you to do about ordering?

Reveal the dialoguereveal
B

There isn't one. You can send a follow-up "disregard our earlier message" — but that's damage control, not undo. The honest answer is that a notification is semantically irreversible, and no compensating transaction changes what already happened in the customer's inbox.

REVERSIBLE:                        IRREVERSIBLE:
  lock a resource --> unlock it        send an email --> "un-send" it?
  reserve funds  --> release them      the customer already read it.
A

So what's the actual design rule — not a platitude, a rule you can enforce in code review?

B

Irreversible steps — notifications, external settlement confirmations, anything that leaves the system's boundary permanently — must be ordered last in the saga, only after every reversible step has already succeeded. If it can't be undone, it doesn't get to run until everything undoable already has.

SAGA ORDERING RULE:
  [reversible][reversible][reversible] ... [irreversible, LAST]
  by the time the irreversible step runs, every other step
  has already proven it will succeed or the saga never got this far.
A

What if the irreversible step itself fails — the email provider times out?

B

Then it retries, because failing to send is recoverable — the failure mode we can't compensate for is the message actually arriving and then needing to un-arrive. A failed send is just a send that hasn't succeeded yet.

Where this leaves us: "compensation" quietly assumed every action was reversible, and that assumption is false for a whole class of real steps. Ordering irreversible actions last isn't a nice-to-have — it's the only way the compensation model in stage 6 stays honest at all.
Engineering Philosophy

Compensation is not the same as rollback. A database write can be undone. A sent email or a completed payment cannot. Design your compensation steps before you design the happy path.

Stage 8

Who actually decides the next step?

Architect A

Right now, who tells step 2 to run after step 1 succeeds — and who decides which compensations to fire when step 3 fails?

✎ think first

If every service listens for events from every other service and decides for itself what to do next, where does the "what's the current state of this saga" answer actually live?

Reveal the dialoguereveal
B

Two shapes, and they fail differently. Choreography — each service publishes an event when it finishes, and listens for events to know when to act — has no single point of failure, but nobody, at any moment, can answer "what's the overall status of this loan disbursement?"

CHOREOGRAPHY:
  Payment svc  --publishes--> "lien.marked"
  Loan svc     --listens, then publishes--> "loan.disbursed"
  Notify svc   --listens, then publishes--> "customer.notified"
  Nobody holds the full picture. Debugging means reconstructing
  the story from scattered logs across three services.
A

So centralize it. What does that cost?

B

Orchestration — one saga coordinator explicitly calls each step in order and explicitly triggers compensations on failure. It's fully visible and debuggable, but that coordinator is now a piece of shared infrastructure every saga depends on — a new component that can itself have bugs, and needs its own durability story.

ORCHESTRATION:
  Saga Coordinator: step1 --> OK --> step2 --> FAILS
                    --> explicitly calls compensate(step1)
  ONE place holds "here's exactly where this saga is."
A

For a loan disbursement — money, compliance, audit trail — which do you actually ship, and why does that context matter?

B

Orchestration, because auditability isn't optional here — a regulator asking "what exactly happened to this loan and in what order" needs one authoritative timeline, not a reconstruction exercise across five services' logs. Choreography's decoupling is a genuine strength, just not the one this specific workload needs most.

Where this leaves us: orchestration buys a single source of truth for a saga's state, at the cost of a new central component. That component now needs everything the coordinator in stage 4 needed — durability, recovery — but this time without 2PC's blocking, which is the whole reason we're here.
Stage 9

The orchestrator retries a step. Did it already run?

Architect A

The orchestrator calls "disburse loan," the response times out, and it retries. Did the first call actually fail, or did it succeed and only the response got lost? What happens if you disburse twice?

✎ think first

What's the minimum information the bank's disbursement API needs from the orchestrator to tell "this is a retry of a call I already processed" apart from "this is a genuinely new disbursement request"?

Reveal the dialoguereveal
B

Every step and every compensation needs a unique, stable idempotency key — tied to the saga instance and the step, not generated fresh on each call. The bank's API stores which keys it's already processed and returns the original result instead of disbursing again.

Orchestrator calls disburse(saga_id=S991, step=2, idem_key="S991-step2")
  Call 1: times out on the response, but the bank actually processed it
  Call 2 (retry): SAME idem_key --> bank recognizes it, returns cached result
  Money moves ONCE, no matter how many times the orchestrator retries.
A

Does the compensation side need the same treatment, or is "undo" naturally safe to repeat?

B

Same treatment, for the same reason — a retried "release the lien" compensation without its own idempotency key could double-release a lien that a different, later saga had already legitimately re-acquired. Idempotency isn't just a forward-path concern; every action a saga can take, forward or backward, needs it.

Where this leaves us: the network's ability to lose a response but still deliver the request means every action in this design has to be safe to receive twice. That single requirement now applies uniformly to steps, compensations, and — as the next stage reveals — to anyone reading a saga's state mid-flight.
Stage 10

The isolation you gave up in stage 6 just bit a real customer

Architect A

Mid-saga, the lien is marked but the loan isn't disbursed yet. The customer opens the app and checks their mutual fund units. What do they see, and is it true?

✎ think first

If what they see is "technically accurate right now, but about to change," is that a bug, or an unavoidable consequence of a decision made two stages ago? What would actually fix it?

Reveal the dialoguereveal
B

They see their units as locked against a loan that, as far as they can tell, doesn't exist yet — no disbursed cash, no visible loan account. That's not a bug in the read path. It's the exact isolation gap stage 6 admitted to when it chose sagas over 2PC.

Mid-saga, real customer state:
  NSDL: units LOCKED (lien marked, step 1 done)
  Loan ledger: no loan record yet (step 2 not done)
  Customer sees: "why are my units locked with no loan to show for it?"
A

So fix the read path — hide locked units until the saga completes?

B

Hiding the lock would be worse — it would let the customer place another order against units that are, in fact, already encumbered. The honest fix is a semantic lock: the read path shows the units as "pending — loan application in progress," an explicit, named intermediate state, not silence and not a lie.

WITHOUT semantic state:      WITH semantic state:
  "units: LOCKED" (why?)        "units: pending loan disbursement"
  customer confused             customer informed, and other systems
                                 know not to also touch these units
Where this leaves us: a saga can't hide the intermediate state it necessarily passes through — it can only choose to name it honestly. That named "pending" state turns out to matter even more the moment a second, independent saga tries to touch the same resource — which is exactly the next stage's problem.
Stage 11

The hardest question: two sagas, one resource, at the same time

Architect A

A customer starts a LAMF loan against their mutual fund units. Before that saga finishes, they also submit a redemption request for the same units, through a completely different saga. Both are in flight. What stops both from succeeding?

✎ think first

The semantic "pending" state from stage 10 told readers what was happening. Does it stop a second writer from acting on the same resource, or only inform it?

Reveal the dialoguereveal
B

Informs, not stops — a "pending" label is just a status field unless something enforces it. Without an explicit rule, the redemption saga could run its own local checks, see units that appear present, and proceed, completely unaware the loan saga has already claimed them.

WITHOUT enforcement:
  Loan saga:       step1 marks lien --> units "pending loan"
  Redemption saga: reads units --> sees them, proceeds to redeem
  BOTH saw a valid-looking state. BOTH proceeded. Conflict.
A

So what actually enforces it — another distributed transaction to coordinate two sagas?

B

No — that just reintroduces stage 4's blocking problem one layer up. The fix is a semantic lock that any saga touching a given resource must acquire as its own first, reversible step, before doing anything else — the resource itself refuses a second claim while one is already active, the same way a database row lock would, but scoped to the saga's lifetime instead of a transaction's.

Loan saga:       step0: ACQUIRE semantic lock on units --> granted
Redemption saga: step0: ACQUIRE semantic lock on units --> DENIED, already held
                 --> redemption saga fails FAST, at step 0, cleanly
                 -- no wasted work, no silent conflict
A

Isn't a semantic lock just 2PC's locking, renamed, with the same blocking risk you spent three stages escaping?

B

The difference is scope and ownership, not the existence of a lock — 2PC held locks across a coordinator waiting on unanimous agreement from strangers; here, the resource itself owns and enforces its one lock, released the instant its own saga finishes or compensates, with no external coordinator anyone else has to wait on. Locking didn't disappear from this design. It got pushed down to exactly the one place — the contested resource — where it actually belongs.

Where this leaves us: sagas don't eliminate the need for locking — they relocate it, from a cross-service coordinator that blocks everyone, to the resource itself, which blocks only the specific thing actually in conflict. That's a narrower, cheaper lock, not a different philosophy.
Stage 12

Your own database write and your own event aren't atomic either

Architect A

Step 1 writes "lien marked" to your database, then publishes a "lien.marked" event to the queue that step 2 listens on. The process crashes between the write and the publish. What happened?

✎ think first

You've spent eleven stages solving atomicity between external services. Is your own database write and your own message publish somehow exempt from the exact same problem?

Reveal the dialoguereveal
B

No exemption — I nearly gave myself one by accident. The database write and the event publish are two separate systems, exactly like NSDL and the bank were. A crash between them leaves the database saying "lien marked" while the event that should trigger step 2 never goes out. The saga silently stalls.

DB write "lien marked" --> COMMITS
*** process crashes ***
publish "lien.marked" event --> NEVER SENT
Saga is now stuck, forever, with no step 2 ever triggered.
A

So you're back to needing atomicity between two systems. Are we starting this whole argument over?

B

No — this specific case has a narrower fix, because both writes are ours. The outbox pattern: write the event into an "outbox" table in the same local database transaction as the state change itself. One database, one real ACID transaction, so both rows commit together or neither does.

ONE local DB transaction:
  UPDATE lien SET status='marked'
  INSERT INTO outbox (event='lien.marked', payload=...)
  COMMIT -- both rows exist, or NEITHER does. Real ACID, same DB.

Separate background process reads the outbox table,
publishes each row to the queue, marks it published.
A

The background publisher can still crash after publishing but before marking the row as sent. Doesn't that create duplicates?

B

Yes — and that's fine, because we already solved duplicates in stage 9. The publisher can safely re-publish an outbox row it's unsure about, because every downstream consumer is required to be idempotent. The outbox guarantees at-least-once delivery; idempotency, already built, turns that into effectively-once processing.

Where this leaves us: the outbox pattern only works because the state change and the event live in the same local database — it's a narrow, honest exploitation of real ACID where it's actually available, not a workaround for the cross-service case. And it only stays safe because idempotency, built back in stage 9, is already standing guard on the other end.
Stage 13

Who remembers where every saga actually is?

Architect A

The orchestrator from stage 8 is itself just a service. It crashes mid-saga, taking down the only record of "step 2 completed, step 3 in progress." Restart it. What does it know?

✎ think first

If the orchestrator's saga state lived only in that process's memory, what's actually lost when it restarts — and where should that state have lived instead?

Reveal the dialoguereveal
B

If state lived only in memory, it knows nothing — every in-flight saga simply vanishes from the orchestrator's awareness, even though NSDL still has a lien marked and is waiting for a next step that will now never come.

In-memory orchestrator state:
  saga S991: step2 done, step3 pending  -- lives in RAM
  *** process crashes and restarts ***
  saga S991: -- GONE. Orchestrator has no memory this saga ever existed.
  NSDL is still holding a lien for a saga nobody is driving anymore.
A

So persist it to a database. Isn't that just moving the same single-point-of-failure problem you diagnosed for the 2PC coordinator back in stage 4?

B

It's the same shape of problem, with a materially different consequence — a crashed orchestrator with durable state can restart, read its own saga log, and resume exactly where it left off, driving every step forward or backward on its own. Nothing else in the system is blocked waiting for it in the meantime, because sagas, unlike 2PC participants, never held a lock waiting on the coordinator's word.

Durable saga log (its own DB table):
  saga S991: step1=DONE, step2=DONE, step3=PENDING
  Orchestrator crashes, restarts, reads the log:
  "resume S991 at step3" -- picks up exactly where it stopped.
  Meanwhile: NOTHING else was blocked waiting for this recovery.
A

Doesn't the orchestrator's own database now need to be replicated, or you've just built a new single point of failure with extra steps?

B

Correct, and that's an honest final cost, not a hidden one — the saga log needs the same replication and durability discipline as any other critical store in this platform. The difference from 2PC is what happens during the gap before that durability kicks in: participants keep making local, independent progress instead of freezing in place.

Where this leaves us: the orchestrator earns the right to be a single logical coordinator only because its state is durable and its absence never blocks anyone else — which is the one property 2PC's coordinator, back in stage 4, never had.
Stage 14

System 2.0 — what would you redesign?

Architect A

Last question. If you were starting today, with everything you now know, what's the one decision from this conversation you'd revisit — and what would you knowingly give up to change it?

✎ this one has no reference answer

This is deliberately open. Pick one stage's decision — sagas over 2PC, orchestration over choreography, semantic locking, anything — and argue what you'd change and what it costs. A real answer defends a trade-off; it doesn't just criticize.

No answer key. Notice what this whole conversation never did: it never said "a distributed transaction system has a coordinator, compensations, and idempotency keys" as a starting fact. Every mechanism showed up because a previous decision was caught, in the very next line, being insufficient —
Problem → Constraint → Trade-off → Design Decision → New Problem → Next Design Decision
— not Feature → Definition → Next Feature. That's the real difference between reciting a saga framework's checklist and understanding why it has to look the way it does. Your answer to this last question is the actual test of which one you did.
Stages not covered here — extend this same format into them yourself:
  1. TCC (Try-Confirm/Cancel) as a middle ground between 2PC and sagas
  2. Saga timeouts — how long is "still in progress" allowed to last before it's treated as failed
  3. Cross-saga deadlock — two sagas each waiting on a semantic lock the other holds
  4. Observability — reconstructing a saga's full timeline for a regulator, on demand
  5. Testing sagas — simulating partial failure at every single step deliberately
  6. Event sourcing as an alternative foundation for saga state, instead of a status table