All notesEngineering12 Sept 2026 · 8 min read

One payout, one transfer.

How a locked job, a unique reference and one extra question to the provider keep money from moving twice.

Cover · Payouts

A payout is the one place in a money product where a bug costs real money and cannot be undone. Everything around it wants to run the same transfer twice: retries, timeouts, workers, webhooks.

Three ways a payout runs twice

  1. The provider times out. You retry, but the first request had actually gone through.
  2. Two workers pick up the same job at the same moment.
  3. The provider sends the same webhook twice, and each one triggers a follow-up.

None of these are exotic. Each one happens in the first month of any payout system, so the fix has to be built in, not patched on.

Lock it, name it, ask first

The job is unique per payout, the row is locked while it is processed, and every transfer is sent with a unique gateway_partner_ref. A second worker simply finds the status has already changed and leaves.

01Lock the row
02Check the status
03Send with a unique reference
04On retry, ask the provider first
SendPayout.php · simplified
<?php

class SendPayout implements ShouldQueue, ShouldBeUnique
{
    public function uniqueId(): string
    {
        return "payout:{$this->payoutId}";
    }

    public function handle(): void
    {
        DB::transaction(function () {
            $p = Payout::lockForUpdate()->findOrFail($this->payoutId);

            if ($p->status !== 'pending') {
                return; // someone else already moved it
            }

            $this->provider->send($p->gateway_partner_ref, $p);
        });
    }
}

The last step matters most. After a timeout, the system does not blindly resend. It asks the provider whether that reference already exists, and only sends again if it does not.

“Before any retry, ask the provider whether the first attempt already went through.”

Webhooks are retries from the other side

The same idea applies in reverse. Every incoming webhook is deduplicated before it touches a balance, so a repeated message becomes a no-op instead of a second credit.

If you remember one thing: make the second attempt boring. The best payout code is the code that, when run twice, does nothing the second time.

Pascal AdnanFullstack developer & founder · Jakarta
#payments#laravel#idempotency
Next note · 02 / 10A ledger that never edits an entry.