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.