Definition
- Choose UUID – When identifiers must be minted independently by many actors without a central coordinator: distributed systems, sharded databases, offline-first clients, or public identifiers that must not be guessable.
- Avoid random UUID – For heavy-write relational tables (B-tree fragmentation), tight storage (16 bytes vs 4-8), when chronological sorting is needed, or simple single-node applications.
- UUIDv7 middle path – Combines coordination-free uniqueness with approximate time ordering and insert locality, making it the right default when UUID properties are genuinely needed.
- Public vs internal – Never expose an internal auto-increment key in a public API; use a separate opaque identifier for public URLs.
At the National Bank of India, a UUID is like letting every branch, ATM, and mobile app mint its own globally-unique reference number on the spot without phoning head office for the next number. That is essential when branches are offline or when you later merge their records, and it is great for public reference numbers that customers should not be able to guess or count. But for the central high-speed transaction ledger that runs on one cluster and must stay compact and time-ordered, a simple running counter from the central sequence is tidier, smaller, and faster. You pick the tool by whether coordination-free generation is actually needed.
Understanding the Concepts
When should you choose a UUID?
You choose a UUID when generation must be decentralized. In a distributed system many services create records at once, and asking a central sequence for the next number becomes a bottleneck and a single point of failure.
-- National Bank of India: distributed payment services
-- Each service mints its own UUID, no central sequence needed
CREATE TABLE payment_requests (
request_id UUID PRIMARY KEY, -- minted by any service
account_id VARCHAR(20) NOT NULL,
amount DECIMAL(15,2) NOT NULL,
status VARCHAR(10) NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
// com.bank.service.PaymentRequestService
package com.bank.service;
import java.math.BigDecimal;
import java.sql.*;
import java.util.UUID;
public class PaymentRequestService {
/**
* Each service instance mints a UUID locally.
* No central sequence, no coordination, no bottleneck.
*/
public UUID createRequest(
Connection conn, String accountId, BigDecimal amount)
throws SQLException {
UUID requestId = UUID.randomUUID(); // UUIDv4
// Better: use UUIDv7 for time ordering
String sql = """
INSERT INTO payment_requests
(request_id, account_id, amount)
VALUES (?, ?, ?)
""";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, requestId);
ps.setString(2, accountId);
ps.setBigDecimal(3, amount);
ps.executeUpdate();
}
return requestId;
}
}
Other use cases for UUID:
- Database sharding – Each shard creates non-colliding ids without talking to the others.
- Offline clients – Mobile banking apps create records while disconnected and sync later without renumbering.
- Non-guessable public URLs – A random UUID in the URL prevents enumeration attacks.
-- Non-guessable public URL
-- Bad: /api/orders/1001 (attacker tries 1002, 1003...)
-- Good: /api/orders/550e8400-e29b-41d4-a716-446655440000
When should you avoid a UUID?
Avoid a random UUID when its costs outweigh its benefits.
-- 1. Heavy-write relational table: random UUIDs fragment the B-tree
CREATE TABLE audit_log (
log_id BIGSERIAL PRIMARY KEY, -- auto-increment: dense, fast
-- NOT: log_id UUID DEFAULT gen_random_uuid() -- fragmented
event_type VARCHAR(50),
event_data TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. Tight storage: 16 bytes vs 8 bytes per row
-- With 200M rows:
-- BIGINT PK: 200M x 8 bytes = 1.6 GB
-- UUID PK: 200M x 16 bytes = 3.2 GB (double)
-- CHAR(36): 200M x 36 bytes = 7.2 GB (quadruple)
-- Plus: InnoDB embeds PK in every secondary index
-- 3. Chronological sorting needed
SELECT * FROM transactions ORDER BY txn_id;
-- Auto-increment: sorted by creation time (free)
-- UUIDv4: sorted by random bytes (meaningless)
-- 4. Simple single-node app: no need for distributed generation
-- A database sequence is simpler, smaller, and sufficient
What is the balanced middle path?
When you genuinely need UUID properties but also care about write performance, use time-ordered UUIDv7.
-- UUIDv7: coordination-free + time-ordered + insert locality
CREATE TABLE payments (
payment_id UUID PRIMARY KEY, -- UUIDv7 generated in app
account_id VARCHAR(20) NOT NULL,
amount DECIMAL(15,2) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- UUIDv7 values are approximately monotonically increasing
-- Inserts land near the rightmost page, like auto-increment
-- But every service can mint them independently
| Feature | Auto-increment | UUIDv4 | UUIDv7 |
|---|---|---|---|
| Coordination | Central sequence | None | None |
| Insert locality | Perfect | Random (fragmented) | Near-sequential |
| Size | 4-8 bytes | 16 bytes | 16 bytes |
| Time ordering | Yes | No | Approximate |
| Guessable | Yes | No | Partially (timestamp) |
| Best for | Single-node, write-heavy | Distributed, unguessable | Distributed, write-heavy |
How should you handle public vs internal identifiers?
Never expose an internal auto-increment key directly in a public API.
-- National Bank of India: separate internal and public identifiers
CREATE TABLE orders (
order_id BIGSERIAL PRIMARY KEY, -- internal: compact, fast
public_ref UUID NOT NULL DEFAULT gen_random_uuid(), -- public: unguessable
account_id VARCHAR(20) NOT NULL,
amount DECIMAL(15,2) NOT NULL
);
CREATE UNIQUE INDEX ux_orders_public_ref ON orders(public_ref);
-- Internal queries use the fast BIGINT PK
SELECT * FROM orders WHERE order_id = 12345;
-- Public API uses the unguessable UUID
SELECT * FROM orders WHERE public_ref = '550e8400-e29b-...';
What is the decision framework?
Ask three questions to decide.
1. Do multiple actors need to mint IDs without coordination?
YES -> UUID (prefer UUIDv7)
NO -> auto-increment / sequence
2. Must the ID be non-guessable in a public URL?
YES -> UUID for the public identifier
NO -> auto-increment is fine internally
3. Is the table write-heavy with many secondary indexes?
YES + needs UUID -> UUIDv7 (not UUIDv4)
YES + no UUID needed -> auto-increment
What are common mistakes?
-- Mistake 1: UUID everywhere regardless of need
-- A simple lookup table does not need a UUID
CREATE TABLE branches (
branch_id UUID PRIMARY KEY -- overkill for 50 branches
);
-- Better:
CREATE TABLE branches (
branch_code VARCHAR(10) PRIMARY KEY -- simple, meaningful
);
-- Mistake 2: Storing UUID as CHAR(36) instead of native UUID
CREATE TABLE payments (
id CHAR(36) PRIMARY KEY -- 36 bytes, wastes space
);
-- Better:
CREATE TABLE payments (
id UUID PRIMARY KEY -- 16 bytes, native type
);
-- Mistake 3: Using UUID as an auth token
-- A UUID is an identifier, not a secret
-- Use cryptographically secure tokens for authentication
What should a senior engineer know?
- A UUID is an identifier, not a security token. It must not replace proper authorization checks.
- In InnoDB, the 16-byte UUID is embedded in every secondary index. With 5 secondary indexes, that is 80 extra bytes per row across all indexes.
- After switching key strategies, measure index size and insert throughput to confirm the improvement.
- Consider using UUIDv7 as the default for new tables that need distributed generation, and auto-increment for everything else.
What are common interview follow-up questions?
- What is the UUIDv7 binary structure and how does it give time ordering?
- Why does random UUIDv4 cause index fragmentation in InnoDB?
- How does a Snowflake ID compare to UUIDv7?
- When is auto-increment strictly better than any UUID variant?
Visual Overview
┌─────────────────────────────────────────────────┐ │ UUID Decision Framework │ ├─────────────────────────────────────────────────┤ │ │ │ Need coordination-free IDs? │ │ │ │ │ ├── YES ──► Need time ordering? │ │ │ │ │ │ │ ├── YES ──► UUIDv7 │ │ │ │ │ │ │ └── NO ──► UUIDv4 │ │ │ (or UUIDv7 anyway) │ │ │ │ │ └── NO ──► Auto-increment / sequence │ │ │ │ Need non-guessable public ID? │ │ │ │ │ ├── YES ──► Add UUID column for public API │ │ │ Keep BIGINT PK internally │ │ │ │ │ └── NO ──► BIGINT PK is sufficient │ │ │ │ Storage comparison (200M rows): │ │ BIGINT: 200M x 8 bytes = 1.6 GB │ │ UUID: 200M x 16 bytes = 3.2 GB │ │ CHAR(36): 200M x 36 bytes = 7.2 GB │ └─────────────────────────────────────────────────┘
Key Takeaway
The decision to use a UUID key comes down to whether you genuinely need decentralized, coordination-free identifier generation, or unguessable public identifiers. You choose a UUID when many actors must mint ids independently – distributed systems where a central sequence would be a bottleneck, sharded databases where each shard must avoid collisions without talking to the others, and offline or mobile clients that create records while disconnected and sync later without renumbering. You also choose a UUID for public identifiers that must not be guessable or enumerable, because an auto-increment id in a URL lets an attacker simply count upward and enumerate every record.
You avoid a random UUID when those properties are not needed and their costs hurt. Heavy-write relational tables suffer from random-UUID index fragmentation and slower inserts. Tight storage or bandwidth makes the 16-byte UUID expensive versus a 4 or 8 byte integer, especially in InnoDB where it is embedded in every secondary index. Workloads that need chronologically sortable ids cannot use a random UUID because it carries no time. And simple single-node apps are better served by a plain database sequence, which is smaller and simpler. When you do need UUID properties but still care about write performance, the balanced choice is a time-ordered UUIDv7, which keeps coordination-free uniqueness while restoring insert locality and approximate ordering.
At the National Bank of India Rajesh applied this per table rather than globally. The mobile app creates payment requests offline and syncs later, and the public URLs for those requests must not be guessable, so the request id is a UUID minted on the device. But the internal ledger is central, write-heavy, and queried by time, so its primary key is a compact auto-increment sequence that stays dense and sorts chronologically. Each table got the key that matched its actual needs.
The nuance Pankaj always adds is that he never exposes an internal auto-increment key directly in a public API even when it is used internally – he pairs it with a separate opaque public identifier – and he reminds the team that a UUID is an identifier, not a security token, so it never replaces proper authorization checks.
Remember one sentence: use a UUID when many actors must mint ids with no coordination or when ids must be unguessable, and avoid random UUIDs for compact, write-heavy, sortable single-node tables.
---