Design a URL shortener. That is the prompt, and it already contains your first design decision. The service is a key-value lookup, and you will assume reads outnumber writes 100 to 1. With that assumption, most of the design work goes to two things: keeping the short codes that get clicked the most in cache, and generating unique short IDs without collisions. The redirect path, the database, and the write path follow from those two.
Scope the core to two features and a few constraints
The requirements phase is where you decide what is in scope. Commit to the core, and set the rest aside.
The functional core is two features:
- A user submits a long URL and receives a short one.
- Someone opens the short URL and is redirected to the long one.
Everything else is optional, and you commit to it only if the interviewer raises it or you have time after the core works. Custom aliases, expiration dates, and click analytics are the three that usually get named. Authentication, account management, and spam detection get one sentence each and stay out of the design.
The non-functional constraints carry the design:
- Short codes are unique, and each code maps to exactly one long URL.
- Redirects take milliseconds, and the service stays available when a component fails.
- The system holds on the order of a billion links, and reads outnumber writes 100 to 1.
Say the scope out loud: "I'll design for the two core features and these constraints. Analytics and auth are out unless you want them in." That gets the interviewer's confirmation and decides what the next 40 minutes contain. The cues matter because each optional feature changes the design. Analytics in scope adds an async event path behind the redirect. Expiration in scope adds a TTL rule and a cleanup job. "The same long URL should always return the same short code" changes the ID scheme from a counter to a hash.
The numbers are 1B links, 12 writes per second, and 1,157 reads per second
State the assumptions, then do the arithmetic out loud. The interviewer re-runs the math to check you, and the visible division is part of the answer.
Assumptions: 1B links stored, 1M new links per day, a 100 to 1 read to write ratio, and a 10x peak factor.
- Writes: 1,000,000 / 86,400 ≈ 12 writes per second on average, about 120 at peak.
- Reads: 100M redirects per day, so 100,000,000 / 86,400 ≈ 1,157 reads per second on average, about 11,600 at peak.
- Storage: a row holds the short code (about 8 bytes), the long URL (about 100 bytes), and creation and expiration timestamps (about 16 bytes), plus metadata and index overhead. Round to about 500 bytes per row. 1B rows × 500 bytes = 500 GB, which a single database holds at this scale with room to spare.
- Cache working set: assume 20% of the day's 1M new links take most of the clicks. 200,000 keys × 500 bytes ≈ 100 MB, which fits in a small in-memory cache.
- Bandwidth: a redirect response is about 500 bytes, so 100M per day ≈ 50 GB per day, and about 6 MB per second at peak.
Reading the numbers: writes are negligible, storage is trivial, and everything is reads. The design must keep the read path fast at peak and keep the IDs unique under the write rate. If the interviewer changes an assumption (10B links, a different ratio), you redo the arithmetic.
The high-level design is one write path and one read path
Draw one box per responsibility and keep it at five boxes, six when analytics is in scope.
- Load balancer in front of the application servers.
- Write service: POST /shorten validates the URL, asks the ID generator for the next code, inserts the row, and returns the short URL.
- Read service: GET /{code} looks up the code and returns a redirect.
- Cache (an in-memory store such as Redis) between the read service and the database.
- Database: the source of truth for code to long URL, keyed on the short code with a UNIQUE constraint.
- Queue: click events land here when analytics is in scope. A worker consumes them, and the redirect never waits on them.
The creation flow is validate, generate, insert, return. The redirect flow is cache lookup, on a miss a database lookup, cache set with a TTL at or below the link's remaining lifetime, then a 302 to the long URL. An expired link returns 410, and an unknown code returns 404.
Here is the full system, with the write path and the read path together:
The redirect never blocks on the queue.
Default to a counter for short IDs; keep the hash-based option ready
This is the decision the interview turns on, so name the tradeoff explicitly. Two approaches are standard.
Hash-based. Hash the long URL (say SHA-256), take part of the digest, encode it in base62, and keep the first 7 or 8 characters. It is deterministic, so the same long URL always produces the same code, which is a feature if the prompt wants deduplication and a bug if a user shortens the same page twice for two campaigns. Collisions are possible. With n codes in use out of |S| possible, the chance the next one collides is about n divided by |S|. At 1B used codes out of 62^8, about 218 trillion, that is about 1 in 218,000, rare but not zero, so the insert path needs a UNIQUE check and a retry with a fresh hash.
Counter-based. Take the next value from a counter and encode it in base62. 1B in base62 (0-9A-Za-z) is "15ftgG", six characters. Codes grow to seven only past 62^6, about 56.8B links, and seven characters hold 62^7, about 3.5T. Collisions are impossible by construction, as long as the counter store issues each value once, and the code decodes back to its ID. Two costs: the codes are predictable, so anyone can walk the sequence, and the counter needs one store that hands out IDs so two writers never get the same number.
The verdict: default to the counter. Uniqueness is a correctness requirement, and the counter makes it structural while the coordination cost at this write rate is negligible. At 120 peak writes per second with batches of 1,000, the write services fetch the counter about 0.12 times per second. The coordination cost is small, and the insert path never needs a collision retry.
Switch to the hash-based option when one of these is in the prompt: the same long URL must always return the same short code (dedup is a feature); the system must stay stateless, with many regions writing without a shared counter; or the codes must be hard to guess, in which case you salt the hash with a secret (HMAC) and accept the collision retry.
If predictability is a concern (the codes encode something private, or the interviewer pushes on enumeration), say the fix. Apply a secret transformation before encoding, such as XOR with a key or a shuffled mapping, or accept enumeration because a short URL is public by design. Name which one you are picking and why.
Use 302 redirects so you can still expire and count links
301 and 302 are the two redirect codes the choice turns on, and their semantics decide the choice. A 301 Moved Permanently says the resource has been assigned a new permanent URI, and future references should use the one in the Location header. A 302 Found says the resource is temporarily located there, and the client keeps using the original URI (RFC 9110, HTTP Semantics, sections 15.4.2 and 15.4.3).
For a shortener, 301 is the wrong code. It tells the client the move is permanent, and a 301 response is cacheable, so a later visit may skip your service and go straight to the destination (RFC 9110, HTTP Semantics, section 15.4.2). If the destination can change, the link can expire, or you count clicks, a 301 breaks all three: the redirect goes to the stale target, your 410 never arrives, and the click never reaches you.
The verdict: return 302. Every visit keeps flowing through your service, so expiration, target changes, and click counting all keep working. Expired link: 410 Gone, the code for a resource that is no longer available. Unknown code: 404.
The cue in the interview: if the interviewer says the destination never changes and repeat visits should cost you nothing, a 301 is defensible. Say the tradeoff (you give up control and analytics) and confirm the requirement before you commit.
Cache the read path until the database sees about 116 reads per second
The read flow is cache first. On a miss, the read service queries the database, sets the cache entry with a TTL at or below the link's remaining lifetime, and returns the 302.
The redirect flow, with its three outcomes:
A cache entry that outlives the link serves an expired link as if it were live, and the 410 you promised never happens. Setting the TTL to the remaining lifetime makes the cache evict itself on expiration.
The math: if the cache catches 90% of the 1,157 average reads per second, the database sees about 116. At peak, 90% of 11,600 leaves about 1,160. A single indexed database handles both rates at this scale, and the lookup is a primary-key read. Managed options fit the shape. DynamoDB is a serverless, fully managed NoSQL database with key-value and document data models and single-digit millisecond performance at any scale (DynamoDB developer guide, Introduction).
The hot link is the one code that gets most of the traffic. It is a single cache entry, so the memory cache absorbs it. If one code reaches millions of reads per second, move that redirect to the edge. A CDN such as CloudFront, which AWS describes as a content delivery network service that distributes content quickly and reliably, can serve the cached redirect close to the user, so most hits on the hot code are served at the edge. You pay CDN cost and complexity for the latency win, and that trade is worth it only for the hot code.
Invalidation stays simple because a mapping is never edited. The URL does not change after creation. Delete the cache key when the link is deleted, and let the TTL handle the rest.
The counter is the write bottleneck; batch it and split it by region
The write path is small, 12 writes per second on average, but the counter is a shared resource, and every insert needs the next ID from it.
Batching. Each write service instance asks the counter store for a block, say 1,000 IDs, and hands them out locally until the block runs out. At 120 peak writes per second that is about 0.12 requests per second to the counter store. Even a 100x write spike is 12 requests per second, which a single in-memory store (Redis, or a managed service such as Amazon ElastiCache, which offers Redis OSS compatibility) serves without effort.
Scaling the instances. Split the read and write services and scale each to its own traffic. The read service scales for the 100 to 1 ratio, and the write service scales for 12 writes per second. The split exists because of that ratio, so name the ratio as the reason for the split.
Multi-region. Give each region a disjoint range of IDs, the first billion in region A and the next billion in region B, so regions do not coordinate for IDs. Reads are served by per-region caches.
Failure. If the counter store dies, writers pause until it recovers. The UNIQUE constraint on the code is the safety net. A lost or repeated value fails the insert, and the insert retries with a fresh value, so the counter needs uniqueness, not continuity.
The weak phrasing to avoid is naming a tool without a mechanism. "Use a distributed ID generator such as Snowflake" with no answer to how two regions avoid the same ID leaves the uniqueness question open. Name the mechanism (disjoint ranges, a coordinating store, batching) or do not name the tool.
What a weak answer sounds like
Before your interview, run your own answer against these and note which one you would fail.
- Stacking services before numbers. "Kafka, Redis, Postgres, edge cache" before the read to write ratio is stated. The interviewer is listening for the ratio driving the placement.
- A hash with no collision story. Proposing the hash-based ID and stopping, with no word on how a collision is detected or retried, and no notice that a deterministic hash means the same URL always gives the same code. You must say which one that is, a feature or a bug.
- 301 with expiration in scope. Browsers cache the permanent redirect, so your 410 never arrives and the stale target keeps serving.
- The counter with no mechanism. "A distributed ID generator" named, but no answer to how two writers or two regions avoid the same ID.
- Invisible arithmetic. Stating "about 1,200 reads per second" without the division. A slip of one zero (100M per day is about 1,157 reads per second on average and about 11,600 at peak; one zero off the average is 116) tells the interviewer the design was memorized.
- Ending at the high-level design. The interviewer's next question is the scaling story, reads first, then the counter. Arriving there with nothing leaves the interview with no depth to show.
What changes the design
State the limits of the default. These are the conditions you would confirm with the interviewer before committing.
- Analytics in scope. Click events go to a queue behind the redirect. The redirect never waits on analytics, and the click counter lives in the worker, not the read path.
- Expiration in scope. The TTL stays at or below the remaining lifetime, a cleanup job removes expired rows, and lookup returns 410.
- Multi-region. Disjoint counter ranges, per-region caches, and a decision about whether writes go to the home region or the nearest one.
- Write-heavy variant. If the interviewer flips the ratio to 100 writes per read, the cache story inverts. The counter and insert path become the bottleneck, the read path needs nothing special, and the design effort moves to replication and partitioning the writes.
- Deterministic codes required. The counter is out. You take the hash with a salted HMAC and put the collision retry into the insert contract.
Rehearse the prompt out loud against the weak answer list
The goal is to reproduce this reasoning live, and a memorized architecture falls apart the moment the interviewer changes one assumption. Rehearse once, out loud, in under 30 minutes: state the scope, run the arithmetic with the divisions visible, commit to the counter in one sentence that names the tradeoff, and say what would flip it to a hash. Then run the weak answer list against yourself.
The CramHQ System Design Interview Readiness path starts with a free assessment. No credit card required. It gives you a Readiness Report, your top decision gaps, and a targeted repair preview before you unlock the full path, which is $49.99 for 12 months of access and includes Decision Drills across 16 decision areas and the System Design Readiness Exam.
