Design a URL Shortener
The most common system design warm-up for a reason: small enough to finish in 40 minutes, with just enough real trade-offs — encoding scheme, cache strategy, redirect semantics — to actually differentiate candidates. Walked through here as a conversation, following the five-phase structure.
Clarify requirements
The prompt lands as one sentence: "Design a service like TinyURL." Before anything else, the questions worth asking out loud:
- Custom aliases — can a user request a specific short code, or is every code system-generated? This changes whether collisions are a system concern or a user-facing error.
- Expiration — do links live forever, or expire after a configurable time? Affects whether codes can be reclaimed and reused.
- Analytics — does the system need to track click counts, referrers, or geography per link? If yes, that is a second write path that must not slow down the redirect itself.
- Read/write ratio — a URL shortener is read-heavy by nature (each link is created once, clicked many times); confirm the interviewer agrees before designing around that assumption.
For this walkthrough: no custom aliases in v1, links do not expire, basic click-count analytics are required, and reads dominate writes by roughly 100:1 — reasonable assumptions, stated explicitly rather than picked silently.
Estimate scale
Assume 100 million new short links created per month and a 100:1 read/write ratio. That is roughly 40 writes/second and 4,000 reads/second on average — and a real design should budget for peak traffic at 3-5x that average, so target roughly 15,000-20,000 reads/second at peak. Each stored record (short code, long URL, creation timestamp, optional expiry) is well under 500 bytes; even at 10 billion links stored over several years, that is under 5 TB — small enough that storage volume is not the bottleneck here. Read throughput and redirect latency are.
High-level design
A write (creating a short link) flows: client → API layer → an encoding service that produces a unique short code → a write to the primary datastore. A read (following a short link) flows: client → API layer → a cache lookup by short code → on a hit, an immediate redirect; on a miss, a datastore lookup, a cache fill, then the redirect. Given the 100:1 read skew from the estimate above, a cache in front of the datastore is not a nice-to-have — it is the single highest-leverage component in this design, and a candidate who adds it unprompted (rather than waiting for the interviewer to ask "how would you handle the read load") is demonstrating that the estimate in phase two actually informed phase three.
Deep dive: the encoding scheme
This is where the interesting decision in this specific problem lives, and it is worth walking through both options out loud rather than jumping straight to one.
Option A — a counter, base62-encoded. Maintain a globally unique, monotonically increasing counter; encode each new value in base62 (a-z, A-Z, 0-9) to get a short, collision-free code. A 6-character base62 code covers over 56 billion values — comfortably enough. The catch is the counter itself: a single centralized counter is a bottleneck and a single point of failure at write volume, so in practice this is implemented as pre-allocated counter ranges handed out to each application server (server A gets IDs 1-1,000,000, server B gets 1,000,001-2,000,000, and so on), trading a small amount of ID-space waste on server restart for no coordination on the write path.
Option B — hash the long URL. Run the long URL through MD5 or SHA-256, take the first 6-8 characters of the base62-encoded digest. No counter, no coordination — but collisions are now possible (two different long URLs producing the same truncated hash) and must be handled explicitly, typically by appending a fixed salt and rehashing on collision, checked against the datastore before the write commits.
Neither is wrong. What an interviewer is grading is whether you can state the trade-off plainly — Option A needs a distributed counter but has zero collision handling; Option B needs zero coordination but requires explicit collision handling on every write — and justify a pick given this problem's actual write volume (40/second average, from the estimate above), which is low enough that either works, so the pick can reasonably come down to implementation simplicity rather than raw performance.
Trade-offs and follow-ups
Two follow-ups an interviewer commonly raises here, and what a strong answer sounds like for each:
- "What if this needs to run in multiple regions?" The counter-based scheme now needs per-region counter ranges (each region owns a disjoint slice of the ID space) to avoid cross-region coordination on every write, at the cost of losing strict global ordering of IDs — a trade-off worth naming explicitly rather than glossing over.
- "How would you add click analytics without slowing down the redirect?" Do not write analytics synchronously on the read path — that couples redirect latency to however long an analytics write takes. Instead, fire an asynchronous event (to a queue) after the redirect is already served, and let a separate consumer aggregate click data. This is also a good moment to volunteer the eventual-consistency trade-off: click counts will lag reality by however long the queue takes to drain, and that is an acceptable cost for analytics that were never on the critical path.
How the bar changes by level
At SDE 2, reaching a correct single-region design with a stated encoding-scheme trade-off and a cache in front of the datastore is a strong 40-minute answer. At Senior, the multi-region follow-up above is expected territory, not a stretch goal. At Staff and Principal, the interview typically does not stay on this problem in isolation at all — it becomes one service inside a larger conversation: how this fits into a broader URL platform with enterprise custom domains, abuse and spam detection at the shortening step, and the organizational question of which team owns the shared counter infrastructure other services might also depend on. The technical core is the same; what is graded shifts from "can you design this service" to "can you reason about this service's place in a much larger system."
See Design a Rate Limiter for a second worked problem where the central trade-off is concurrency and distributed state rather than encoding.
Frequently asked questions
- Base62 encoding or a hash function — which does the interviewer want?
- Neither is "the" answer; the interviewer wants you to name the trade-off. A counter with base62 encoding guarantees no collisions and short codes but requires a coordinated, monotonically increasing counter (a real distributed-systems problem at scale). Hashing the long URL (MD5/SHA, truncated) needs no shared counter but must handle collisions explicitly. Say both, pick one, and state why — that is the actual answer.
- Does the interviewer care about redirect status codes (301 vs 302)?
- It is a small detail that a well-prepared candidate volunteers and a good interviewer notices: 301 (permanent) lets browsers and CDNs cache the redirect, cutting load on your service but making a link's destination hard to change later; 302 (temporary) keeps every redirect hitting your service, which costs more but keeps redirects mutable and click-trackable. It rarely decides the interview, but naming it unprompted is exactly the kind of unprompted trade-off that signals seniority.
- How is this problem different at Staff level versus SDE 2?
- At SDE 2, a correct single-region design with a sensible encoding scheme and a cache is a strong answer. At Staff, the same starting point is expected in the first ten minutes, and the interview really begins with follow-ups this page's walkthrough treats as central: multi-region writes without a single point of coordination, analytics ingestion without slowing the redirect path, and what changes if this is one endpoint inside a much larger URL platform with abuse detection and enterprise custom domains.
Related
Try this exact problem, scored
Free account. Practice this problem against an AI interviewer calibrated to your target level.