Exactly-Once Delivery Doesn't Exist. Your Architecture Should Stop Pretending It Does.
July 14, 2026 · 5 min read
Somewhere right now, an architect is sitting in a vendor briefing, and a slide has just appeared with the words "guaranteed exactly-once delivery." Heads are nodding. A procurement decision is tilting. And a distributed system that will one day double-post a payment is quietly being conceived.
I run integration platforms at a financial institution — the kind of place where a duplicated event isn't a log-line curiosity, it's money moving twice. So let me say plainly what that slide never does: end-to-end exactly-once delivery, across arbitrary systems and a real network, is impossible. Not difficult. Not "hard until our next release." Impossible — and it has been since before Kafka existed, for reasons no roadmap can repeal.
The good news is that this impossibility doesn't matter, because the actual solution is older, simpler, and more reliable than the promise. But you have to stop believing the slide first.
The Two Generals Don't Care About Your Vendor
Here's the whole problem in one paragraph. A producer sends a message and waits for an acknowledgment. The message is processed — and then the acknowledgment is lost. A switch hiccups, a pod restarts, a timeout fires. The producer now faces a choice with no third option: retry, and risk creating a duplicate, or don't, and risk the message never having been processed at all. Pick your failure: at-least-once or at-most-once. "Exactly-once" is not on the menu, because the sender can never distinguish "processed, ack lost" from "never arrived."
This is the two-generals problem, and it's a theorem, not an engineering gap.
So what are vendors selling? Almost always, exactly-once processing within one carefully bounded scope — a stream-processing framework that can make read-process-write atomic inside its own world, with its own state store, its own offsets, its own transactions. That's real, and useful. But the moment your event crosses out of that world — into your database, your mainframe, a partner's API, an email — the bound is gone and the theorem is back. What the buyer hears ("nothing will ever be duplicated anywhere") and what the vendor means ("our internal topology won't double-count") are two different claims separated by a footnote nobody reads.
Duplicates Are Not the Problem. Fragility Is.
Once you accept at-least-once as the honest baseline, the design question transforms. It stops being "how do I prevent duplicates?" — you can't — and becomes "how do I make duplicates harmless?"
That property has a name: idempotency. A consumer is idempotent when processing the same event twice has the same effect as processing it once. And unlike exactly-once delivery, idempotency is entirely achievable, with tools your team already understands:
- Natural idempotency. Some operations are safely repeatable by their nature. "Set customer status to CLOSED" can arrive five times; the account is just as closed. Where you can express changes as absolute states rather than relative deltas, do it. "Set balance to $500" is repeatable; "add $50" is a time bomb.
- Idempotency keys. Every event carries a unique ID. Consumers record the IDs they've processed and silently skip repeats. It's a deduplication set with a retention window — boring, proven, effective.
- Conditional processing. Version numbers or optimistic concurrency: apply the event only if the target is in the expected prior state. Duplicates and out-of-order stragglers alike bounce off harmlessly.
The design-review question that should replace the vendor slide is this: "What happens if this event arrives twice?" If the answer is a shrug, the design isn't done. Ask it of every consumer, every time. It costs nothing and catches the failure mode that will otherwise find you in production, at month-end, during close.
The Bug Hiding in Plain Sight: the Dual Write
There's a second, sneakier version of this problem that lives on the producer side, and I'd estimate it's the most common correctness bug in event-driven systems today.
A service handles a request. It updates its database, and it publishes an event announcing the change. Two writes, two systems. Now ask the two-generals question again: what happens if the process dies between them? Update the database first, crash before publishing — your system changed state and told no one; downstream consumers are now silently wrong, forever. Publish first, crash before committing — the world has been told about a change that never happened. Either order, same disease: two systems that can disagree, connected by a gap the failure will find.
The standard cure is the outbox pattern, and it's beautiful in its dullness. Don't publish to the broker in the request path at all. Instead, write your business change and the event into the same database, in the same local transaction — the event goes into an "outbox" table. One transaction, one system, atomic by construction: either both happened or neither did. A separate relay process reads the outbox and publishes to the broker, retrying until acknowledged.
Notice what the relay's retries produce: possible duplicates. At-least-once, again. Which is fine — because your consumers are idempotent now. The whole architecture snaps together: outbox on the producer side, idempotency on the consumer side, at-least-once in between. Every piece is simple, comprehensible, and testable. No magic, no footnotes, no theorem violations. This combination has quietly powered correct financial systems for decades while the industry argued about delivery semantics.
Design for the Network You Have
There's a broader lesson here, and it's the thesis of the book this essay comes from: the systems that survive production are the ones designed for the failure modes that actually exist, not the guarantees the marketing promised. Retries exist, so duplicates exist, so consumers must tolerate them. Processes crash between writes, so dual writes must be eliminated, not hoped away. "The request" disappears in an asynchronous system, so observability has to be rebuilt around correlation, not call stacks.
None of this is glamorous. All of it is what separates an event-driven architecture from a distributed monolith with a message broker in the middle.
My new book, Designing Event-Driven Architecture: A Comprehensive Guide, treats these unglamorous parts as the main event: delivery guarantees and idempotency, event sourcing and CQRS (including when to say no — which is most of the time), sagas and compensating actions, schema evolution across forty consumers, observability for async systems, event-driven architecture in regulated industries, legacy integration, and how the same event backbone becomes the nervous system for AI agent systems. It's the second volume in the "Designing X" series, written — like the first — for architects who have to defend their decisions.







