Junior 5 min · March 05, 2026

URL Shortener Design — Why Auto-Increment Kills at Scale

Auto-increment locks dropped throughput from 1000/sec to 0 mid-campaign.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • A URL shortener maps a long URL to a short code and redirects clients via HTTP 301/302
  • Hashing strategies: base62 encoding of unique IDs vs hash-then-collision-check
  • Redirects are cheap: aim for <10ms total latency at P99
  • Caching must handle hot keys: a single viral link can generate millions of requests per minute
  • Biggest mistake: using a single database counter to generate IDs — single point of failure and bottleneck
Plain-English First

Imagine every long book title in a library had a short call number stamped on its spine — '792.4 SHA' instead of 'The Complete Works of Shakespeare, Volume III'. A URL shortener does exactly that for web addresses. You hand it a massive, ugly link and it gives you back a tiny code — like a coat-check ticket — that it keeps pinned to the original address. When someone shows up with the ticket, the system finds the coat (the real URL) and sends them straight to it.

Every time you see a link like 'bit.ly/3xQp9R' in a tweet, a QR code, or an SMS campaign, a surprisingly complex distributed system is working behind the scenes. URL shorteners process billions of redirects per day, and companies like Bitly, TinyURL, and Twitter's t.co have quietly become some of the most read-heavy services on the internet — often handling tens of thousands of requests per second at peak. Getting this design wrong at scale doesn't just mean slow pages; it means broken marketing campaigns, dead QR codes on printed packaging, and lost revenue that can't be recovered.

The core problem sounds trivial: map a long string to a short one and reverse the mapping on demand. But that simplicity is deceptive. You need to generate short codes that are globally unique, store hundreds of millions of mappings efficiently, serve redirects in under 10 milliseconds, handle hot keys (a single viral link getting millions of hits per minute), expire links, support custom aliases, and survive datacenter failures — all simultaneously.

By the end of this article you'll have a production-grade mental model for a URL shortener: you'll know exactly how to generate collision-free short codes, why you should never put a counter in a single database row, how to layer caching to absorb viral traffic spikes, and what the interview panel is really testing when they ask you this question.

What is Design URL Shortener?

A URL shortener is a service that takes a long URL and returns a shorter, unique alias that redirects clients to the original URL. The typical flow: a client submits a long URL via an API, the service generates a short code (e.g., 'abc123'), stores the mapping in a database with optional metadata (creation time, expiration, owner), and returns the full short URL (e.g., 'https://short.url/abc123'). When a client requests that short URL, the service looks up the code, retrieves the original URL, and issues an HTTP redirect (301 for permanent, 302 for temporary). Analytics (clicks, referrers, timestamps) are usually logged asynchronously.

Production Insight
Redirects are cheap, but each one hits the DB if caching is missed. A P99 latency of 5ms is achievable with Redis in front.
Database fallback kills throughput — every missed cache is an order of magnitude slower.
Rule: cache aggressively and survive a cache miss without cascading failures.
Key Takeaway
URL shortener = write-once, read-often system.
Cache the mapping, not the redirect.
The short code is the primary key — design for O(1) lookup.

Short Code Generation — Hashing vs Counter-Based IDs

There are two dominant strategies for generating short codes. The first is hash-based: take the long URL, compute a hash (e.g., MD5 or SHA-256), take the first N characters (usually 6–8), check for collisions, and if one exists add a salt or retry with a different prefix. The second is ID-based: use a globally unique integer (from a distributed ID generator) and encode it in base62 (0-9, a-z, A-Z) to produce a compact alphanumeric string. Base62 encoding of a 64-bit integer yields up to 11 characters — typical shorteners use 6–7 characters, which gives 62^6 ≈ 56 billion combinations.

ID-based systems are simpler for uniqueness (just generate a unique ID) but require a reliable ID generator. Hash-based systems must handle collisions and require longer codes for the same collision probability. Most production systems prefer ID-based with base62 encoding because the code space is deterministic and collision-checking is trivial.

io/thecodeforge/shortener/Base62Encoder.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package io.thecodeforge.shortener;

import java.util.ArrayList;
import java.util.List;

public class Base62Encoder {
    private static final String BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final int BASE = 62;

    public static String encode(long id) {
        if (id == 0) return String.valueOf(BASE62.charAt(0));
        List<Character> chars = new ArrayList<>();
        while (id > 0) {
            chars.add(BASE62.charAt((int) (id % BASE)));
            id /= BASE;
        }
        StringBuilder sb = new StringBuilder(chars.size());
        for (int i = chars.size() - 1; i >= 0; i--) {
            sb.append(chars.get(i));
        }
        return sb.toString();
    }

    public static long decode(String shortCode) {
        long id = 0;
        for (char c : shortCode.toCharArray()) {
            int digit = BASE62.indexOf(c);
            if (digit == -1) throw new IllegalArgumentException("Invalid character in code: " + c);
            id = id * BASE + digit;
        }
        return id;
    }
}
Avoid MD5 for Short Codes
MD5 collision probability is low for short prefixes but predictable — an attacker can craft colliding URLs. Use SHA-256 with a salt or, better, an ID-based system. Never use MD5 for security-sensitive applications.
Production Insight
Base62 encoding of a Snowflake ID gives a short, URL-safe, and collision-free code.
Hash-based systems need collision handling: if hash collides, append a salt and rehash until unique.
Rule: for production, prefer ID-based generation. It's simpler to reason about and debug.
Key Takeaway
Base62 encoding of a distributed ID is the standard.
Hash-based maps URL→code deterministically but has collision overhead.
Rule: don't mix auto-generated and custom aliases in the same code space without a prefix.
Choosing Between Hash and ID-Based
IfYou need deterministic mapping from long URL to short code (same URL always gets same code)
UseUse hash-based with a fixed-length truncation. Accept collision risks and handle retries.
IfYou need a small code space (e.g., 6 chars) and don't care about deterministic mapping
UseUse ID-based with base62. Easier to scale and guarantee uniqueness.
IfYou need to support custom aliases (user picks the code)
UseUse ID-based for auto-generated, but store custom aliases in a separate namespace or table.

Database Schema & Write Path

The core database stores the mapping from short code to long URL. The schema is simple: primary key on short_code, columns for original_url, created_at, expiration_at, owner_id (optional). But at scale, the write path must be designed for high throughput during creation bursts. Write operations are not the bottleneck (traffic is ~99% reads), but if you use a single database for ID generation, you get into trouble. Instead, decouple ID generation from the database: generate IDs in an application tier using Snowflake-like algorithms (or pre-allocated segments). Then insert the mapping asynchronously? No — inserts must be synchronous for consistency, but they can be batched and buffered.

For reads, index on short_code is critical. Use a covering index (include original_url) to avoid disk access. Partition the table by short_code prefix to distribute writes. Use a read replica for analytics queries, but always route redirect lookups to the primary or cache first.

schema/create_links_table.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- TheCodeForge schema for URL shortener
CREATE TABLE links (
    short_code VARCHAR(10) NOT NULL PRIMARY KEY,
    original_url VARCHAR(2048) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP NULL,
    owner_id BIGINT NOT NULL DEFAULT 0,
    click_count BIGINT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Index for fast lookups (covering index)
CREATE INDEX idx_short_code ON links (short_code) INCLUDE (original_url, expires_at);

-- For analytics queries (non-urgent)
CREATE INDEX idx_owner_created ON links (owner_id, created_at);
Schema Design Tip
Keep the short_code as VARCHAR but validate it's alphanumeric. Use utf8mb4 to support emojis in original URLs (some users will paste them). Avoid storing the full URL twice — normalise if you need to deduplicate.
Production Insight
Index on short_code is the most critical index. A covering index avoids a separate data file lookup.
Write scalability is not about inserts per se, but about ID generation. Pre-allocate ID blocks to workers.
Rule: partition the links table by the first two characters of short_code to spread writes across shards at extreme scale.
Key Takeaway
Schema is simple — the complexity is in ID generation and caching.
Covering index on short_code turns lookup into an index-only scan.
Rule: always query the primary for redirects; use replicas for reporting only.

Caching Layer — Survival Guide for Viral Traffic

A single viral link can generate millions of requests per minute. Without caching, your database will melt. The caching architecture needs at least two tiers: L1 (in-memory cache per application instance) and L2 (distributed cache like Redis or Memcached). L1 stores the hottest keys (recently accessed short codes) and evicts using LRU. L2 stores a larger set of mappings with a longer TTL.

Cache-aside pattern: on a redirect request, check L1 → if miss, check L2 → if miss, fetch from DB and populate both caches. Set a TTL of 24 hours for L2, but proactive invalidation when a link is deleted or expires. For read-heavy workloads, consider a write-through cache: on creation, immediately write to cache and DB asynchronously (with a queue). That way the first read is already fast.

Hot key problem: when a single short code gets 100k requests per second, Redis can become a hotspot. Solutions: local L1 caching (each app server caches the hot key), or use Redis with replicas and client-side sharding to distribute reads.

io/thecodeforge/shortener/RedirectCache.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package io.thecodeforge.shortener;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import redis.clients.jedis.Jedis;

import java.util.concurrent.TimeUnit;

public class RedirectCache {
    private final Cache<String, String> l1 = CacheBuilder.newBuilder()
            .maximumSize(100_000)
            .expireAfterWrite(1, TimeUnit.MINUTES)
            .recordStats()
            .build();

    private final Jedis jedis;  // L2 connection pool
    private static final String PREFIX = "url:";
    private static final int L2_TTL_SECONDS = 86400;

    public String getOriginalUrl(String shortCode) {
        // L1 lookup
        String url = l1.getIfPresent(shortCode);
        if (url != null) return url;

        // L2 lookup
        url = jedis.get(PREFIX + shortCode);
        if (url != null) {
            l1.put(shortCode, url);
            return url;
        }

        // DB fallback happens outside this method
        return null;
    }

    public void put(String shortCode, String originalUrl) {
        l1.put(shortCode, originalUrl);
        jedis.setex(PREFIX + shortCode, L2_TTL_SECONDS, originalUrl);
    }
}
Tiered Caching: L1 vs L2
  • L1: in-memory per microservice instance. Fastest. Limited size. Evict aggressively.
  • L2: Redis cluster. Shared across all instances. Tolerates higher latency but still sub-millisecond.
  • Cache miss penalty: L1 miss → Redis hit ~1ms. Redis miss → DB hit ~10ms. Every miss hurts throughput.
  • Proactive populate: write-through cache on URL creation prevents the first request from hitting the DB.
Production Insight
Hot keys can overwhelm a single Redis node. Use L1 caching to absorb the top 10 hot keys locally.
Cache stampede occurs when many requests miss cache simultaneously — use early re-compute (e.g., set a probabilistic TTL)
Rule: monitor cache hit rate per short code. If a code has >10% cache misses, promote it to L1 proactively.
Key Takeaway
Tiered caching is non-negotiable for viral traffic.
Hot keys need local L1 caching to keep Redis from melting.
Rule: always populate cache on write, not just on read.

Redirect Mechanics — HTTP Status and Performance

When a client requests a short URL, the server must respond with an HTTP redirect. Two status codes matter: 301 (Moved Permanently) and 302 (Found). 301 tells the browser to cache the redirect permanently — subsequent requests go directly to the long URL without hitting the shortener. This is great for performance but breaks analytics if you want to count every click (because cached browsers don't hit your service). 302 tells the browser not to cache — every request hits the shortener, enabling click tracking.

Most services use 302 by default for dynamic analytics, and offer 301 as an option for permanent links. The redirect response also includes the Location header. The server must set CORS headers if the short URL is embedded in an iframe.

Performance: the entire redirect (from request to response) should complete in under 10ms at P99. This includes DNS resolution on the client side, TCP connection, TLS handshake, and the server processing. The server side is typically <1ms with caching. Server-side improvements: keepalive connections, HTTP/2 multiplexing, and edge caching (CDN).

http/redirect-example.httpHTTP
1
2
3
4
5
6
7
8
9
10
HTTP/1.1 302 Found
Location: https://www.example.com/long-article-url
Cache-Control: no-cache, no-store, must-revalidate
Content-Length: 0
Access-Control-Allow-Origin: *

# Alternatively, a 301 redirect for permanent links:
HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/long-article-url
Cache-Control: public, max-age=31536000, immutable
302 Redirects Can Bust Caches
Using 302 for all links means every click goes to your origin server. At scale, that's expensive. Consider using 301 for 'permanent' links (by default after a few hours of existence) and 302 for new links. Or use 307/308 for clients that require preserving the HTTP method.
Production Insight
Use 302 for analytics-required links; 301 for permanent ones to offload traffic.
Edge caching with a CDN (CloudFront, Cloudflare) can serve redirects from the edge — reduces latency to <2ms globally.
Rule: set a short TTL (like 1 hour) on CDN cache for 302, so you can still update links quickly.
Key Takeaway
Redirect status determines browser caching behaviour.
301 saves bandwidth but loses click data.
Rule: use 302 by default and switch to 301 after the link is 'stable'.

Expiration, Custom Aliases, and Analytics

Real URL shorteners support link expiration (e.g., for temporary campaign links) and custom aliases (user picks a meaningful short code). Expiration is implemented by storing an expires_at column and checking during redirect lookup. If the current time exceeds expires_at, return 410 Gone or redirect to a fallback page. Custom aliases require a separate validation: they must be unique globally and not conflict with auto-generated codes. A common approach is to reserve a prefix for auto-generated codes (e.g., starting with a digit) and allow custom aliases to start with a letter. Or use two separate tables.

Analytics: every redirect should asynchronously log the click event (time, referrer, user-agent, IP) to a high-throughput queue (Kafka, Kinesis). A separate consumer processes the stream to update click counts and generate reports. The click count on the links table should be denormalised for quick display but must be updated asynchronously to avoid write contention. Use eventual consistency: the consumer updates the count in the DB via upsert.

sql/expired-link-check.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
-- TheCodeForge: check expiration during redirect lookup
SELECT original_url,
       CASE WHEN expires_at IS NOT NULL AND expires_at < CURRENT_TIMESTAMP THEN 1 ELSE 0 END AS expired
FROM links
WHERE short_code = ?
LIMIT 1;

-- Then in application code:
if (row.isExpired()) {
    response.setStatus(410); // Gone
    return;
}
redirectUserTo(row.getOriginalUrl());
Custom Alias Validation
Validate that custom aliases are at least 4 characters, alphanumeric only, and not in a reserved list (like 'api', 'login'). Use a Bloom filter to quickly reject common unwanted aliases.
Production Insight
Expiration checks add ~1ms to the redirect path. Index on expires_at can slow writes. A better approach: set a short TTL in cache and let the cache expire naturally — but then the link stays accessible in cache after DB expiration. So you must invalidate cache on expiration.
Analytics pipelines must be idempotent: a retry should not double-count clicks. Use a unique event ID per click.
Rule: separate analytics writes from the redirect path entirely to avoid impacting latency.
Key Takeaway
Expiration requires either DB query on every redirect or proactive cache invalidation.
Custom aliases need namespace separation from auto-generated codes.
Rule: analytics should be eventually consistent and never block the redirect.
● Production incidentPOST-MORTEMseverity: high

The Single-Table Counter That Took Down a Shortener

Symptom
Short code generation slowed from 1000/sec to 0 during a marketing campaign. The entire shortener stopped accepting new links.
Assumption
An auto-increment column in a single writer database is simple and works fine for moderate traffic.
Root cause
All writes went to one RDBMS primary. Under high write load, the InnoDB auto-increment lock (table-level for INSERT) caused contention. When the database crashed, all new link creation was blocked.
Fix
Switched to a distributed ID generator using Snowflake-like IDs (64-bit, with timestamp + worker ID + sequence) and segregated write traffic across multiple worker nodes. Each worker generates IDs without coordination.
Key lesson
  • Never rely on a single database auto-increment for ID generation at scale — it's a write bottleneck and a single point of failure.
  • Use distributed ID generators or pre-allocated ID ranges to eliminate contention.
  • Always design for write scalability even if you expect read-heavy workload — shortener creation traffic spikes during campaigns.
Production debug guideSymptom → Action guide for common URL shortener issues4 entries
Symptom · 01
Short code returns 404 even though it exists in the database
Fix
Check cache layer (Redis/Memcached) for stale entries. Invalidate the key and verify DB. Also check if the link has expired (deadline in DB).
Symptom · 02
Redirect takes >50ms consistently
Fix
Check reverse proxy (Nginx) caching rules. Ensure 301 redirects are set for permanent links so browsers cache them. Profile Redis lookup time — high latency could indicate a hot key causing resource contention.
Symptom · 03
Custom alias already taken but user didn't set it
Fix
Check whether a separate namespace for custom aliases is colliding with auto-generated codes. Use a distinct prefix or separate database for custom aliases.
Symptom · 04
Analytics data lost for specific short codes
Fix
Verify that the async analytics pipeline (Kafka + streaming job) is not dropping messages. Check for backpressure in Kafka consumer groups. Ensure idempotent insertion to prevent duplicates.
★ Quick Debug: URL Shortener Redirect IssuesUse these commands to diagnose the most common production redirect problems.
Short code not found in DB but exists in cache
Immediate action
Check cache consistency — likely a stale entry after deletion or expiration.
Commands
curl -v http://shortener.io/shortCode # Check redirect headers and status
redis-cli GET shortener:shortCode # Check if key exists
Fix now
Purge the cache key and ensure DB lookup triggers cache refresh.
High latency on redirect+
Immediate action
Identify whether latency is from cache layer or DB fallback.
Commands
curl -w '@curl-format.txt' -o /dev/null -s http://shortener.io/abc123 # Measure time_namelookup, time_connect, time_starttransfer
redis-cli --latency -h <redis-host> # Check Redis latency
Fix now
Add local L1 cache using a small in-memory cache (e.g., Guava or Caffeine) to absorb hot key traffic.
Custom alias not working for a new link+
Immediate action
Verify that the custom alias is not already in use and that the request reached the service.
Commands
docker compose logs creation-service | grep customAlias # Check logs for creation attempt
SELECT * FROM links WHERE short_code = 'customAlias' # DB query to confirm existence
Fix now
Return explicit 409 Conflict for duplicate custom aliases and force a different alias.
Hash-Based vs ID-Based Short Code Generation
PropertyHash-BasedID-Based
Deterministic URL→CodeYesNo (same URL gets different codes)
Collision freeCollisions possible, need retryAlways unique (ID guarantees)
Code lengthFixed (e.g., 7 chars)Variable, depends on ID size (6-10 chars)
ID generation bottleneckNone (hash is deterministic)Requires distributed ID generator (Snowflake)
Supports custom aliasesEasy (prefix hash with alias)Need separate namespace

Key takeaways

1
URL shorteners are read-heavy systems; cache aggressively in two tiers (L1 + L2).
2
ID-based short code generation with base62 encoding avoids collisions and scales horizontally.
3
Use 302 for analytics tracking, 301 for permanent links to reduce origin load.
4
Analytics must be asynchronous and idempotent
never block the redirect path.
5
Design for viral traffic
hot keys need local caching, CDN offload, and throttled creation APIs.

Common mistakes to avoid

3 patterns
×

Using a single database auto-increment for short code IDs

Symptom
Write throughput caps at ~10k/s on a single MySQL node. Viral campaign creates request queue and eventually timeouts. Service becomes unavailable for new link creation.
Fix
Replace with distributed ID generator (Snowflake algorithm) or pre-allocate ID ranges to worker nodes.
×

Not caching redirect lookups

Symptom
Database load spikes 100x during viral link, causing slow queries and cascading timeouts. Latency jumps from 5ms to 500ms.
Fix
Implement at least a Redis cache layer with write-through on creation. Use local L1 cache for extreme hot keys.
×

Using 302 for all links (no CDN edge caching)

Symptom
Every request hits origin servers, increasing infrastructure cost and latency. Can't scale globally without expensive regional deployments.
Fix
Use 301 for permanent links after a grace period. Serve 301 redirects from a CDN edge cache with long TTL. Reserve 302 for temporary/analytics-only links.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How would you generate a unique short code for every URL in a distribute...
Q02SENIOR
Explain how you would handle a viral link generating 1 million requests ...
Q03JUNIOR
What's the difference between 301 and 302 redirects, and why would you c...
Q01 of 03SENIOR

How would you generate a unique short code for every URL in a distributed system?

ANSWER
Use a distributed ID generator like Snowflake (64-bit: timestamp + worker ID + sequence). Encode the ID in base62 to produce a short alphanumeric code. This guarantees uniqueness without coordination. Alternatively, use a hash of the long URL (e.g., SHA-256 truncated) with collision retry, but that's less efficient and non-deterministic if you want same URL → same code.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is a URL shortener in simple terms?
02
Why not just use a hash of the URL as the short code?
03
How do I handle custom aliases? Do they conflict with auto-generated codes?
04
What's the best caching strategy for a URL shortener?
🔥

That's Real World. Mark it forged?

5 min read · try the examples if you haven't

Previous
Software Architecture Explained: Patterns, Trade-offs and Real Decisions
1 / 17 · Real World
Next
Design Twitter Feed