A URL shortener has two workloads: create a durable mapping once, then look it up many times. The decisions that matter are how a code becomes unique and how a popular link avoids sending every reader to the database.
This is one strong approach, and others work too. Scale figures come from the question's brief; the extra estimates below are assumptions for the exercise.
Turn the brief into requirements
Customers create short links, optionally choosing an alias or expiry. Anyone can open a link without signing in. Click analytics and account management stay outside this design.
The brief says about 100 million people open short links each day, the service holds around a billion links, and links are opened about 100 times as often as they are created. Treating that as 100 million opens a day gives about 1,160 opens and 12 creates a second on average. The average hides the hard case, where one campaign gets a large share of all clicks. Assume a peak of 100,000 opens a second spread over a handful of viral links.
Aim for redirects well under 100 ms inside the service, not counting the destination site's response. During a database outage, keep redirects working even if some mappings are briefly stale. Codes must stay unique when creation runs on many service instances. Ask whether expiry is strict; this approach checks the stored expiry on every redirect, including cache hits.
Model one record per link
A link record contains code, destination_url, created_at, optional expires_at, and the creator's account ID. The code is the primary key. Creation takes the account from a verified session instead of trusting an owner ID in the body. Opening a link requires no account.
Treat each creation as a distinct link. Two customers shortening the same destination may want different expiry times and aliases, and a hash of the destination alone cannot tell them apart.
The conceptual API is:
POST /urlsaccepts a destination, optional alias and expiry, and returns201with the short URL.GET /{code}returns a redirect with aLocationheader,404for an unknown code, or410for a known expired link.- Alias conflicts return
409; malformed destinations and invalid expiry values return a validation error.
Reserve service paths such as api before allocating aliases. Validate destination schemes against an allowlist such as HTTP and HTTPS. Creating a link does not require fetching the destination. Adding link previews later would bring in a separate, security-sensitive fetching workload.
Let the database enforce unique codes
Use a durable sequence to allocate numeric IDs, then encode them in base62. Seven base62 characters give about 3.5 trillion possible strings. Encoding shortens the visible ID. It does not encrypt it or stop enumeration.
Generated codes and custom aliases share one namespace. If an alias already occupies a generated code, retry with the next allocated ID. A competing insert must fail without overwriting the existing mapping. A PostgreSQL unique constraint enforces this, so the second of two racing inserts fails.
Random codes with a unique constraint and a retry are equally valid. They avoid depending on a sequence, but you have to choose a length and an acceptable collision rate. With either generator, "check whether the code exists, then insert" cannot be the only protection, because another writer can insert between the two operations.
Start with one database handling creation. If sequence round trips become a cost, give each instance its own disjoint range of numeric IDs. Gaps after an instance crashes are harmless; handing the same range to two live instances is not. Range allocation must itself be durable and atomic.
Separate creation from the redirect path
The creation service validates the request, allocates a code, inserts the mapping, and returns only after the database commits. It may warm the cache after that commit. A failed cache write does not undo a link that was created.
A redirect instance looks for the mapping in its small local cache, then the shared cache, then the database. On a miss it fills the caches with the complete mapping, including expiry. It compares expires_at with the current time before returning the destination, and no cache entry lives past the link's expiry.
This example returns 302 with explicit cache headers. HTTP distinguishes temporary and permanent redirects, and RFC 9110 makes a 301 heuristically cacheable. A temporary status code is not a full caching policy on its own. A permanent short link that never changes could use a permanent redirect and longer browser caching. An expiring campaign link needs tighter control over how long browsers and proxies can reuse the response.
Handle a viral code as a hot key
Adding database shards does little for one code read 100,000 times a second. Cache the popular mapping near the redirect instances, spread shared-cache reads across replicas where that helps, and use request coalescing so that one instance makes one lookup when many requests miss at once.
When the shared cache fails, unrestricted fallback reads can overwhelm the database. Keep serving local cached mappings, cap database concurrency, and reject excess uncached requests instead of letting every redirect instance open new database work. Cache "not found" results for unknown codes briefly to cut repeated misses, and rate limit clients probing random codes.
There is a race when a code is cached as "not found" just before it is created. Clear that entry when the link commits, or send a miss right after creation to the primary. A read replica may not have the new link yet. If you call this eventually consistent, also say what the customer sees when they open their new link straight away.
Shard by code when one database is not enough
For a rough capacity exercise, assume 500 bytes per link. A billion mappings then need about 500 GB before indexes, replication and backups. Actual URL lengths and database overhead decide the real capacity plan.
Partition by a stable hash of the code once one database cannot hold the data or handle the reads and writes. A routing layer maps hash ranges to shards, and each shard has replicas and one write authority. Avoid taking the code modulo the current machine count, which would move most links whenever that count changes.
Each custom alias still belongs to exactly one shard, which enforces its uniqueness. If the allocator also has to span regions, allocate globally disjoint ranges, or use random codes with a single authority for each code. If two isolated regions can each accept the same alias, the uniqueness guarantee is gone.
What fails, and what the design gives up
Database failover may pause creation. Cached redirects keep working until their entries expire; uncached links need a healthy replica or a restored primary. A cached expired record must still return 410, never the old destination.
This design treats destination mappings as immutable. Supporting edits or abuse takedowns adds invalidation and a policy question: how long may an old destination stay usable? Short TTLs bound ordinary staleness, and urgent blocks may need a separately distributed denylist. You cannot promise immediate global revocation while letting disconnected caches answer indefinitely.
Trace a redirect during a traffic spike
Try the URL shortener question in Grind. Say your requirements, build the design from memory, and talk through one creation and one redirect. Explain why your uniqueness rule holds with two concurrent writers, then say what you would change first during a viral campaign.


