vimatech/laravel-document-numbering
Laravel package for generating sequential document numbers for your models. Configure custom formats, prefixes, suffixes, date placeholders, padding, and reset periods, with database-backed, concurrency-safe numbering for invoices, orders, and other records.
What the package claims to be. A narrow, single-purpose Laravel package: allocate the next number in a sequence, safely, for a named document type (invoice, quote, credit note), scoped per tenant/company/series, with a configurable string pattern and a period-based reset (annual/monthly). The differentiating claim is row-lock-based concurrency safety with gap-free numbering by default — meaning the number is allocated inside the caller's transaction and only persists if that transaction commits.
Architecture fit. This is a classic "small library, high blast radius" component. The architectural surface is small — a service provider, a config file, one or two migrations (a counters table and possibly an allocations/audit table), a facade or injectable generator service, and a token-based pattern renderer. That means it composes cleanly with any Laravel app regardless of whether the domain layer is fat models, actions, or DDD-lite. The important architectural question is not where it plugs in but when it is called: the package can only be correct if allocation happens at the moment a document is finalized and persisted, not when a draft is created. If a draft burns a number and is later deleted, "gap-free" is a lie. Confirm that the intended usage pattern is "allocate inside the same DB transaction that writes the invoice row."
Integration feasibility. High on paper, low on evidence. Publishing config, running a migration, and calling a generator is a half-day of work for one engineer. The friction is entirely in the non-happy paths: backfilling existing sequences so the first generated number doesn't collide with history; seeding per-scope counters to the correct high-water mark; reconciling documents that already exist with ad-hoc numbering; and deciding what "period reset" means at a timezone boundary (a midnight UTC reset vs. a Europe/Madrid fiscal-year reset is a different legal answer).
Technical risk. Concentrated in four places:
SELECT ... FOR UPDATE on a single counter row serializes every allocation for that scope. That is correct and cheap at 10–100 documents/day and becomes a lock-contention and deadlock surface at thousands/hour, especially if the caller holds the lock across a slow transaction. Throughput ceiling must be measured, not assumed.Key questions to put to the maintainer (or answer by reading the source).
SKIP LOCKED used or plain FOR UPDATE?Absent satisfactory answers to the first, second, and sixth, this should not touch production invoices.
Stack fit. Native Laravel, no exotic dependencies, no queue or external service required — which is the right shape. It slots into the existing Eloquent/transaction model and does not require a new piece of infrastructure. That is its main strategic advantage over "build a numbering microservice" or "use Redis INCR," both of which introduce a second consistency domain that must be reconciled with the database on every failure.
Build vs. buy. Be explicit that this is a ~200–400 line component. The purchase price of adoption is low, but so is the price of a clean-room internal implementation, and the internal version carries no maintainer risk and can be tuned to exactly the compliance regime you operate under. Recommendation: vendor the package (fork into an internal namespace) rather than depend on it via Composer. You get the MIT-licensed head start, you own the patch cadence, and you are not exposed to abandonment. If the team prefers a Composer dependency, pin it exactly and require a passing concurrency test suite in your own CI before merge.
Migration path. Assume the organization already has documents numbered by something weaker (auto-increment IDs, or date('Y').'-'.count() which is subtly racy). The migration is:
Compatibility. Verify the Laravel/PHP support matrix against your actual runtime before anything else; a package released "recently" may still exclude the version you run. Confirm the migration is additive and reversible, and that it does not assume a specific table prefix or connection. If you run multi-tenant with per-tenant databases, confirm the counters table is created in each tenant connection, not just the central one — this is the single most common integration defect in packages of this class.
Sequencing (suggested 6–8 week path for a mid-size team).
| Week | Deliverable |
|---|---|
| 1 | Source read-through; written answers to the key questions above; go/no-go on fork-vs-depend |
| 2 | Spike branch: install, migrate, generate 10k numbers under 20-way concurrency; record p50/p99 latency and deadlock rate |
| 3 | Finance/legal review of the pattern, reset rule, and gap-free claim for each operating jurisdiction |
| 4 | Backfill script + reconciliation report; seed counters in staging |
| 5–6 | Shadow-mode run on quotes and credit notes; alarm on any gap or duplicate |
| 7 | Invoice cutover behind a feature flag with per-scope kill switch |
| 8 | Decommission legacy numbering path; document the new runbook |
Maintenance. Low code churn, but non-zero ownership. A numbering component changes rarely and breaks catastrophically. Budget for: a quarterly dependency review, an owner who understands the transaction semantics (not just the API), and a regression test that asserts monotonicity and gaplessness per scope under concurrency. If forked, add a short design note explaining why the lock is held and what would break if it were released early — future engineers will otherwise "optimize" it into a correctness bug.
Support and escalation. Incorrect invoice numbers are a finance and legal incident, not an engineering incident. Define the escalation path now: who can stop issuance, who can void and reissue, who notifies the tax authority if required. Duplicate numbers are worse than gaps in most regimes; both are reportable in some. The package should never be the only thing standing between a bug and a customer-facing invoice — wrap it in a thin internal service with validation (monotonic per scope, format matches regex, period matches document date) that fails loudly before persistence.
Scaling. Two regimes. Below roughly a few hundred documents per hour per scope, a row lock is fine and you should not over-engineer. Above that, expect contention, lock-wait timeouts, and deadlocks between concurrent transactions that each touch the counter and other rows in different orders. Mitigations, in order of preference: (a) keep the locked transaction as short as possible — allocate as the last write before commit; (b) shard scopes (per-entity or per-series counters rather than one global counter); (c) move to a dedicated allocation service with block reservation — accepting gaps, which requires a compliance decision. Also note that FOR UPDATE forces the read to the primary; read/write splitting will silently not help here.
Failure modes to instrument.
Ramp-up. The API is small enough that a mid-level Laravel engineer is productive in under a day, and the config file is largely self-documenting. The conceptual ramp-up is the real cost: transaction isolation, row locking, and why "gap-free" and "high throughput" fight each other. Plan for a 30-minute walkthrough with the team plus a one-page ADR, and make sure at least two engineers — not one — can debug a numbering incident.
Overall recommendation. Treat as pilot-grade, not production-grade for invoices at adoption time. It solves a real, painful, frequently-botched problem with the right mechanism, and MIT licensing makes the code yours if you want it. But zero stars, zero dependents, a single maintainer, and a compliance-critical domain mean the decision is "we are adopting this code and its correctness", not "we are depending on a vendor." Fork it, test it under concurrency yourself, validate the gap-free guarantee with finance, pilot it on quotes and credit notes, and only then let it touch invoices — and even then, behind a unique constraint and a kill switch.
How can I help you explore Laravel packages today?