UUID vs Auto-Increment IDs
debt(d7/e3/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints list phpstan and laravel-migrations but automated is explicitly 'no'. Index fragmentation from UUID v4 only manifests under load on large tables — silent in development, only visible via performance profiling in production. Exposed sequential IDs in URLs are a code-review concern. No automated rule flags the v4-vs-v7 distinction or CHAR(36) vs BINARY(16) by default.
Closest to 'simple parameterised fix' (e3). The quick_fix is 'use UUID v7 instead of v4' — a targeted swap within the migration/model layer. However, switching storage format from CHAR(36) to BINARY(16) or changing an existing primary key type touches migrations, model casts, and foreign keys across multiple related tables, so it's slightly more than a one-liner but stays within a single component boundary.
Closest to 'persistent productivity tax' (b5). The choice of ID strategy (UUID v4 vs v7, CHAR vs BINARY) applies across web, cli, and queue-worker contexts per applies_to, and it affects every table's schema, every join, every foreign key, and every URL-exposed resource. It's not quite load-bearing across the whole architecture (b7) but it does slow down multiple work streams — migrations, debugging, performance tuning — whenever the wrong choice is made.
Closest to 'serious trap' (t7). The misconception field explicitly states 'UUIDs are always better than auto-increment IDs' — a belief that contradicts the reality of B-tree index fragmentation, storage overhead, and predictability of UUID v1. A competent developer familiar with 'use UUIDs for safety' guidance would naturally reach for UUID v4, which is precisely the wrong choice for database primary keys. This contradicts the common mental model imported from other ecosystems or security guidance.
Also Known As
TL;DR
Explanation
Auto-increment IDs (1, 2, 3…) are simple and efficient but expose business intelligence (order counts, user registration rates) and enable IDOR enumeration attacks. UUIDs (v4: random 128-bit, v7: time-ordered 128-bit) are globally unique across systems and don't leak data. UUID v4 random ordering causes B-tree index fragmentation — UUID v7 (time-ordered, draft RFC) or ULID solve this with sequential generation. In PHP, ramsey/uuid and symfony/uid provide UUID/ULID generation. For distributed systems and event sourcing, UUIDs are essential; for simple internal IDs, auto-increment with proper access controls is often sufficient.
Common Misconception
Why It Matters
Common Mistakes
- Using UUID v4 (random) as a database primary key — causes index fragmentation due to random insertion order; prefer UUID v7 or ULID.
- Storing UUIDs as CHAR(36) instead of BINARY(16) — wastes 20 bytes per row and slows index operations.
- Exposing sequential integer IDs in URLs even when UUIDs are used internally.
- Using UUID v1 (time + MAC) in security contexts — the MAC address component is predictable.
Code Examples
// UUID v4 as primary key — random insert order fragments B-tree index:
CREATE TABLE orders (
id CHAR(36) PRIMARY KEY DEFAULT (UUID()), -- CHAR(36) wastes space, random order
...
);
-- Better: UUID v7 (time-ordered) in BINARY(16):
CREATE TABLE orders (
id BINARY(16) PRIMARY KEY, -- Ordered, compact
...
);
// Use ramsey/uuid — PSR-compliant UUID generation
use Ramsey\Uuid\Uuid;
// UUID v4 — random
$id = Uuid::uuid4()->toString(); // '550e8400-e29b-41d4-a716-446655440000'
// UUID v7 — time-ordered (better for DB index locality, PHP 8.x)
$id = Uuid::uuid7()->toString();
// MySQL — store as BINARY(16) for efficiency
CREATE TABLE orders (id BINARY(16) DEFAULT (UUID_TO_BIN(UUID(), 1)) PRIMARY KEY);
// PHP value object
readonly class OrderId {
public function __construct(public string $value) {
if (!Uuid::isValid($value)) throw new \InvalidArgumentException('Invalid UUID');
}
}