Reliably publish an event exactly when a local database transaction commits, by writing the event to the same transaction instead of a separate call.
If a service writes to its database and then makes a separate call to publish an event, there's a gap between them: the database write can succeed while the publish call fails (network blip, broker down) — now the database says the order was placed, but nothing downstream ever heard about it.
Instead of publishing directly, the service writes the event into an "outbox" table as part of the very same local database transaction as the actual data change — so both succeed or both roll back together. A separate background process reads the outbox table and reliably publishes each event, retrying independently.
`placeOrder` writes both the order and its outbox event in one transaction — either both happen, or neither does. A separate poller does the actual publish.
class Database {
#tables = { orders: new Map(), outbox: [] };
transaction(work) {
// In a real database this is a single atomic commit covering both writes.
work(this.#tables);
console.log("db: transaction committed (order + outbox event together)");
}
}
const db = new Database();
function placeOrder(id, items) {
db.transaction((tables) => {
tables.orders.set(id, { id, items });
tables.outbox.push({ type: "OrderPlaced", orderId: id, published: false });
});
}
// A separate background process, polling independently and retrying on failure.
function publishOutboxEvents() {
for (const event of db["_Database__tables"]?.outbox ?? []) {
if (!event.published) {
console.log(`publisher: publishing ${event.type} for ${event.orderId}`);
event.published = true;
}
}
}
placeOrder("order-1", ["book"]);
publishOutboxEvents();