A webhook handler may save a change and then send a notification. If the process stops between those steps, the database can be correct while the listener never receives the intended message.

An outbox records the planned side effect in the same database transaction as the local state change. A worker delivers the outbox item later.

Keep the promise small

The transaction should save the state update and the information needed for the next step. It does not make the external email or message service part of that transaction. Delivery can still fail or have an unknown outcome.

Give each logical side effect a stable identity. Use a documented downstream idempotency mechanism where one exists, and keep delivery attempts separate from the fact that a notification was planned.

Store only the content the worker needs, with suitable protection for private fields. Set limits on retry count, processing leases, and how failed items become visible to an operator.

Check both interruption windows

First, stop the handler before its database transaction commits. Neither the state change nor its outbox item should appear as completed work. Next, stop after commit but before delivery. The outbox item should remain available for the worker.

Finally, simulate a delivery that succeeds but loses its response. Decide how your downstream system and duplicate controls handle the retry. An outbox alone does not guarantee exactly-once delivery to every external service.

This pattern is useful when a missed side effect matters. Keep simpler event handling for cases that do not need it, and document precisely which consistency problem the outbox is solving in your application.