The trap is the dual write: your service updates its database and publishes an event to a broker. Those are two systems, two commits — and nothing makes them atomic. Crash in between and you’ve either changed your data without telling anyone, or announced something that never happened. Both are silent corruption, and both are miserable to debug after the fact.

The outbox pattern collapses the two writes into one. You insert the event into an outbox table in the same local transaction as the business change — a single atomic commit, no distributed transaction required. A separate relay (a poller, or change-data-capture tailing the DB log) reads unsent rows, publishes them to the broker, and marks them sent.

The relay can crash after publishing but before marking the row — so it may publish twice. That’s fine: it’s at-least-once, and your consumers were already idempotent. You got reliable event delivery while each service still owns its own store — no shared database.