0004 — RabbitMQ with a transactional outbox
Events are written to an outbox table; RabbitMQ is delivery only.
Superseded by 0041. The transactional outbox pattern is retained, but the broker is Azure Service Bus in all environments. RabbitMQ has been removed.
Decided. Domain events are inserted into an events table in the same
transaction as the state change. A publisher drains unpublished rows to the broker.
The table is the durable record; the broker is delivery.
Alternatives
- JetStream as both log and delivery (legacy). One system, but the log is also the system of record, which 0001 rejects.
- CDC / Debezium reading the WAL. Events for free, no application code.
- Google Pub/Sub. Fully managed, no ops.
- HTTP feeds only, no broker. Consumers poll
GET /v1/events?after=<id>.
Why an outbox rather than CDC
CDC payloads are row diffs shaped by the table schema, so every consumer couples to our column layout and a refactor breaks them. It also cannot express a business fact that spans two writes.
The outbox costs one INSERT and gives a curated domain fact with a chosen shape,
versioned by new event_type, replayable independent of storage.
Why RabbitMQ rather than Pub/Sub
Pub/Sub retention caps out around a month, so it cannot be the log. That is fine — under this design the log is the table — but the deciding factors were per-consumer credentials and permissions (which is how PII routing is enforced; see 0005), quorum queues, and a management UI usable for DLQ triage.
The broker is deliberately swappable: nothing in the data model depends on it. Moving to Pub/Sub or Service Bus is one publisher and one consumer wrapper.
Why keep the HTTP feed too
Bootstrapping a new consumer and catching up after an outage read the feed, not the broker. The feed is plain SQL over the same table, so it works regardless of broker retention.
Consequences
- At-least-once delivery. Consumers are idempotent, keyed on
events.id. - No cross-message ordering guarantee. Where order matters, compare
events.idand discard stale. eventsgrows forever; partition by month and archive cold partitions.- Table placement is per domain, not one service-wide
eventstable; see 0015. - Azure delivery is Service Bus with the same topic names; see 0016. RabbitMQ remains the local broker.