The dual write problem, and why the outbox exists

Writing a row and publishing an event are two systems, and there is no ordering of the two that is safe.

// publish first: the event may describe a row that does not exist
$this->events->publish(new OrderPlaced($order));
$order->save();                     // ← crash here

// save first: the row exists and nothing downstream knows
$order->save();
$this->events->publish(new OrderPlaced($order));   // ← or here

// the outbox: one transaction, one system
DB::transaction(function () use ($order) {
    $order->save();
    DB::table('outbox')->insert(['topic' => 'orders.placed', /* ... */]);
});

Neither ordering is correct and both are shipped constantly, because the failure window is small and the symptom is a missing downstream record that nobody attributes to this. The outbox moves the atomicity into one system, which is the only place it can exist. A relay reading the table gives at-least-once delivery, so consumers must be idempotent regardless — that requirement does not go away.