You are in a system design interview. The interviewer says: “Design a URL shortener like bit.ly.” What do you do? You do not jump into databases and load balancers. You start by understanding what the thing actually is, what it needs to do, and how big it needs to get. This is the complete walkthrough — from the first question to the final follow-up.
What is a URL shortener? Think of it like a valet parking ticket. You hand the attendant your car (a long, complicated URL), and they hand you a small paper ticket (a short code like aB3x9Q). When you come back, you hand them the ticket, and they return your car. The ticket is meaningless on its own — it is just a reference to something stored behind the counter.
You have used these hundreds of times. When someone tweets a link and it looks like bit.ly/3kR9xP, that is a short URL. When you click it, the service looks up 3kR9xP in its database, finds the original long URL, and redirects your browser there. The whole thing takes under 50 milliseconds.
Real-world examples: bit.ly (the OG), TinyURL (one of the first, launched in 2002), t.co (Twitter/X’s built-in shortener), goo.gl (Google’s, now retired), and Short.io (custom domain shortening for businesses).
Why do companies build these? Three reasons. First, analytics — every click is tracked, so you know where your users come from, what device they use, and what time they clicked. Second, branding — nyti.ms/arts looks better in a newspaper than https://www.nytimes.com/2026/04/22/arts/design/museum-exhibition-review.html. Third, link management — if the destination URL changes, you update it in one place and every short link automatically points to the new address.
Before designing anything, you need to know what you are building. In an interview, you ask the interviewer clarifying questions. Here is what a typical conversation looks like:
“Should users need to log in?” — Probably not for basic shortening, but yes for managing links and viewing analytics.
“Do we need custom aliases?” — Yes, brands want nyti.ms/arts not nyti.ms/7xK2pQ.
“Do links expire?” — Optional, but useful for temporary campaigns.
“Are we focused on reads or writes?” — Mostly reads. People create a link once but share it thousands of times.
These are the things the system must do:
my-brand/sale)These are the quality constraints:
For this interview, we are explicitly NOT building: user authentication, payment processing, link preview generation, QR code creation, or bulk import/export. Mentioning these shows the interviewer you understand scope boundaries.
This is where you show the interviewer you can do back-of-the-napkin math. You state your assumptions clearly, then calculate.
Assumptions:
Calculations:
These numbers tell us something important: reads dominate by 100x. That means our system design should be read-optimized. Cache everything. The write path only handles ~200 QPS — that is trivial.
Now we define the contract between clients and servers. A clean REST API with four endpoints covers everything:
POST /api/shorten → Create a short URL
GET /{shortCode} → Redirect to original
DELETE /api/links/{id} → Delete a link
GET /api/links/{id}/stats → View analytics
The first two are the critical path. The second two are management endpoints. Let us walk through each one.
Why 301 vs 307? A 301 (Moved Permanently) tells the browser to cache the redirect forever. Great for performance, bad if the URL changes. A 307 (Temporary Redirect) always hits the server. Use 307 for links with expiration dates, 301 for permanent links.
Why not use query parameters? GET /redirect?code=aB3x9Q works, but GET /aB3x9Q is shorter, more shareable, and easier to print on physical media. The short code goes in the URL path.
Idempotency: POST /api/shorten should return the existing short code if the same URL was already shortened, rather than creating a duplicate. This is not strictly idempotent, but it is what users expect.
We need two tables: one for URL mappings and one for analytics. The URL table is the critical path — every redirect queries it. The analytics table is write-heavy but read-only for dashboard queries.
SQL (PostgreSQL): Better if you need joins (users + their links), complex queries (analytics aggregations), and strong consistency. Easier to reason about. Sharding is harder at scale.
NoSQL (DynamoDB/Cassandra): Better if you need horizontal scale, simple key-value lookups (short_code → URL), and flexible schemas. Built-in replication. Harder to do aggregations.
The interview answer: Start with SQL for the URL table (you need strong consistency — two requests for the same custom alias must not both succeed). Use a separate time-series database or NoSQL table for analytics (write-heavy, append-only, eventual consistency is fine).
The most important index is UNIQUE INDEX (short_code). Every redirect query does SELECT original_url FROM urls WHERE short_code = ?. Without this index, every redirect is a full table scan on a table with billions of rows. With it, the lookup is O(1).
This is the heart of the interview. How do you turn a long URL into a short, unique string? There are three approaches, and the interviewer expects you to compare them.
Give each URL a sequential numeric ID (1, 2, 3, …) and encode it in Base62. ID 12345 becomes 3d7 in Base62. Simple, no collisions, but predictable — anyone can enumerate all URLs by incrementing the counter.
Run the URL through MD5 or SHA-256, take the first 6-7 characters. Same URL always produces the same code (deterministic). But collisions are possible (two different URLs might hash to the same prefix). You handle collisions by checking the database and appending characters if needed.
Pick 6-7 random characters from [0-9a-zA-Z]. No coordination needed. Truly unpredictable. But you must check the database for uniqueness on every write, and under high load you will get collisions that require retries.
For an interview, Option 1 (Base62) is the strongest answer. It guarantees no collisions, is easy to implement, and the predictability problem is solved by using a distributed ID generator (Snowflake IDs) instead of a single auto-increment counter. Mention the other two as alternatives you considered and explain why you rejected them.
Base62 is the most common encoding scheme for URL shorteners. The character set is 0-9 (10 digits), a-z (26 lowercase), A-Z (26 uppercase) — 62 characters total. Why 62? Because these are all “URL-safe” characters that do not need percent-encoding in a URL path.
The math is compelling:
Even at bit.ly’s scale (500M new URLs/year), 7 characters gives you 3.5 trillion IDs — enough for 7,000 years at current rates. Eight characters is overkill.
Encoding is just repeated division. Take the numeric ID, divide by 62, the remainder gives you the rightmost character. Repeat with the quotient until you reach zero. Read the remainders in reverse order.
Decoding is the reverse: for each character, multiply the running total by 62 and add the character’s index value.
A single auto-increment counter is a bottleneck. Instead, use a distributed ID generator like Twitter’s Snowflake: a 64-bit number where the first bits are a timestamp, the middle bits are a machine ID, and the last bits are a sequence number. This gives you unique, time-ordered IDs across multiple servers without coordination.
If you choose the hash approach, collisions are a real concern. The birthday paradox tells us that collisions become likely much sooner than you would expect. With 7 characters (62^7 = 3.5 trillion possible codes) and 500 million URLs, the collision probability is still low — but not zero.
Rehash: If the short code is already taken, hash the URL with a salt (append a counter) and try again. hash(url + "1"), hash(url + "2"), etc. until you find an unused code.
Append: If aB3x9Q is taken, try aB3x9Q1, aB3x9Q2, etc. Simple but makes the code longer.
Pre-check: Before committing, query the database. If the code exists, generate a new one. This is the most common approach but adds latency to the write path.
With the auto-increment + Base62 approach, collisions are impossible — each ID is unique by definition. This is why most production systems use it. The hash approach sounds elegant but adds complexity for no real benefit.
Here is the full architecture. A user creates a short URL, and later someone clicks it. These are two completely different paths through the system, and they have different performance requirements.
POST /api/shorten with the long URLSET short:aB3x9Q <original_url>)This path is not performance-critical. 200 QPS is trivial. Latency of 100-200ms is acceptable.
GET /aB3x9Q301 Location: <original_url> immediatelyThis is the happy path. It takes under 10ms because the CDN serves it from an edge node near the user. The origin server never sees the request.
GET /aB3x9QSELECT original_url FROM urls WHERE short_code = 'aB3x9Q'301 to client, CDN caches the responseThis path is slower (30-50ms) but should be rare for popular links. The 80/20 rule applies: 20% of URLs get 80% of traffic, so caching the top URLs eliminates most database queries.
The basic design works for millions of URLs. Here is what changes when you need to handle billions.
The single highest-impact optimization. Cache the redirect response at the CDN level. Most URL shorteners see extreme skew — a viral link might get millions of clicks in an hour while most links get single-digit clicks. Cache the top 20% of URLs and you eliminate 80% of database reads.
Cache strategy: cache-aside pattern. On a redirect, check Redis first. If miss, query the database and populate the cache with a TTL (e.g., 1 hour). The CDN sits in front of everything and caches the actual 301 response.
When a single database cannot hold all your data, shard by short_code. Use consistent hashing to distribute URLs across N shards. Each shard holds roughly 1/N of the data. Add more shards as you grow.
The redirect query only needs one shard: hash(short_code) % N → shard_id. This means every redirect touches exactly one shard, keeping latency low.
The shorten endpoint is the most abusable. Someone could write a script to create millions of short URLs. Use a token bucket rate limiter: 10 requests per minute per IP for anonymous users, 100 per minute for authenticated users. Implement with Redis counters.
Writing analytics on every redirect adds latency and database load. Instead, fire-and-forget: the API server publishes a click event to a message queue (Kafka, SQS), then returns the redirect immediately. Worker processes consume events from the queue, parse IP addresses, look up countries, and write to a time-series database. The redirect path never waits for analytics.
For 99.99%+ availability, deploy to 3+ regions with active-active replication. Use geoDNS to route users to the nearest region. The trade-off is eventual consistency — a URL created in US-East might take a few seconds to appear in EU-West.
Every design decision has a trade-off. Good interviewers will poke at your choices. Here is how to handle the common follow-ups.
With 62^7 = 3.5 trillion possibilities, random guessing is impractical. But if you use sequential IDs, an attacker could enumerate URLs by incrementing. Solutions: add random padding to the Base62 string, use Snowflake IDs (which include machine ID bits), or add a secret salt before encoding.
Short URLs hide the destination. Someone could shorten a phishing page and share it widely. Solutions: scan URLs against threat intelligence databases (Google Safe Browsing API), require users to verify ownership for custom domains, and show a preview page for unknown short URLs.
Every redirect returns an error page. At bit.ly’s scale, this means millions of broken links. Solutions: multi-region active-active deployment, database replicas in each region, automated failover, and health checks that remove unhealthy nodes from the load balancer.
Custom aliases are harder than random codes because you need to check for availability AND prevent namespace conflicts. Solutions: reserve popular prefixes, implement a “taken” check with eventual consistency, and allow users to claim namespaces (like brand.*).
| Decision | Choice | Alternative | Why |
|---|---|---|---|
| Short code generation | Auto-increment + Base62 | Hash + truncate | No collisions, simpler logic |
| Database | Sharded PostgreSQL | DynamoDB | Strong consistency for aliases |
| Cache | Redis + CDN | Memcached | Data persistence, CDN for edge |
| Analytics | Async message queue | Synchronous write | Zero impact on redirect latency |
| ID generation | Snowflake IDs | Single auto-increment | No single point of failure |
| Redirect | 301 permanent | 307 temporary | Better caching at CDN |
Before walking into an interview, make sure you can answer all of these without looking:
short_code needs a UNIQUE index?