Back to path
IntermediateTradepost · Project 6 of 12 ~7h· 5 milestones

Emit events without losing them

Continues from the last build: Writes are transactional, idempotent, and the order state machine cannot be skipped.

Last rung you closed the hole where two shoppers could buy the last unit of the same product.

Transactional outbox patternBullMQ job producers and workersAt-least-once delivery semanticsHMAC webhook signing and verificationExponential backoff with jitterDead letter handlingConsumer-side idempotent event processingPostgres row locking for polling workloads

What you'll build

The API commits order state and its outbound event in one atomic write. A relay worker turns undispatched outbox rows into signed, retried webhook deliveries without ever losing one, and a documented receiver pattern makes duplicate deliveries harmless.

See how we teach, before you sign up

You don't just get code dumped on you. Every starter file and every solution is explained line-by-line, in plain English. Here's one real file from this project:

src/db/schema.tstypescript
export const outboxEvents = pgTable('outbox_events', {
  id: uuid('id').primaryKey().defaultRandom(),
  aggregateType: text('aggregate_type').notNull(),
  aggregateId: uuid('aggregate_id').notNull(),
  topic: text('topic').notNull(),
  payload: jsonb('payload').notNull(),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  dispatchedAt: timestamp('dispatched_at'),
});

export const deadLetterEvents = pgTable('dead_letter_events', {
  id: uuid('id').primaryKey().defaultRandom(),
  eventId: uuid('event_id').notNull(),
  payload: jsonb('payload').notNull(),
  lastError: text('last_error').notNull(),
  failedAt: timestamp('failed_at').notNull().defaultNow(),
});

Reading this file

  • dispatchedAt: timestamp('dispatched_at'),Nullable, no default, this is the flag the relay's poll query filters on.
  • payload: jsonb('payload').notNull(),Stores the exact event body that will be signed and sent, keep it a flat, versioned shape per topic.
  • eventId: uuid('event_id').notNull(),Links a dead-lettered delivery back to its originating outbox row for replay or investigation.
  • createdAt: timestamp('created_at').notNull().defaultNow(),The relay's poll query orders by this so old events are enqueued before newer ones.

The core new tables for this rung. dispatchedAt starts null and the relay flips it once, never resets it, so a row is never enqueued a third time under normal operation.

That's 1 of 7 explained code blocks in this single project.

The build, milestone by milestone

  1. 1

    Prove fire-and-forget loses events

    5 guided steps

    If you cannot reproduce the dual-write problem, you cannot prove you fixed it. Every later milestone's verify step compares against this baseline behavior.

  2. 2

    Add the outbox table and write it in the same transaction

    5 guided steps

    The outbox pattern turns a distributed problem, keep two systems in sync, into a local one, keep two rows in the same database in sync, which Postgres already guarantees you for free inside a transaction.

  3. 3

    Build the relay worker

    6 guided steps

    The relay is the bridge between a database row and a queue job. Polling on an interval, rather than trying to get a trigger to fire synchronously, keeps the relay simple and makes it safe to restart at any point without losing rows.

  4. 4

    Deliver webhooks like a grown-up

    5 guided steps

    A merchant's endpoint will be down sometimes, that is normal operations, not a bug. Retrying with backoff gives it time to recover without hammering it, signing proves the payload actually came from you, and a dead letter path means a permanently broken endpoint produces a page, not silence.

  5. 5

    Dedupe on the consumer side

    6 guided steps

    This is the piece that turns honest at-least-once delivery into an exactly-once effect for the merchant. It is also the single most common interview question about this whole rung, how do you not lose events, and the honest answer is you do not prevent duplicates, you make them harmless.

What's inside when you start

3 starter files, ready to clone
5 guided milestones
4 full reference solutions
7 code blocks explained line-by-line
5 "is it working?" checks
6 interview questions it prepares you for

You'll walk away with

outbox_events table written in the same transaction as every order state change, never after it
A relay worker process that polls undispatched rows, enqueues BullMQ jobs keyed by event id, and marks rows dispatched
A webhook delivery worker that signs payloads with HMAC-SHA256, retries 5 times with exponential backoff plus jitter, and dead-letters exhausted jobs
A documented receiver pattern showing signature verification and a processed_events unique constraint for consumer-side dedupe
An integration test suite proving both the original dual-write loss and its fix, plus a duplicate-delivery test proving the side effect fires once
A short doc merchants can copy verbatim for verifying inbound Tradepost webhooks

This is portfolio-grade. Build it free.

Sign up to unlock every milestone step-by-step, the code skeletons, full reference solutions, and checkable tasks, with your progress saved as you build.

Start building