Guide

Inside one database, a transaction either commits everything or nothing. Across two systems, that gift is gone. A dual write is code that writes to two systems without a shared transaction. If the process crashes between the two writes, one write has happened and the other hasn't. Nothing in the system will fix that gap on its own.

Concept

A dual write is any function that changes state in two places, such as a database and a message broker, with no transaction that covers both.

The big idea

Consider the naive checkout code in an order service:

TypeScript
async function checkout(cart: Cart) {
  const order = await ordersDb.insert({ ...cart, status: "PLACED" });
  await payments.charge(cart.userId, cart.total);   // remote service
  await inventory.reserve(cart.items);              // remote service
  await kafka.publish("order-placed", order);       // message broker
}

Each line is a separate operation that can fail on its own. The sequence goes wrong in several ways:

  • The charge succeeds, then the inventory call fails because an item is out of stock. The customer paid for something you can't ship.
  • The process crashes after the charge but before publishing the event. The order is paid, but the warehouse never hears about it.
  • The charge call times out. Did it happen? See the unknown-outcome problem in failure models.
  • The database insert commits, the Kafka publish fails, and a retry inserts a second order.

Three ways out

You have three broad options:

  1. Make the systems take part in one atomic commit protocol. This is two-phase commit. It is strong and expensive.
  2. Accept temporary inconsistency and repair it with compensating steps. This is the saga approach.
  3. Restructure so one operation touches only one store. The transactional outbox does this for the common case of "update the database and publish an event". It writes the event into the same database, in the same transaction as the change.

Common mistakes

  • Publishing before writing. The event announces a change that may then roll back, so consumers act on something that never happened.
  • Assuming a retry loop solves it. A retry can repeat the first write and create a duplicate. The fix is idempotency, not repetition.
  • Ignoring the window. Every line between two writes is a possible crash point. Ask what happens if the process dies right there.

Check yourself

1. Why is publishing before writing worse than writing before publishing? The event announces a change that may roll back. Consumers then act on a fact that never became true.

2. Name one way to remove the window entirely for "update a database and publish an event". Use an outbox: write the event into the same database, in the same transaction as the change, and let a relay publish it.

Key takeaways

  • A dual write is a crash window between two systems that have no shared transaction.
  • The options are atomic commit across systems, compensating steps, or restructuring so one store is touched.
  • For the common "change the database, then publish" case, the outbox removes the window.

Further learning

Next, see what a single atomic commit across several databases costs in two-phase commit.