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.
Architecture fit
The package targets a well-known gap in Laravel: the framework ships auto-incrementing primary keys but nothing for human-facing sequential identifiers (invoice numbers, order numbers, ticket IDs) that must be gapless-ish, scoped, and formatted. The described design — a dedicated sequence table, a transaction wrapping SELECT ... FOR UPDATE on a row keyed by a grouping column, an increment, and a formatter — is the conventional and correct approach for this problem. It fits Laravel's package conventions cleanly: service provider, publishable config and migration, and a model trait for booted()-time assignment. That means low coupling to application code and an easy exit path. Grouping (per tenant, per document type, per year) and formatting (prefix, padding) are the two features that actually drive adoption in this category, so the feature surface is aimed at real use rather than novelty. The main architectural caveat is that the lock is a serialization point: every writer in a group queues behind one row. That is fine for orders and invoices; it is wrong for high-volume event or log identifiers.
Integration feasibility
Integration cost is low on paper: composer require, publish migration/config, add a trait or call a facade inside an existing transaction. No queue worker, no external service, no new infrastructure. The hard part is not installation, it is semantics. Two questions determine whether it drops in or requires rework: (1) does the package open its own transaction, or does it enlist in the caller's? and (2) what happens to the counter when the caller's transaction rolls back? If the counter increments in its own committed transaction, you get gaps on rollback (usually acceptable, often contractually not). If it shares the caller's transaction, gaps disappear but the row lock is held for the entire duration of the caller's work, which materially raises contention and deadlock exposure. Neither answer is universally right, but the choice must be known before design sign-off.
Technical risk
FOR UPDATE (gap locks vs. row locks, REPEATABLE READ vs. READ COMMITTED), and SQLite has no FOR UPDATE at all — it degrades to a database-wide write lock. Dev-on-SQLite / prod-on-MySQL is the classic way this class of package fails in production while passing CI.MAX(existing)+1. Get that wrong and you emit duplicates against live data — a data-integrity incident, not a bug.Key questions
Stack fit
Good fit for a standard Laravel monolith or modular monolith on MySQL 8+ or PostgreSQL, PHP 8.1+, where the sequential IDs are user-visible document numbers. Poor fit for multi-region active-active writes (the lock is single-primary), for event/telemetry ingestion at volume, and for any environment where SQLite must remain a supported production driver.
Migration path
MAX(existing sequence) + 1 inside the same migration or a one-off artisan command, and make it idempotent. Do this before the trait is enabled on any model.Compatibility
Pin the package version, and pin the Laravel/PHP matrix in your own CI so an upstream constraint bump cannot silently lock you out of a framework upgrade. Verify behavior on the exact MySQL/PostgreSQL version and isolation level you run in production. If you operate multiple database engines across environments (common in teams with SQLite local dev), add a test that runs the sequence path against the production engine in CI.
Sequencing
Spike (1–2 days) → concurrency proof on target engine (1 day) → single-entity pilot (1 sprint) → seed/backfill runbook written and rehearsed → broader rollout → decommission any ad-hoc MAX(id)+1 or count()+1 code paths, which are the actual bug this package replaces.
Maintenance
Single maintainer, tiny user base, MIT license. The license is the mitigation: the package is small and forkable, and the core logic is a few dozen lines around a locking read. Budget for the possibility that you own this code within 12–24 months. Do not adopt it in a module where you cannot afford to maintain a fork.
Support
GitHub issues only; no commercial backing, no SLA. Response latency should be assumed to be days-to-weeks. For a dependency on the invoice-numbering path, that means your team is the first responder. Acceptable for an internal tool; needs explicit sign-off if it touches revenue-recognized documents.
Scaling
Throughput is bounded by serialization on the counter row per group. Levers, in order of preference: (a) increase group cardinality so concurrent writers hit distinct rows (tenant, document type, period); (b) shorten the caller's transaction so the lock is held briefly; (c) batch-allocate blocks of numbers per request — fast, but converts a rare gap risk into a routine one; (d) if you genuinely need six-figure writes/sec, this is the wrong pattern — use a DB-native sequence or a distributed ID generator and treat the human-readable number as a derived, separately-assigned attribute.
Failure modes to instrument
Ramp-up
Low. The public API is small (a trait, a facade, a config file) and a mid-level Laravel developer is productive in half a day. The part that requires senior review is the concurrency contract: isolation levels, lock duration, transaction boundaries, and the gap-on-rollback decision. Write those four decisions into the ADR before integration, not after the first duplicate invoice.
How can I help you explore Laravel packages today?