Commit the change and its event together
Picture
Saving an order then publishing its event leaves a gap: a crash between the two can save the order without notifying anyone.
Predict
Does an outbox remove every possibility of duplicate event delivery?
Walk through the small example
No. A relay can resend after an uncertain send or acknowledgment.
Mental model and runnable Python model
Use one local transaction for the business row and outbox event with a stable event ID. A relay publishes committed events, retries transient failures, and marks progress. Consumers deduplicate or apply naturally idempotent operations with a durable boundary. Polling and change-data-capture relays have different operational tradeoffs. Preserve required per-aggregate order using sequence numbers or partition keys, not an assumed global queue order. Clean up outbox history according to replay and audit needs.
What the function promises: The code models an atomic business-row/outbox insert in one in-memory SQLite transaction. It does not implement the relay, broker, or durable external consumer.
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE orders (id TEXT PRIMARY KEY, status TEXT NOT NULL);
CREATE TABLE outbox (id TEXT PRIMARY KEY, order_id TEXT NOT NULL, payload TEXT NOT NULL);
""")
def create_order(order_id, event_id):
with db:
db.execute("INSERT INTO orders VALUES (?, ?)", (order_id, "created"))
db.execute("INSERT INTO outbox VALUES (?, ?, ?)",
(event_id, order_id, "order.created"))
create_order("o1", "e1")
try:
create_order("o2", "e1")
except sqlite3.IntegrityError:
pass
assert db.execute("SELECT id FROM orders ORDER BY id").fetchall() == [("o1",)]
assert db.execute("SELECT id FROM outbox").fetchall() == [("e1",)]
db.close()
Work and storage: Transaction and relay throughput depend on database indexes, write volume, batching, and retention. The example’s tables use primary-key uniqueness.
When to recognize it: A relay reads pending rows and sends them. It can crash after sending but before marking sent, so duplicates remain possible.
Check before applying it: Outbox provides atomic local state and publication intent, not universal exactly-once effects.
Change one rule
The relay sends e1, then crashes before recording success. What should the consumer do when e1 returns?
See what changes
Recognize the same event ID and avoid repeating a committed effect, or make the effect itself idempotent. Do not assume the outbox suppressed that resend.
Connect
Choose storage from reads and writes: Use one transaction for the business change and publication intent.
Coordinate a workflow without one global transaction: Coordinate several local transactions with explicit compensating behavior.