Skip to content

Table Schema

The outbox_events table is the heart of the Transactional Outbox pattern. It is designed to be highly performant for concurrent polling relays while keeping a strict audit trail of every event.

outbox_events Schema Reference

Column Type Purpose
id BIGSERIAL Sequential Primary Key. Ensures new rows are appended to the end of the table, avoiding B-tree index fragmentation.
event_id UUID Business ID of the event. It is automatically mapped to the Nats-Msg-Id header to enable JetStream deduplication. This ID remains stable across retries.
subject TEXT The NATS JetStream subject where this event will be published (e.g. order.created).
headers JSONB Optional NATS headers. The relay will always automatically inject Nats-Msg-Id here.
payload JSONB The body of the event.
status TEXT Tracks the lifecycle. Valid values are pending, published, or failed.
retry_count SMALLINT Number of failed publish attempts. Used to trigger dead-lettering after OUTBOX_MAX_RETRIES.
last_error TEXT Stores the last exception traceback for debugging purposes if a publish fails.
aggregate_id TEXT The ID of the source entity (e.g., the Order ID). Used for ordering and observability.
aggregate_type TEXT The class name of the source entity (e.g., Order).
created_at TIMESTAMPTZ When the event was initially written to the outbox.
updated_at TIMESTAMPTZ Timestamp of the last relay modification. Kept accurate via a Postgres Trigger.
scheduled_at TIMESTAMPTZ The earliest time this event is allowed to be published. Exponential backoff pushes this forward on failures.
published_at TIMESTAMPTZ The exact time a successful JetStream ACK was received.

Why use event_id and not id for NATS?

The id is a sequential integer (BIGSERIAL). While useful for database performance, it is specific to the database sequence state. The event_id is a UUIDv4 generated by the application at the exact moment the event is staged. By mapping this UUID to the Nats-Msg-Id header, NATS JetStream can perform strictly deterministic deduplication, even if the database sequence was reset or if the event was re-imported from a backup.