· SWE Editorial · System Design · 6 min read
Design a URL Shortener: Architecture Diagram and Data Flow
A layer-by-layer breakdown of how a request actually moves through a URL shortener — CDN, load balancer, app servers, cache, and database — with separate diagrams for the write path and the read path.
Beyond the Box-and-Arrow Sketch
Most candidates can draw a URL shortener architecture in five boxes. Far fewer can explain, precisely, what happens to a single request as it crosses each layer, or why the write path and read path deliberately diverge. That precision is what a staff-level interviewer is probing for once the high-level shape is on the whiteboard. This article assumes you’ve already nailed down requirements (see the companion interview guide) and goes one layer deeper: the actual data flow, layer by layer.
The Full Layer Stack
Client (browser/app)
│
▼
┌────────────────┐
│ CDN │ ← caches redirects for hot/viral links at the edge
└───────┬────────┘
│ (cache miss)
▼
┌────────────────┐
│ Load Balancer │ ← L7, round-robin or least-connections across app servers
└───────┬────────┘
│
▼
┌────────────────┐
│ App Servers │ ← stateless; handle both /shorten (write) and /{code} (read)
└───────┬────────┘
│
┌─────┴─────┐
▼ ▼
┌──────┐ ┌─────────┐
│Cache │ │Database │
│Redis │ │(KV store│
└──────┘ │ or SQL) │
└─────────┘
Every layer exists to shed load from the layer below it. The CDN sheds load from the load balancer, the load balancer sheds load from app servers, the cache sheds load from the database. Say this explicitly — it demonstrates you understand caching as a system-wide discipline, not just “add Redis.”
The Write Path: Creating a Short URL
1. Client POST /api/shorten { long_url: "https://..." }
2. Load balancer routes to any available app server (write requests
are NOT cache-eligible, so this is a straight pass-through)
3. App server:
a. Validate the long URL (well-formed, not on a blocklist)
b. Generate a unique short code (base62 counter, see interview guide)
c. Write { short_code, long_url, created_at, expires_at } to DB
d. (Optional) Pre-warm the cache with this new mapping
4. App server returns { short_url: "https://sho.rt/aZ9kLp" } to client
Key design decision: the write path talks to the primary database node, never a replica, because you cannot risk a read-your-write inconsistency where a user creates a link and then immediately gets a 404 redirecting it. This is a classic follow-up question — always name the consistency requirement explicitly.
Writes are infrequent relative to reads (per the 100:1 to 1000:1 ratio established in the requirements), so the write path does not need to be nearly as horizontally scaled as the read path — a smaller fleet of write-capable app servers is fine.
The Read Path: Resolving a Short URL
1. Client GET https://sho.rt/aZ9kLp
2. CDN checks edge cache for this path
HIT → CDN returns cached 301/302 redirect directly (no origin hit)
MISS → forward to load balancer
3. Load balancer routes to any app server
4. App server checks Redis cache for short_code -> long_url
HIT → return redirect immediately, log click event async
MISS → query database, populate cache, return redirect
5. Client's browser follows the Location header to long_url
The critical detail: on a cache miss, the app server must populate the cache before returning, using a read-through pattern, so the next request for the same code is a cache hit. Never let the client-facing response wait on analytics writes — click-tracking (covered in the companion data model article) should be fire-and-forget via a message queue or async task, not inline in the redirect’s critical path. A redirect blocked on an analytics DB write is a latency bug waiting to happen.
Cache Invalidation: The Part Everyone Glosses Over
URL shortener mappings are close to immutable — once created, a short_code -> long_url pair almost never changes. This makes caching unusually easy compared to most systems, but there are still three invalidation scenarios worth naming in an interview:
- Link expiration. If a link has a TTL, the cache entry must expire at or before the DB record does. Set the Redis TTL equal to (or slightly shorter than) the link’s expiration to avoid serving a dead link from cache.
- Manual deletion/deactivation. If a user or admin deletes a link, you must actively invalidate the cache key (
DEL short_code) rather than waiting for natural TTL expiry — otherwise the link keeps working from cache after “deletion,” which is a real production bug and a great interview gotcha to raise yourself. - Custom alias collision resolution. If a custom alias is claimed, then released, then reused for a different long URL, a stale cache entry could redirect the new alias to the old destination. Always invalidate on any write to an existing key, not just on creation.
The general rule to state out loud: cache invalidation is driven by the write path, not by a background sweep. Any service that writes to the mapping table owns clearing the corresponding cache key in the same transaction/step.
Read vs. Write Path Side-by-Side
| Aspect | Write Path (/shorten) | Read Path (/{code}) |
|---|---|---|
| Frequency | Low (1 unit) | High (100-1000x) |
| Target latency | ~200ms acceptable | <100ms, ideally <20ms cached |
| Talks to | DB primary only | CDN → Cache → DB replica |
| Scaling lever | Vertical + modest horizontal | Aggressive horizontal + edge caching |
| Consistency need | Strong (read-your-write) | Eventual is fine |
| Cache role | Populate on write (optional) | Primary defense layer |
Handling Redirect Status Codes Correctly
This deserves its own callout because it trips up even strong candidates. A 301 (Moved Permanently) tells the browser to cache the redirect itself, so subsequent visits never even hit your CDN or servers — great for scale, bad if you ever need to change or expire the destination, and bad for per-click analytics since the browser skips your server entirely after the first hit. A 302 (Found/Temporary) forces the browser to check in every time, giving you accurate click counts and full control, at the cost of every single click hitting your infrastructure. Most production shorteners default to 302 specifically because click analytics is a core product feature, not an afterthought — say this and you’ll sound like someone who has actually operated one of these systems.
Putting It Together
The architecture only makes sense once you see it as two separate flows sharing infrastructure: a small, strongly-consistent write path that touches the primary database, and a massive, cache-first read path that fans out across CDN, edge cache, and application-layer cache before ever touching a database replica. Drawing one combined diagram is fine for the whiteboard, but narrating these as two distinct flows — with distinct scaling levers, distinct consistency requirements, and a clear invalidation rule tied to the write path — is what turns a “draws boxes” answer into a “understands distributed systems” answer.
For 50+ system design deep-dives like this one, see The 0-to-1 SWE Interview Playbook (Amazon: https://www.amazon.com/dp/B0H256Z1MF?tag=sirjohnnymai-20). It includes annotated request-flow diagrams for the URL shortener plus a dozen other classic interview questions.
Pair this article with “Capacity Estimation” to size each of these layers with real numbers, and “Data Model and APIs” for the exact schema and endpoint contracts referenced above.