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.

Distributed payment engine architecture diagram

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

  1. Are API charge requests protected by idempotency keys?
  2. Is there message-id dedup on webhook and async consumers?
  3. Is there a uniqueness constraint on idempotency key in the payment table?
  4. Are state changes and event publishing in the same transaction via the outbox pattern?
  5. Does the finalize handler skip without reprocessing when already in terminal state?
  6. Does reconciliation periodically scan for drift that layers 1–5 missed?

What to take away

  1. Exactly-once messaging is a lie; at-least-once + failure makes duplicates unavoidable.
  2. An effectively-once business outcome is achievable through defense in depth — one layer isn't enough.
  3. Each protection layer answers a different question; all must work together.
  4. 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

Next in series

Same series

Paylaş