Architectural · Data & Consistency · DWG NO. 053

Outbox Pattern

Reliably publish an event exactly when a local database transaction commits, by writing the event to the same transaction instead of a separate call.

TypeData & Consistency
ScopeSingle service's write path
ComplexityMedium
Common inEvent-driven services, Saga participants, CDC pipelines

"Save to the database" and "publish an event" aren't one operation

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.

Write the event to an outbox table in the same transaction

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.

Local transactionorders tableoutbox tablepoll & publishPublishereventMessage Broker

Writing data and its event in one atomic transaction

`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.

outbox-pattern.js
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();

Trade-offs