Playbook
Effectively-Once Processing in Payments (Effectively Once Processing In Payments)
Exactly-once messaging is a lie. How defense in depth — idempotency, dedup, outbox, and reconciliation — produces an effectively-once business outcome.
Distributed Payment Engine
Part 20 of 22
A series on distributed payment architecture — the gap between capture and complete.
The previous part showed the recovery pipeline working automation-first, with evidence-driven runbooks at uniqueness walls. This part goes into the messaging guarantees underneath — and the industry's most common misconception: exactly-once delivery.
Broker vendors promise 'exactly-once semantics'. The reality: in a distributed system, a message being processed exactly once can never be physically guaranteed. Networks drop, workers crash, brokers redeliver. The question isn't 'how many times did the message arrive' — it's how many times did the business outcome happen.
Exactly-once messaging → impossible (at-least-once + failure = duplicate)
Effectively-once outcome → achievable (defense in depth)
Where the concepts first show up
📦 At-least-once delivery
A message arrives at least once; it may be redelivered after a network fault.
📦 Effectively-once
Even if a message is processed multiple times, the business outcome (charge, refund, order) happens only once.
📦 Defense in depth
Multiple independent protection layers instead of relying on a single mechanism.
📦 Idempotent consumer
A consumer that produces the same result on a second delivery, with no side effects.
Exactly-once messaging isn't a broker feature; effectively-once is a system design.
Why exactly-once is a lie
A worker receives a message, processes it, sends an acknowledgment — but the acknowledgment gets lost on the network. The broker redelivers. The worker processes again. This is inherent to at-least-once delivery; no matter how much a broker claims 'exactly-once', duplicates on the consumer side are unavoidable.
Worker processes message → result: Captured
Ack lost on the network
Broker redelivers
Worker processes again → result: ??? (double charge risk)
The problem isn't how many times the message arrived — it's what happens on the second arrival. If the second arrival produces no side effect, you've achieved effectively-once.
Defense in depth: one layer isn't enough
An effectively-once business outcome is the combination of complementary layers. None is sufficient alone; together they produce 'a message that arrived at least once affects the outcome at most once'.
Layer 1: Idempotency key (API)
→ a second charge request with the same key is rejected
Layer 2: Consumer dedup (messaging)
→ the same message id isn't processed twice
Layer 3: DB uniqueness constraint
→ a second row with the same idempotency key can't be written
Layer 4: Outbox pattern
→ an event is published only together with a transaction commit
Layer 5: Reconciliation
→ periodically fixes drift that layers 1–4 missed
If one layer fails, another catches it. If the idempotency key is skipped, the uniqueness constraint catches it; if the constraint is bypassed, reconciliation fixes it.
Each layer answers a different question
| Layer | Question | Protects against |
|---|---|---|
| Idempotency key | Same intent again? | Double charge at API level |
| Consumer dedup | Same message again? | Double processing at messaging level |
| Uniqueness constraint | Second row with same key? | Double record at DB level |
| Outbox | Event published before commit? | Lost or premature event |
| Reconciliation | Local vs PSP mismatch? | Everything that slipped through |
Writing an idempotent consumer
The rule for an idempotent consumer: same input, same output; no side effect on the second call.
PaymentCaptured webhook (messageId=wh_991)
→ check dedup table: was wh_991 already processed?
→ yes → skip (log: duplicate suppressed)
→ no → finalize, write to dedup table
The finalize step itself must be idempotent too: if the payment is already Captured, no event should be republished, no order recreated. The dedup table protects the messaging layer; the terminal-state check protects the business logic.
Outbox: event and state in the same transaction
The outbox pattern resolves the dilemma of 'state updated but event not published' or 'event published but state not updated'. The event is written to an outbox table together with the local transaction; a separate publisher reads the outbox and sends to the broker.
BEGIN TRANSACTION
UPDATE payment SET status=Captured
INSERT INTO outbox (event=PaymentCaptured, paymentId=8812)
COMMIT
→ publisher reads outbox → sends to broker
→ send succeeds → deletes outbox row
Outbox doesn't provide messaging exactly-once — but it guarantees consistency between state and event. Reconciliation catches what slips out of the outbox.
Distinctions that get blurred
❌ Broker exactly-once = system exactly-once
✓ Broker dedup + consumer idempotency + DB constraint = effectively-once
❌ An idempotency key is enough everywhere
✓ An idempotency key protects the API; webhooks and async workers need separate layers
❌ Effectively-once = a perfect system
✓ Effectively-once = outcome once; the inconsistency window closes via reconciliation
Delivery guarantee vs business outcome
| Guarantee | What it promises | Sufficient for payments |
|---|---|---|
| At-most-once | Message once, may be lost | No — lost payment |
| At-least-once | Message at least once, may duplicate | No — alone |
| Exactly-once (broker) | Theoretical, breaks on consumer side | No |
| Effectively-once (system) | Business outcome once | Yes |
Effectively-once checklist
- Are API charge requests protected by idempotency keys?
- Is there message-id dedup on webhook and async consumers?
- Is there a uniqueness constraint on idempotency key in the payment table?
- Are state changes and event publishing in the same transaction via the outbox pattern?
- Does the finalize handler skip without reprocessing when already in terminal state?
- Does reconciliation periodically scan for drift that layers 1–5 missed?
What to take away
- Exactly-once messaging is a lie; at-least-once + failure makes duplicates unavoidable.
- An effectively-once business outcome is achievable through defense in depth — one layer isn't enough.
- Each protection layer answers a different question; all must work together.
- Reconciliation is the last layer; it doesn't replace the others, it completes them.
Even if your broker promises exactly-once, your payment system shouldn't believe it — it should build effectively-once business outcomes through defense in depth.
The next part synthesizes this 22-part journey: a production payment engine design checklist.
FAQ
Frequently asked questions
What is At-least-once delivery?
A message arrives at least once; it may be redelivered after a network fault.
What is Effectively-once?
Even if a message is processed multiple times, the business outcome (charge, refund, order) happens only once.
Is it true that "Broker exactly-once = system exactly-once"?
Broker dedup + consumer idempotency + DB constraint = effectively-once
What does this part lock in?
Broker vendors promise 'exactly-once semantics'. The reality: in a distributed system, a message being processed exactly once can never be physically guaranteed. Networks drop, workers crash, brokers redeliver. The question isn't 'how many times did the message arrive' — it's **how many times did the business outcome happen**. Exactly-once messaging is a lie; at-least-once + failure makes duplicates unavoidable. The previous part showed the recovery pipeline working automation-first, with evidence-driven runbooks at uniqueness walls. This part goes into the messaging guarantees underneath — and the industry's most common misconception: exactly-once delivery.
Engineering Principles Learned
- Exactly-once messaging is a lie; an effectively-once business outcome is achievable.
- Defense in depth: idempotency, dedup, uniqueness, outbox, and reconciliation work together.
- Each protection layer answers a different question; one layer alone isn't enough.
Continue reading
Continue reading
Next in series
Designing a Production Payment Engine
The synthesis of a 22-part series: an architectural checklist for a production payment engine built around a checkout orchestrator and provider gateway.
Next in series
Payment Recovery Pipeline and Runbooks
Automation first: the reconciliation worker and recovery pipeline. When uniqueness walls block replay, evidence-driven human runbooks take over.
Same series
What Fintech Companies Actually Hire For
Career perspective: fintech companies aren't looking for Stripe SDK skills — they want failure thinking, reconciliation, idempotency, and evidence-driven…