1.What the outbox table itself needs to store, and why it is not a generic event log
The outbox table has a narrower, more specific job than a generic event log — it exists purely to hold events that are waiting to be relayed, not to be a permanent historical record. It needs enough structure for the relay to reliably pick up, publish, and then retire each row: an identifier for the event, enough context to route or serialize it correctly (what kind of event it is, what business entity it relates to), the actual payload, and a way to track whether it has been published yet. Once an event is confirmed published, the outbox row has done its job — unlike an event log, which is often kept indefinitely as a source of truth, an outbox table is a transient staging area that is safe to prune once the relay has finished with a row.
Key features
- A practical outbox row typically needs: a unique event ID, an aggregate/entity type and ID (what business object this event is about), an event type (what kind of thing happened), a serialized payload, a created timestamp, and a published flag or timestamp
- The published flag (or a nullable published_at timestamp) is what the relay updates only after confirmed delivery — this is the same "mark on confirmed success, not optimistically" discipline the relay concept in this section already covers
- Unlike a general-purpose event log meant to be a durable, queryable historical record, an outbox table is a transient staging area — its rows exist only to bridge the moment between "committed locally" and "confirmed published," and have no ongoing purpose after that
- Because it is transient, an outbox table needs an explicit cleanup/retention policy (delete or archive rows past a certain age once published) — without one, a table meant to be small and fast to scan grows indefinitely and its own performance degrades over time
- If a durable historical record of every event actually is needed for other reasons (audit, replay, analytics), that is a genuinely different requirement best served by a separate event log or stream (e.g. writing the same events to Kafka after they are published, or reading the outbox's own already-published rows into a separate durable store) — not by keeping outbox rows around indefinitely for a purpose they were never designed for
A typical outbox row shape, and outbox vs a general event log
- Outbox table
- Transient staging until relayed
- Deleted/archived once published
- Queried for almost nothing after publish
- General event log
- Durable historical record
- Kept indefinitely, or per long-term policy
- Queried for auditing, replay, analytics
Command
CREATE TABLE outbox (
event_id UUID PRIMARY KEY,
aggregate_type TEXT NOT NULL,
aggregate_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ -- NULL until relayed
);An outbox table needs just enough structure to relay events reliably — an ID, entity/event type, payload, and a published flag — and is a transient staging area, not a permanent event log. It needs its own retention/cleanup policy for rows already confirmed published, or a table meant to stay small keeps growing indefinitely for no remaining purpose.