The outbox pattern, because two writes are not atomic

Writing a row and publishing a message are two systems, and any ordering of them has a window where one succeeded and the other did not.

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

    Outbox::create([
        'topic'   => 'orders.placed',
        'payload' => json_encode($order->toEvent()),
    ]);
});

// a separate process reads the outbox and publishes,
// marking rows as sent. at-least-once, by construction.

Both writes are in one transaction because they are in one database, which is the entire trick — the message becomes durable at the same instant as the state change it describes. The publisher is a separate concern and may publish a message twice if it crashes after publishing and before marking, so consumers must be idempotent regardless. The outbox table needs pruning, and a publisher that falls behind is an unbounded queue in your primary database.