Skip to content

Build a transactional outbox

The outbox row is written in the Order transaction, while the relay records a consumed key in an inbox. This makes intent durable but is not a broker, a scalable relay, or an exactly-once guarantee.

Before you begin

Run composer run reference:test:outbox. Choose this tier only when an external integration needs durable intent.

Relay with an inbox guard

examples/reference-app/app/Pulsar/Services/Internal/Modules/Orders/UseCases/RelayOutbox.php — Transactional relay and inbox deduplication.

php
<?php

namespace App\Pulsar\Services\Internal\Modules\Orders\UseCases;

use Illuminate\Support\Facades\DB;

final class RelayOutbox
{
    // #region transactional-outbox-relay
    public function execute(string $messageKey): bool
    {
        return DB::transaction(function () use ($messageKey): bool {
            if (DB::table('inbox_messages')->where('message_key', $messageKey)->exists()) {
                return false;
            }
            DB::table('inbox_messages')->insert(['message_key' => $messageKey, 'created_at' => now(), 'updated_at' => now()]);
            DB::table('outbox_messages')->where('idempotency_key', $messageKey)->update(['relayed_at' => now(), 'updated_at' => now()]);

            return true;
        });
    }
    // #endregion
}

Verify and troubleshoot

The test proves atomic write, relay retry, and one inbox row. Production relays still need locking, retention, poison-record handling, monitoring, and throughput design.