Idempotency and the Transactional Outbox: Making Retries Safe
Learn why network retries create duplicate effects and how idempotency keys and an outbox close common reliability gaps.
On this page
Key takeaways
- A timeout does not tell a caller whether the remote side effect happened.
- Idempotency turns repeat delivery into a safe, explicitly defined operation.
- A database write and broker publish are not one atomic transaction by default.
- An outbox stores the event beside the business change and publishes it asynchronously.
A timeout creates uncertainty
Suppose a payment request reaches a service, the service charges the card, and its response is lost. The caller sees a timeout but cannot know whether the charge happened. Retrying may be necessary to make progress, yet a naive retry can charge twice. This is not an edge case; partial failure is a normal property of networked systems.
An idempotency key gives retries a stable identity. The client sends the same key for attempts representing the same intended action. The service records that key with the outcome and returns the existing result for a duplicate rather than repeating the side effect. The key must be scoped to the right principal and operation, retained long enough for realistic retry windows, and protected by a uniqueness constraint so concurrent duplicates cannot both win.
Make the business effect durable first
The idempotency record and the business change should be committed together whenever they live in the same database. Otherwise the service can crash after performing one write but before recording enough information to recognize the retry. Store a stable response or reference to the resulting resource when callers need a repeatable answer.
Idempotency does not mean every request with the same key is interchangeable. Validate that a repeated key carries the same material request intent. Reusing one key for different amounts or accounts should be rejected, not silently mapped to the first result.
Why the outbox exists
Consider updating an order row and publishing OrderPlaced to a broker. If the database commits and publishing fails, downstream consumers never hear about the order. If publishing happens first and the database rolls back, consumers receive an event for a change that does not exist.
The transactional outbox stores the business update and an event row in one database transaction. A separate publisher reads pending rows, sends events, and marks them delivered. The publisher may send an event more than once if it crashes after sending but before marking success, so consumers still need idempotent processing, usually keyed by event ID or aggregate version.
Operational details matter
Track outbox age and backlog, make publishing retryable with bounded backoff, and define a poison-message path for events that repeatedly fail. Preserve ordering where the domain requires it, typically per aggregate rather than globally. Retain enough metadata to diagnose which event is stuck and why.
Retries, idempotency, and an outbox do not create magical exactly-once execution across a network. They provide durable state transitions and safe repetition, which is the practical foundation for reliable distributed workflows.