An email confirming an order that does not exist. A colleague showed me this one last week, together with the mirror bug: an order that exists and never got its email.

The code was the obvious kind. Commit the Doctrine transaction, then dispatch to the AMQP transport. Two systems, two writes, no shared transaction. Publish after commit and the process can die between them, event lost. Publish inside the transaction and the broker gets an event for data that may still roll back. Both orders are wrong, and under real load this fires weekly.

The boring fix is the transactional outbox. One extra table:

CREATE TABLE outbox (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  event_type VARCHAR(255) NOT NULL,
  payload JSON NOT NULL,
  created_at DATETIME NOT NULL,
  published_at DATETIME NULL
);

The event row goes into the same transaction as the order. One database, one commit, so the event exists exactly when the order exists. A separate publisher reads unpublished rows, pushes them to the transport, marks them published. With Messenger this maps cleanly: the Doctrine transport can play the outbox, and a worker relays to AMQP.

The price is honesty about delivery. The publisher can crash after sending and before marking, so the same event goes out twice. That is at-least-once, and there is no cheap way around it. Every consumer needs an idempotency key, the outbox row id works. Store processed ids, skip repeats. This is not a detail on top of the pattern. It is the pattern.

No new infrastructure. No distributed transactions, no CDC pipeline. A table, a loop, a unique key on the consumer side.

I wrote the publish-after-commit version myself, on another project, three years ago. It is probably still there.