ADR-0003: Transactional outbox pattern for guaranteed event delivery
Status
AcceptedTags
messaging, nats, events, reliability, distributed-systems, outboxDecision
Use the transactional outbox pattern for all critical event publishing. Events are written to anoutbox_events table in the same database transaction as the business write, then a background relay worker publishes them to NATS JetStream.
Why
Without the outbox pattern, publishing to NATS after a DB write creates a dual-write risk: if NATS is down or the app crashes between the two operations, the event is permanently lost while the DB record exists. The outbox pattern eliminates this by making the event write part of the same transaction. If the transaction rolls back, the event is never created. If NATS is down, the relay worker retries until it succeeds.How it works
Trade-offs
| Zero data loss | Events survive NATS outages |
| Automatic retry | No manual recovery |
| Audit trail | Full event history queryable in DB |
| Eventual consistency | 1–2 second publish delay |
| Storage overhead | Outbox table grows; needs cleanup job |
Rules for agents
- Always write the outbox event inside the same
db.Transactionas the business entity write — never after the transaction commits - Use
outboxRepo.Save()inside the transaction, nevernats.Publish()directly in business use cases - The relay worker is the only code that calls
nats.Publish() - Dead letter events must be logged with
event_id,event_type, andaggregate_idso ops can replay them manually - Cleanup: call
outboxRepo.DeletePublished(ctx, 7)daily