fab2s/souuid
fab2s/souuid generates sortable, time-ordered UUIDs for Laravel and PHP. These UUID-compatible IDs sort chronologically, improving database index locality and making them practical, efficient primary keys.
Architecture fit. fab2s/souuid is a narrow, single-purpose library: it generates time-ordered UUIDs so that primary keys remain sortable and index-friendly while retaining UUID semantics (no central sequence, client-side generation). For a Laravel codebase it slots in at the persistence layer — model key generation and the corresponding migration column type. It is framework-agnostic, so treat "Laravel integration" as something we build (a trait or base-model override of newUniqueId() / uniqueIds()), not something we inherit. Architecturally it is a leaf dependency with no service provider, config surface, or middleware, which keeps the blast radius small.
Integration feasibility. Technically trivial: composer require, one trait, one column type, one cast. The real feasibility question is not "can we" but "should we," because Laravel ≥9 already ships Illuminate\Support\Str::orderedUuid() and the HasUuids trait, which covers the same job. Our evaluation must therefore be comparative: does souuid offer a materially better ordering guarantee, a more compact/binary representation, higher generation throughput, or a documented monotonicity contract that core Laravel lacks? If not, this is a redundant dependency.
Technical risk.
random_bytes().Key questions for the maintainer/spike.
Ramsey\Uuid and Laravel's uuid validation rule?BINARY(16) storage, and a matching Eloquent cast?gmp, bcmath)?Stack fit. Good on the write path, neutral elsewhere. It fits any Laravel app using Eloquent with UUID primary keys, and is most valuable where we insert at high volume into a clustered-index database (MySQL InnoDB, SQL Server) and want to avoid both auto-increment hotspotting and random-UUID page-split churn. It does not fit workloads requiring strictly opaque, non-guessable identifiers — a time-prefixed ID leaks generation time and rough insert order, which matters for public-facing resource URLs.
Migration path. Do not retrofit existing tables. Phase it:
souuid against Str::orderedUuid() and AUTO_INCREMENT on a representative table — insert throughput, index size, page-split rate, and select-by-PK latency. This is the go/no-go gate.uuid/char(36) column.Compatibility. Two friction points to resolve before writing code: (a) foreign keys and route-model binding — if we go binary, every FK column and every implicit binding must match, which is a broad, easy-to-miss change; (b) JSON/API surface — binary keys must be hex/base62-encoded at the boundary or clients break. Prefer the string form unless the benchmark shows a decisive binary win.
Sequencing. Spike → decide string vs binary → land a shared HasSouuid trait in the base model → adopt on one new low-risk table → observe for a release cycle → broaden. Keep the trait as the single seam so swapping back to Str::orderedUuid() is a one-file revert.
Maintenance. Ongoing cost is near zero in steady state — no config, no migrations of its own, no runtime services. The recurring cost is upgrade diligence: with one maintainer and low visibility, we must pin the version, review every bump by diffing the generator rather than trusting the changelog, and budget for the possibility that we take ownership of the code. A 200-line generator is forkable in an afternoon; treat that as the exit plan and note it in the ADR.
Support. Community-only. No vendor SLA, no paid tier, no security advisory feed. Our own tests must therefore assert the properties we depend on (sortability, uniqueness under concurrency, RFC round-trip, monotonicity across ticks) rather than trusting upstream coverage. That test suite is the real support contract.
Scaling. This is where the package earns its keep: ordered IDs preserve index locality, so write amplification stays flat as the table grows, unlike random UUIDv4 where index size and page splits degrade throughput non-linearly. It removes the single-writer ceiling of auto-increment, which matters for multi-region or sharded writes. The tradeoff is a small, bounded ordering window (IDs from the same tick interleave), which is harmless for PK purposes.
Failure modes to watch.
whereIn misses. Enforce with a schema assertion test.ORDER BY id.Ramp-up. Low. A mid-level engineer can adopt it in under a day given the trait pattern. The knowledge that actually needs transferring is not the API but the rationale: why we chose ordered UUIDs over auto-increment or random UUIDs, what the column-type decision implies for FKs and APIs, and how to read the fragmentation metric. Capture that in the ADR alongside the spike results, or the next team will re-litigate the choice — or worse, quietly mix ID strategies across tables.
Bottom line for the decision record: low technical risk, low integration cost, non-trivial strategic risk from single-maintainer status, and a value proposition that overlaps Laravel core. Proceed only if the spike shows a measurable win over Str::orderedUuid(); otherwise adopt core and skip the dependency.
How can I help you explore Laravel packages today?