~learn.
Code lab · World 03 · Messaging and Async Processing

Transactional outbox relay.

Write the order and its event in one transaction, relay events in order, survive a crash between publish and mark, and dedupe in the consumer.

Hard~45 min2 example + 10 hidden tests+120 XPJump to the code ↓
Saving also stores the Python runtime (~13.5 MB, once for every lab) so this lab runs with no internet.

Writing the order to the database and then publishing OrderCreated to Kafka is a dual write: a crash between the two loses the event (Day 2, chapter 1). The transactional outbox writes the order and the event to the database in one transaction; a separate relay publishes outbox rows and marks them published (section 2.2).

The relay can still crash after publishing and before marking. On restart it publishes the same event again, so the pipeline is at-least-once, and consumers deduplicate by event id (section 3.2, solution 1). At-least-once delivery plus an idempotent consumer is what "exactly-once" means in practice: every event takes effect once.

You get a fake database with transactions and a fake broker (see Given). Build three pieces:

1. create_order(db, order_id, user_id, amount): inside one with db.transaction(): block, db.insert_order(...) and db.insert_outbox(aggregate_type="Order", aggregate_id=order_id, event_type="OrderCreated", payload={"order_id": order_id, "user_id": user_id, "amount": amount}). Return the outbox event id. If either write fails, neither may persist.

2. OutboxRelay(db, broker, clock, batch_size=100) with run_once() -> int:

  • fetch up to batch_size unpublished rows, oldest id first (db.fetch_unpublished(limit))
  • publish each to topic f"{aggregate_type.lower()}.events" (so order.events), key aggregate_id, value {"event_id": id, "event_type": ..., "payload": ...}
  • after each successful publish, db.mark_published(id, clock.now())
  • if broker.publish raises BrokerUnavailable, stop the batch there: the failed event and everything after it stay unpublished, so order is kept. Return how many were published
  • a Crash is the process dying: let it propagate (it is a BaseException, so a plain except Exception won't swallow it)

3. IdempotentConsumer(handler) with receive(message) -> bool: call handler(message) the first time an event_id arrives and return True; return False for a duplicate. Remember an event id only after the handler succeeds, so a handler that raised gets the redelivery.

Python 3.14 loads on first run (~6.5 MB once)14
Loading the editor…
not run yet · 8 s limit
press Run tests

Write your code, then run 2 example tests and 10 hidden tests. Each run is limited to 8 seconds: an infinite loop gets stopped and Python restarts, your code stays.