---
title: "Overdraft protection — Credits for Laravel | Roundly"
description: "Deductions below the floor throw InsufficientCreditsException by default; allow overdrafts per call or globally, and race-safely."
url: https://roundly-consulting.com/open-source/docs/credits-for-laravel/overdraft-protection
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Credits for Laravel](https://roundly-consulting.com/open-source/docs/credits-for-laravel.md)

# Overdraft protection

By default a deduction that would take the balance below minimum\_balance (zero by default) is rejected with an InsufficientCreditsException — balances cannot silently go negative, and nothing is written. On the facade, allowOverdraft() returns a scope that may go below the floor:

```php
use RoundlyConsulting\Credits\Exceptions\InsufficientCreditsException;

try {
    Credits::for($user)->deduct(1000, 'big purchase');
} catch (InsufficientCreditsException $e) {
    // $e->requested, $e->available, $e->minimum, $e->creditable — nothing was written
}

Credits::for($user)->allowOverdraft()->deduct(1000, 'manual debit');   // may go below the floor
Credits::for($user)->allowOverdraft()->setTo(-50, 'write-off');
```

The same through the trait:

```php
use RoundlyConsulting\Credits\Exceptions\InsufficientCreditsException;

try {
    $user->modifyCredits(-1000, 'big purchase');
} catch (InsufficientCreditsException $e) {
    $e->requested;    // -1000 — the signed amount that was refused
    $e->available;    // 70 — the bucket's balance when the guard decided
    $e->minimum;      // 0 — the floor the guard enforced (credits.minimum_balance)
    $e->creditable;   // the entity
    $e->getMessage(); // 'Insufficient credits: tried to deduct 1000 but only 70 are available.'
}
```

## Allowing an overdraft

Allow a negative balance for a single call — allowOverdraft() on the scope, or the allowOverdraft argument of the trait methods:

```php
$user->modifyCredits(-1000, 'manual debit', allowOverdraft: true);

$user->setCreditsTo(-50, 'write-off', allowOverdraft: true);
```

Or globally, via the allow\_overdraft config key or its env variable. The env value is read as a boolean: 1, true, on and yes enable it; 0, false, off and no leave it off; anything else throws InvalidConfigurationException naming the key:

```bash
CREDITS_ALLOW_OVERDRAFT=true   # 1, true, on or yes — 0, false, off or no leave it off; anything else throws
```

## A custom floor

minimum\_balance is the floor the guard enforces. Raise it to keep a reserve, or set it below zero for a bounded overdraft:

```php
// config/credits.php — a bounded overdraft: balances may fall to -500, never below
'minimum_balance' => -500,
```

With a floor other than 0, the exception says what can still be spent above it and $e->minimum carries the floor — with minimum\_balance 50, a refused debit of 60 on a balance of 100 reads “Insufficient credits: tried to deduct 60 but only 50 can be spent above the minimum balance of 50.”

The guard is enforced per bucket: a deduction against the purchased bucket can never draw on promotional credit. Grants never pass through the guard.

## Concurrency

The balance is never a stored column — it is the sum of an append-only ledger. Every change (add, deduct, modify, setTo and their Money forms) runs in one transaction that first takes the owner’s lock — an UPDATE of the owner’s row in credit\_locks — then reads the bucket’s ledger rows under lockForUpdate(), decides (the overdraft guard for a debit, the delta for setTo()) and appends the new row. Racing changes of one owner therefore serialise on every isolation level:

- READ COMMITTED (the Postgres default): the second change waits for the first, then reads the balance the first left behind — also in an empty bucket with a negative minimum\_balance.
- MySQL / MariaDB REPEATABLE READ (their default): the lock and the ledger read are current reads, so they see the first change’s committed row too.
- Postgres REPEATABLE READ / SERIALIZABLE: the second change’s snapshot predates its wait, so rather than decide on stale data it fails with a serialisation failure (SQLSTATE 40001), and the package retries its transaction — up to 5 attempts — with a fresh snapshot.

## Inside your own transaction

Inside your own DB::transaction(), the snapshot belongs to your transaction, so the package cannot retry for you: on Postgres REPEATABLE READ / SERIALIZABLE a racing change surfaces as Illuminate\\Database\\DeadlockException (SQLSTATE 40001) and nothing is written. Retry the whole transaction — DB::transaction($callback, attempts: 3) does not, because the nested exception no longer carries the SQLSTATE Laravel’s retry checks for:

```php
use Illuminate\Database\DeadlockException;
use Illuminate\Support\Facades\DB;
use RoundlyConsulting\Credits\Facades\Credits;

retry(3, fn () => DB::transaction(function () use ($user): void {
    // … your own reads and writes …
    Credits::for($user)->deduct(30, 'purchase');
}), when: fn (Throwable $e): bool => $e instanceof DeadlockException);
```

The lock is held only for that short transaction. Because every change is written as a new delta row (never an absolute balance), a change is never lost, and credit\_locks holds a version counter only — the ledger stays the single source of truth.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/credits-for-laravel.md)
