Worked example — the Transactional Outbox table, in SQL and as the YAML you'll write.
The outbox is just a regular DB table. Every state-changing transaction writes both the new state and one row into the outbox — atomically. A separate publisher polls for un-dispatched rows and pushes them to the broker:
CREATE TABLE orders_outbox (
id BIGSERIAL PRIMARY KEY,
aggregate_id UUID NOT NULL, -- the Order id
event_type TEXT NOT NULL, -- e.g. 'OrderPlaced'
payload JSONB NOT NULL, -- the event body
occurred_at TIMESTAMPTZ NOT NULL, -- when the fact happened
dispatched_at TIMESTAMPTZ NULL -- NULL = not yet published
);
CREATE INDEX ON orders_outbox (dispatched_at) WHERE dispatched_at IS NULL;
Same schema, expressed in our seed YAML shape (this is what the playground asks for):
table: orders_outbox
columns:
- { name: id, type: bigserial, pk: true }
- { name: aggregate_id, type: uuid, nullable: false }
- { name: event_type, type: text, nullable: false }
- { name: payload, type: jsonb, nullable: false }
- { name: occurred_at, type: timestamptz, nullable: false }
- { name: dispatched_at, type: timestamptz, nullable: true }
Key invariants:
payload is the serialized event body — exactly the structure you defined in Level 3 lesson 1.
dispatched_at IS NULL means "not yet sent". The publisher polls for those, sends them, then writes the timestamp.
- Because the outbox row is written in the same DB transaction as the order itself, you can never "forget" to publish an event.