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.
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_sizeunpublished rows, oldest id first (db.fetch_unpublished(limit)) - publish each to topic
f"{aggregate_type.lower()}.events"(soorder.events), keyaggregate_id, value{"event_id": id, "event_type": ..., "payload": ...} - after each successful publish,
db.mark_published(id, clock.now()) - if
broker.publishraisesBrokerUnavailable, stop the batch there: the failed event and everything after it stay unpublished, so order is kept. Return how many were published - a
Crashis the process dying: let it propagate (it is aBaseException, so a plainexcept Exceptionwon'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.
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.