Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Sequence Laravel Package

hatchyu/laravel-sequence

A Laravel package for generating customizable, concurrency-safe sequential numbers for invoices, orders, and other records. Database-backed sequences support configurable formats, prefixes, reset schedules, and easy model integration, helping you create human-readable IDs without collisions.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and its migration scaffolding first:

composer require hatchyu/laravel-sequence
php artisan vendor:publish --tag=sequence-config
php artisan vendor:publish --tag=sequence-migrations
php artisan migrate

Publish the config before migrations — the config file drives the sequences table name and default column layout, and changing it after migration means another php artisan migrate:fresh (or manual ALTER) in dev.

First use case: generate a daily invoice number for a new order. Wrap the number fetch inside the same DB transaction that persists the model, so a failed insert doesn't burn a sequence value:

use Illuminate\Support\Facades\DB;
use Hatchyu\LaravelSequence\Facades\Sequence;

DB::transaction(function () {
    $number = Sequence::next('invoice', group: now()->format('Y-m'));
    return Invoice::create([
        'number'  => $number,
        'user_id' => auth()->id(),
    ]);
});

The package stores counters in a sequences table with a unique (name, group) index, and obtains the next value via SELECT ... FOR UPDATE inside the surrounding transaction — this is the part you cannot hand-roll with SELECT MAX(id)+1 and expect to survive concurrent requests. If you already have a working next() call, you're 90% done.

Implementation Patterns

Per-tenant / per-year grouping. The group parameter is the escape hatch for every "reset on the first of the year" requirement you'll ever get. Model it as "{$tenantId}-{$year}" (or a period key) rather than tracking a year column on the sequence row — grouping keeps the unique index clean and lets you archive old groups whole:

$group = sprintf('%d-%s', tenant()->id, now()->format('Y'));
$po = Sequence::next('purchase_order', group: $group);
// → 2026-000142

Formatting without string math in your models. Let the formatter live close to the sequence definition. If the package exposes a format template (padding, prefix, date tokens), define it once per sequence name in config and call Sequence::format('invoice', $raw). Avoid str_pad($n, 6, '0', STR_PAD_left) scattered across controllers — that's the thing you'll regret at 500k rows when 000999 becomes 001000 and half your reports sort lexically.

Model trait integration. For models that "own" a sequence (an Order with an order_no, a Ticket with a ticket_ref), use the provided trait and hook into creating so the number is assigned even when the record is created from a seeder, factory, or queue job:

use Hatchyu\LaravelSequence\Concerns\HasSequence;

class Order extends Model
{
    use HasSequence;

    protected string $sequenceName  = 'order';
    protected string $sequenceColumn = 'order_no';

    protected function sequenceGroup(): ?string
    {
        return $this->tenant_id
            ? "{$this->tenant_id}-" . now()->format('Y')
            : null;
    }
}

The trait should assign the value inside the model's creating event, which means the outer create() call must be inside a transaction — otherwise the FOR UPDATE lock is released as soon as the select completes and you can still race.

Idempotent queue jobs. When generating numbers in a job, use ShouldBeUnique on the job and still keep the sequence lookup inside DB::transaction. Lock contention on a hot sequence (invoice at year-end) is real; consider one sequence per shard (invoice-{shard}) and concat at display time if you expect bursts above a few hundred/second.

Backfilling / importing. Turn off the trait when importing historical records by setting the sequence column explicitly and using Order::withoutEvents(fn () => ...) or by calling $model->sequenceColumn conditionally in creating. Don't let the trait overwrite imported numbers.

Testing. Sequences are stateful. Use RefreshDatabase (or at minimum truncate sequences in setUp) so a test that asserts INV-000001 doesn't collide with an earlier test that already consumed it. Add a dedicated concurrency test that spawns two short transactions against the same group and asserts distinct values — it's the one test that justifies the package's existence.

Gotchas and Tips

  • The lock requires an explicit transaction. Sequence::next() outside DB::transaction is not concurrency-safe — the FOR UPDATE row lock is released at statement end. If you call it in a controller action that subsequently fails, you will have skipped a number. That may be acceptable (invoices are usually fine with gaps) or not (receipts often aren't — store the number only on success, and accept gaps rather than trying to "roll back" a sequence).
  • Deadlocks under load. Two transactions that each create an Order and an Invoice will lock those sequences in insert order. If two code paths lock the same two sequence names in opposite orders you'll get Deadlock found. Consistency trick: always call sequences in a deterministic (alphabetical) order, or reserve all needed numbers up front at the top of the transaction.
  • MySQL REPEATABLE READ and gap locks. If your sequences table has secondary indexes used in the SELECT ... FOR UPDATE, InnoDB may gap-lock rows you didn't intend. Keep the lookup index strictly (name, group) and make it UNIQUE.
  • Config cache. php artisan config:cache will freeze the sequence definitions. If you're adding a new sequence name at runtime (uncommon but happens with tenant onboarding), either bust the config cache or define sequences in the DB, not the config file.
  • Migrations on existing tables. The published migration may create the table without a UNIQUE on (name, group) depending on version — verify before relying on it. Without the unique index, a lost race between two first-inserts for a brand-new group silently creates duplicate counter rows.
  • Testing parallelization. paratest runs suites in separate processes against a shared DB — sequences become a shared mutable resource. Give each worker its own DB (DB_DATABASE=testing_{$token}) rather than trying to lock sequences in tests.
  • Extension points. If you need custom storage (Redis counters, DynamoDB for multi-region), look for a driver/contract binding in the config — the package's whole value is the next($name, $group) contract, so bind your own implementation rather than forking. The HasSequence trait usually calls the facade, so a container rebind is enough to swap backends without touching models.
  • Do not rely on the sequence value being "the" primary key. It's a business identifier. Gaps, resets, and re-issued groups (if you ever reuse a group key) will bite you. Keep the auto-increment id for referential integrity and use the sequence number for display and external communication.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource