Playbook
Why Payment Systems Are Distributed Systems (Why Payment Systems Are Distributed Systems)
A payment is never one service's job: basket, stock, provider gateway, and ledger all have to agree. Here's why the synchronous chain breaks down.
Distributed Payment Engine
Part 1 of 22
A series on distributed payment architecture — the gap between capture and complete.
A payment is not a button, it's a coordination problem
When a checkout flow in an e-commerce platform says “charge the customer”, it actually needs five different systems to agree on the same fact: is the basket frozen, is stock available, did the provider gateway take the money, is the order confirmed, is the ledger entry correct. None of this lives inside a single process or a single transaction.
Basket Service Stock Service Checkout Orchestrator Provider Gateway Ledger Service
| | | | |
+----------------+-------------------+--------------------+---------------+
one order, five separate truths
This part explains why you should design a payment flow as a distributed systems problem, not as a single service's function.
Concepts, defined where they first appear
📦 Checkout Orchestrator
Coordinates the steps of an order flow (basket, payment, stock, confirmation); it holds no money and no stock, only decisions and their order.
📦 Provider Gateway
The abstraction layer that translates a real payment provider (PSP) into your internal domain model.
📦 PSP (Payment Service Provider)
The external system that actually holds the card and moves the money; it lives outside your transaction boundary.
📦 Distributed Transaction
An operation that spans multiple independent systems under a single all-or-nothing guarantee.
📦 Eventual Consistency
A consistency model that accepts systems will converge on the same fact shortly, not instantly.
A team that can't tell these apart ends up treating the PSP like its own database. The PSP never joins your transaction; it only reports its own truth, on its own timeline.
One request, five signatures required
When a customer clicks “Pay”, a sequence of decisions happens behind the scenes: is the basket price frozen, is stock reserved, did the request reach the provider gateway, did the PSP actually take the money, are the order lines finalized, is the ledger entry opened. Each of these belongs to a different service, backed by a different database.
The problem starts here: if you chain these six steps synchronously inside one HTTP call chain, you end up with a long, brittle sequence where every step depends on the one before it.
Client → Checkout Orchestrator → Basket Service → Stock Service → Provider Gateway → PSP
Where the synchronous chain breaks
When any step in this chain times out, you're left with two unanswered questions: did the request even arrive, and if it did, was it processed? A timeout from the PSP does not mean “the money wasn't taken” — it means “I didn't get a response.” Retrying the same request blindly can charge the customer twice.
Checkout Orchestrator --(timeout)--> Provider Gateway --(???)--> PSP
was the money taken or not?
This ambiguity is not a bug you can code around; it's the natural consequence of a synchronous chain. Networks are unreliable by definition — the longer the chain, the more ambiguity accumulates.
Why “all or nothing” doesn't work here
Classic distributed transaction protocols (like two-phase commit) assume every participant shares the same coordinator, the same locking protocol, and the same network reliability. The PSP is not part of that world: it won't hold a lock for you, it won't listen for your commit/rollback call, and it tells you what happened on its own schedule — synchronously or, more often, via webhook.
Trying to squeeze “payment + stock + order” into a single transaction doesn't solve an unsolvable problem — it just hides it. You haven't removed the failure mode; you've made it invisible until it shows up in production.
The event-driven fix: two truths, two timelines
The model that actually works is to shorten the synchronous chain and hand everything past it off to events. You send the request to the provider gateway, you record the PSP's response (sync or via webhook) as an event, and every downstream step — stock, ledger, order confirmation — becomes an independent, idempotent consumer of that event.
Provider Gateway → PaymentCaptured (event) → Outbox
↓
Stock Service Ledger Service Checkout Orchestrator
(each consumes independently, at its own pace, with its own retries)
In this model, “payment succeeded” and “order completed” are no longer the same instantaneous fact — they're two related truths, on two timelines, with a measurable gap between them. Systems that refuse to accept this gap drift straight into the state machine bugs we cover in the next part.
The mappings that get confused most often
❌ A payment flow is one service's function
✓ A payment flow is a coordination between several independent services
❌ The PSP behaves like a table in our own database
✓ The PSP is an externally observed system with its own timeline
❌ Timeout means the operation failed
✓ Timeout means the outcome is unknown — retrying without idempotency is unsafe
❌ A distributed transaction can “solve” this
✓ Distributed transactions don't hold up across external boundaries like a PSP
These four wrong mappings are behind most “why did we double-charge this customer” incidents.
A checklist for auditing your own flow
- Count how many services, writing to how many separate databases, participate in your payment flow.
- When the call to your provider gateway times out, does your code retry automatically — and does that retry carry an idempotency key?
- Is “payment succeeded” stored in the same record as “order completed”, or in separate tables?
- If a PSP webhook is delayed or never arrives, how many hours pass before your system notices?
- Which step in your synchronous chain is the longest, and what happens to the rest of the chain if that step goes down?
If you can't answer these five questions cleanly, your payment flow is probably designed around a “single transaction” illusion.
What to take away from this part
- A payment is not a single service's operation; it's a coordination where basket, stock, provider gateway, and ledger agree on the same fact.
- A synchronous HTTP chain multiplies ambiguity with every added step; a timeout is not an outcome, it's an unknown.
- The PSP is not part of your transaction boundary; you talk to it through an event contract, not a synchronous lock.
- Eventual consistency isn't a shortcoming — it's an honest reflection of how the outside world, especially your PSP, actually behaves.
Design your payment system as “instant and single-piece” and production will answer back with “delayed and multi-piece”.
FAQ
Frequently asked questions
What is Checkout Orchestrator?
Coordinates the steps of an order flow (basket, payment, stock, confirmation); it holds no money and no stock, only decisions and their order.
What is Provider Gateway?
The abstraction layer that translates a real payment provider (PSP) into your internal domain model.
Is it true that "A payment flow is one service's function"?
A payment flow is a coordination between several independent services
What does this part lock in?
This part explains why you should design a payment flow as a distributed systems problem, not as a single service's function. A payment is not a single service's operation; it's a coordination where basket, stock, provider gateway, and ledger agree on the same fact. When a checkout flow in an e-commerce platform says “charge the customer”, it actually needs five different systems to agree on the same fact: is the basket frozen, is stock available, did the provider gateway take the money, is the order confirmed, is the ledger entry correct. None of this lives inside a single process or a single transaction.
Engineering Principles Learned
- A payment flow is a coordination between independent systems, not a single service's function.
- The PSP lives outside your transaction boundary; you talk to it through an event contract, not a synchronous lock.
- Eventual consistency is not a weakness — it's the honest design response to how networks and external providers actually behave.
Continue reading
Continue reading
Next in series
Payment State Machine Design: Checkout vs. Payment Lifecycles
Payment.Succeeded does not mean Checkout.Completed. If you don't separate checkout and payment lifecycles, two truths collide in production.
Same series
Why Payment Capture Is Easy but Finalization Is Hard
The PSP taking the money is one step. Finishing the order is a saga that needs stock, finance, notifications, and cleanup to all succeed.
Same series
Immutable Payment Snapshot Design: Freezing the Cart at Intent Time
Re-reading the live basket during payment leaves amount and currency undecided. Without a snapshot frozen at intent time, finalization can't be trusted.