Build a queue workflow
The Internal ConfirmOrderJob carries IDs and an application idempotency key, never an Eloquent Model. Each delivery reconstructs actor and tenant context, authorizes, and enters ConfirmPendingOrder once.
The boundary uses a Job and Model; Domain Event and Listener work have separate ownership.
Before you begin
Use the reference app's fake queue and inbound adapter rules. Laravel queue delivery is at-least-once, so a retry may reach the Job more than once.
Keep the Job durable and thin
The Job declares its retry/backoff policy and passes the same key on every attempt. The UseCase owns the transaction and inserts the key before it confirms the Order.
examples/reference-app/app/Pulsar/Services/Internal/Modules/Orders/Jobs/ConfirmOrderJob.php — Durable payload, context reconstruction, and one UseCase call.
<?php
namespace App\Pulsar\Services\Internal\Modules\Orders\Jobs;
use App\Models\User;
use App\Pulsar\Domain\Orders\Models\Order;
use App\Pulsar\Services\Internal\Modules\Orders\UseCases\ConfirmPendingOrder;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Gate;
final class ConfirmOrderJob implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public function backoff(): array
{
return [10, 30, 60];
}
public function __construct(
public readonly int $orderId,
public readonly int $actorId,
public readonly string $tenantId,
public readonly string $idempotencyKey,
) {}
// #region confirm-order-job
public function handle(ConfirmPendingOrder $useCase): void
{
$actor = User::query()->findOrFail($this->actorId);
$order = Order::query()->findOrFail($this->orderId);
abort_unless($actor->tenant_id === $this->tenantId && $order->tenant_id === $this->tenantId, 403);
Gate::forUser($actor)->authorize('confirm', $order);
$useCase->execute($order->id, $this->idempotencyKey);
}
// #endregion
}examples/reference-app/app/Pulsar/Services/Internal/Modules/Orders/UseCases/ConfirmPendingOrder.php — Transactional idempotency guard and confirmation.
<?php
namespace App\Pulsar\Services\Internal\Modules\Orders\UseCases;
use App\Pulsar\Domain\Orders\Actions\ConfirmOrder;
use App\Pulsar\Domain\Orders\Models\Order;
use Illuminate\Support\Facades\DB;
final class ConfirmPendingOrder
{
public function __construct(private readonly ConfirmOrder $confirmOrder) {}
// #region confirm-pending-order-workflow
public function execute(int $orderId, string $idempotencyKey): Order
{
return DB::transaction(function () use ($orderId, $idempotencyKey): Order {
$existing = DB::table('processed_workflows')->where('idempotency_key', $idempotencyKey)->exists();
$order = Order::query()->lockForUpdate()->findOrFail($orderId);
if ($existing) {
return $order;
}
DB::table('processed_workflows')->insert(['idempotency_key' => $idempotencyKey, 'created_at' => now(), 'updated_at' => now()]);
return $this->confirmOrder->execute($order);
});
}
// #endregion
}Interpretation. The worker restores durable context before authorization and retries through the identical application-owned key.
Verify repeated delivery
Run composer run reference:test:queue. It proves dispatch payload fields, invokes the handler twice, confirms one Order, and finds one processed workflow record. This is idempotent business handling, not an exactly-once broker guarantee.
Troubleshoot
If a Job serializes a Model, replace it with an ID and reload it. Use a queued Listener when a Domain Event reaction owns the work.
Related tasks
Continue with the Artisan Command guide or read Command.