Skip to content
Katabench
Try free
Course preview Katabench Labs for .NET developers

Build a transactional outbox.
Then prove it survives failure.

Orders are saved, but the warehouse never hears about them. Fix a small .NET service backed by real PostgreSQL and RabbitMQ: store the event with the order, relay it reliably, make duplicates harmless, and quarantine poison messages. About 30 minutes.

Intermediate · C#, PostgreSQL, RabbitMQ · About 30 minutes · Disposable browser workspace · Pro workspace in private beta

An order is saved together with its outbox row in one PostgreSQL transaction, a relay publishes the row to RabbitMQ and marks it processed, a consumer deduplicates by message id, and failed rows retry with backoff before being dead-lettered.
The order, outbox, relay, and consumer flow the course builds.

One short lab, four lessons

Fix a dual write, one lesson at a time

Each lesson starts from the code the previous one left behind. You run a scenario that shows the failure, change a few lines, and run it again to prove the fix, so the last lesson tests the relay you actually built.

  1. Lesson 1

    Store the event with the order

    The order commits, then the publish fails and the event is gone. Write the order and its outbox row in one PostgreSQL transaction.

  2. Lesson 2

    Relay the event and mark it processed

    Run the relay and watch it publish the same rows on every poll, then mark each row processed once RabbitMQ confirms it.

  3. Lesson 3

    Make duplicates harmless

    Lose a publisher confirm and the retry delivers twice. Reuse the outbox id as the message id and let the consumer's inbox ignore the repeat.

  4. Lesson 4

    Retry failures, quarantine poison

    One bad row blocks every order behind it. Retry it with backoff, then dead-letter it after the last attempt.

Implement the Transactional Outbox pattern

1 lab · 4 lessons · about 30 minutes · Intermediate

See the lesson loop

The lesson loop

Focused changes, not typing marathons

The code that matters is open in the editor: the order service, the relay, the consumer, the RabbitMQ publisher, and the schema. You write the few lines that carry the pattern. If you get stuck, Reveal solution loads the working code for the lesson.

OrderService.cs
await using var transaction = await connection.BeginTransactionAsync(ct);

await using var order = new NpgsqlCommand(
    "INSERT INTO orders (id, customer_id, total_cents, status) VALUES ($1, $2, $3, 'placed')",
    connection, transaction);
// bind the order values, then ExecuteNonQueryAsync

await using var outbox = new NpgsqlCommand(
    "INSERT INTO outbox_messages (id, event_type, payload) VALUES ($1, $2, $3)",
    connection, transaction);
// bind the event id, "order.placed", and the payload as jsonb

await transaction.CommitAsync(ct);
  1. 1

    Reproduce

    Run the lesson's scenario and watch it fail against real PostgreSQL and RabbitMQ.

  2. 2

    Inspect

    Read the failure line and the counts behind it: orders, outbox rows, queue depth, and shipments.

  3. 3

    Patch

    Make one focused change of a few lines in the files the lesson names.

  4. 4

    Prove

    Press Check. It reads your terminal output and reruns the scenario against the live services.

Executable evidence

The check reads the systems, not your source

PostgreSQL

The order and its outbox row committed by one transaction, processed markers, attempt counts, and dead letters.

RabbitMQ

Publisher confirms, a durable queue, the message id on every delivery, and the queue depth after each poll.

Consumer

An inbox table that turns a redelivered message into a no-op instead of a second shipment.

Failures

A broker outage, a lost confirm, and a poison message, injected on purpose so each fix has something to survive.

Get notified when Labs opens

Live Pro workspaces are in private beta. The opening is announced in the Katabench newsletter, a short note when fresh kata land. No spam, unsubscribe anytime.

Try a graded kata while you wait