The outbox pattern, in the smallest version that works

Writing a row and publishing an event are two systems, and there is no way to make them atomic — so either the row exists and no event was sent, or the event was sent and the transaction rolled back.

DB::transaction(function () use ($order) {
    $order->save();

    // same transaction, same database. either both or neither.
    DB::table('outbox')->insert([
        'topic'      => 'orders.placed',
        'payload'    => json_encode($order->toEventPayload()),
        'created_at' => now(),
    ]);
});

// a separate worker reads the outbox and publishes, marking as sent

The relay gives at-least-once delivery — it can publish and crash before marking the row sent — which is why consumers must be idempotent regardless. Polling the outbox every second is unglamorous and works; reading the binlog is the sophisticated version and is a lot more machinery. The failure this removes is the silent one, where a system is internally consistent and everything downstream is missing an event nobody noticed.