Production systems · Context: Documentation-based; hypothetical example · 7 min read
The order was saved. Why was the confirmation never sent?
A practical guide to the transactional outbox: save business data and the intent to notify together, then make the remaining work recoverable without promising exactly-once delivery.

In this article
The order was saved. Why was the confirmation never sent?
A practical guide to the transactional outbox: save business data and the intent to notify together, then make the remaining work recoverable.
*Prepared with AI assistance for research, drafting and editing. This is a documentation-based explanation with a hypothetical example, not a report of a system I built or tested.*
Imagine an online shop. A customer places an order, the application saves it, and the page displays a confirmation. The application then tries to trigger a confirmation email. Between those two steps, the process stops.
The order exists. The notification work may not.
Retrying the customer's whole order is not necessarily the right repair: the business action already succeeded. The missing piece is a durable record of the work that still needs doing.
A **transactional outbox** addresses that gap by saving the business change and an event describing it in the same database transaction. A separate worker publishes committed events afterward. The useful promise is narrower than “the customer definitely received an email”: the intent to notify is recorded alongside the order and can be recovered. [1]
Two successful calls are not one transaction
Consider this sequence: save the order, then publish an OrderCreated event to a message broker. A broker is the service that carries the event to another part of the application.
If the process stops after the database commit but before publication, the downstream service has nothing to act on. Reversing the calls creates the opposite problem: an event can escape before the order is committed, and the database operation might then fail. [1]
Wrapping the database statements in a transaction does not, by itself, bring a remote broker or email provider inside that transaction. PostgreSQL's transaction tutorial explains the all-or-nothing behavior of the database work; AWS's outbox guidance explains the failure between the database write and the separate notification. [1, 2]
The design question is: **after the order commits, where can the system find the unfinished notification work?**
Save the order and the intent together
In the outbox design, the application writes two records in one local transaction: - The order. - An outbox event, such as OrderCreated, with a stable event ID, the order ID and the data the consumer needs.
Either both records commit, or neither does. A separate relay reads committed outbox events and publishes them to the broker. Here, “relay” just means the worker responsible for that handoff. [1, 2]
The flow is:
**One database transaction:** order + outbox event → commit.
**Later:** relay → broker → notification service → email provider.
This separates accepting the order from completing the notification. If the relay is temporarily unavailable, committed events remain available for recovery, provided the system retains them and the relay resumes. That is a recovery path, not a promise that every downstream service will eventually succeed without intervention.
For the shop example, a status such as “order accepted; confirmation pending” may be more honest than treating the database commit as proof of email delivery. The exact wording depends on the product's contract with its customer.
The crash window moves—it does not disappear
Suppose the relay publishes an event successfully, then stops before recording that handoff in its own database. When it resumes, it may publish the same event again. AWS explicitly flags duplicate messages as an outbox consideration. [1]
Recording the event as published *before* attempting publication is not a safe shortcut: a crash in between could make undelivered work look finished. Recording it afterward leaves the possibility of a repeat.
That is why an outbox is not an end-to-end exactly-once guarantee. The design also needs a plan for repeated events.
A stable event ID lets the receiver recognize the same logical event across retries. But an ID alone does not make processing idempotent. Idempotent processing means repeating the same operation does not create an additional effect. [3]
When the effect is another database update, recording the processed event ID and applying the business change in one transaction can avoid a gap between those two steps. An external email call is different: a local “processed” flag cannot atomically cover the remote provider. If the provider offers an idempotency contract, inspect its scope, retention and behavior before relying on it. Otherwise, duplicate-email risk and ambiguous outcomes still need explicit handling. [3]
A timeout is particularly important: it can mean the caller did not receive a response, not that the remote action never happened. Blindly sending a new request with a new identity can turn uncertainty into a duplicate. [3]
Walk through three failures before choosing the design
Use this as a design review or a test plan—not as a claim that these tests have been run.
**1. The application stops before the transaction commits.**
Expected property: neither the order nor its outbox event is committed. Check that both writes actually participate in the same transaction; two adjacent calls are not enough. [1, 2]
**2. The application stops after commit but before the relay publishes.**
Expected property: the committed event remains discoverable. On recovery, the relay can attempt the handoff without recreating the customer's order. Verify retention, worker recovery and how failed events become visible. [1]
**3. The relay stops after publication but before recording completion.**
Expected property: the receiver can handle a repeat under the chosen delivery contract. Check the event identity and the consumer's actual effect—not just whether the broker accepted a message. For email, provider acceptance is also not evidence that a message reached the customer's inbox. [1, 3]
These failures tell you more than a happy-path demo. They reveal whether the design has a durable recovery point and where it still depends on another system's guarantees.
What operating the outbox adds
An outbox creates work you must own: a relay, retry behavior, retention and cleanup, and a way to inspect failures. AWS also calls out ordering; a basic outbox does not automatically establish the order your business needs. Define whether order matters for events about the same order, and verify the worker, broker and consumer behavior together. [1]
Useful operational questions include:
- How old is the oldest event still awaiting handoff? - Which events keep failing, and who can inspect or replay them safely? - When can published records be removed without undermining the required recovery window? - If messages are repeated or arrive out of order, what does the customer actually experience?
These are suggested design questions, not measurements from an implementation. There is no universal alert threshold or retention period that follows from the pattern alone.
When is it worth the extra machinery?
Consider an outbox when a committed business change must reliably lead to downstream work and a separate publish call leaves an unacceptable gap. If an occasional lost notification is acceptable, a simpler design may be an explicit trade-off. If one existing transactional system already durably records both the business change and the pending job, inspect that contract before adding another mechanism.
The outbox does not repair a notification service that always rejects a malformed event, decide how long customers should wait, or prove delivery. It gives the application a durable place to say: this change happened, and this related work is still owed.
For your next review, draw the database commit and the external handoff as separate points. Stop the process between them. Then ask where the unfinished work lives—and what happens if it runs twice.
Sources
[1] AWS Prescriptive Guidance, [Transactional outbox pattern](https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html).
[2] PostgreSQL documentation, [Transactions](https://www.postgresql.org/docs/current/tutorial-transactions.html).
[3] AWS Builders' Library, [Making retries safe with idempotent APIs](https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/).
Sources consulted 7 November 2026. These are documentation references, not evidence of an Obada implementation, benchmark or delivery result.
Cite this work
Obada Alsehli··n.d.
@article{alsehliwritingsavedordermissingconfirmation,
author = {Alsehli, Obada},
title = {{The order was saved. Why was the confirmation never sent?}},
howpublished = {\url{https://obadaalsehli.com/writing/saved-order-missing-confirmation}}
}Academic attribution & reproducible reference format
BIBTEX