Why architecture matters here

The architecture matters because the two paths have opposite economics, and the classic failure is building one system that splits the difference and serves neither well. The create path is rare, latency-tolerant, and correctness-critical: it must produce a globally unique code, reject a taken custom alias, refuse reserved words, and ideally screen the destination for malware before minting a link that will be trusted by everyone who clicks it. A few tens of milliseconds and a database transaction here are completely fine. The redirect path is the opposite in every dimension: it runs orders of magnitude more often, users abandon if it is slow, and it is almost pure key-value lookup. If you serve redirects from the same relational database that handles creation, a viral link will hammer that database with millions of identical reads and the whole service — including the ability to create new links — degrades together.

The immutability of mappings is the property that rescues the read path. Because abc123 → https://example.com/... never changes, it can be cached indefinitely with no invalidation problem, replicated to edge locations worldwide, and served without ever consulting the origin on a cache hit. A well-run shortener targets a redirect cache hit rate high enough that the origin database sees a trickle of reads even under a traffic spike; the CDN or in-memory cache absorbs the flood. This is why the read/write skew is not a burden but a gift: read-heavy plus immutable is the easiest workload in computing to make fast.

Get the split right and the system is embarrassingly robust: creation is a small, careful transactional service; redirection is a vast, dumb, cached lookup; and analytics is an asynchronous firehose that never touches the redirect's critical path. Get it wrong — analytics written synchronously on each redirect, redirects served from the origin, keys generated with a scheme that collides — and the service falls over at exactly the moment it succeeds, when a link gets popular.

Advertisement

The architecture: every piece explained

Top row: the create path. The Create API takes a long URL (and optional custom alias and TTL) and returns a short code. Key generation is the crux, with three common designs. Counter + base62: a monotonic distributed counter yields a unique integer, encoded into base62 ([a-zA-Z0-9]) so seven characters cover ~3.5 trillion codes — collision-free by construction because each integer is used once, but requires a coordinated counter (a range-leasing service hands each app server a block of ids to avoid a per-request round trip). Key-generation service (KGS): a background job pre-generates unique random codes into an 'available' table; the create path pops one, sidestepping both collisions and hot counters. Hashing: hash the long URL (e.g. take the leading bits of a SHA), encode to base62, and handle collisions by rehashing or appending — simple but collision-prone at scale and awkward when the same URL should get distinct codes. The metadata store persists code → {url, owner, created, ttl}; the custom alias check enforces uniqueness against reserved words and existing codes in one atomic conditional write.

Middle row: the redirect path. The redirect edge answers GET /code with a 301 or 302 to the long URL (301 is cacheable-forever and offloads future hits to the browser but loses per-click tracking and is hard to change; 302 is non-permanent, keeps every click flowing through you, and is the usual choice for a service that counts clicks). The cache tier — in-memory (Redis/Memcached) and/or CDN edge — holds hot codes so the common case never touches storage; on a miss it reads through to the store and populates the cache. Async analytics emits a click event to a queue and returns the redirect immediately — the counting happens off the critical path. Abuse and safe-browsing screens destinations (at create time, and continuously against threat feeds) so the shortener does not become a phishing laundromat.

Bottom rows: durability and lifecycle. Sharded storage partitions the mapping table by code hash or prefix so no single node holds the whole keyspace; because lookups are by exact code, hash-sharding gives even distribution with trivial routing. Expiry and soft delete handle TTL'd links and takedowns: a background sweep removes expired mappings (or a TTL index does it automatically), and deletions leave tombstones so a reused-looking code cannot silently resurrect an old destination. The ops strip lists the recurring concerns: collision handling on the chosen scheme, cache hit rate (the single most important health metric), 404 rate on unknown/expired codes, and the lag of the click-analytics pipeline.

URL shortener — a write-once key generator fronting a read-heavy redirect cache1000:1 read/write, redirects must never missCreate APIlong URL -> short codeKey generationcounter+base62 / KGS / hashMetadata storecode -> URL, owner, TTLCustom alias checkuniqueness + reservedRedirect edgeGET /code -> 301/302Cache tierhot codes in memory/CDNAsync analyticsclick events -> queueAbuse / safe browsingmalware, phishing scanSharded storagepartition by code prefix/hashExpiry + soft deleteTTL sweep, tombstonesOps — collision handling + cache hit rate + 404 rate + click pipeline lagstoreencodepersistvalidateread-throughcacheemitoperateoperate
URL shortener: the create path generates a unique short code and persists the mapping; the redirect path is a cache-fronted read that must be fast and rarely miss, with clicks recorded asynchronously.
Advertisement

End-to-end flow

Create first. A user submits https://example.com/really/long/path?with=params. The Create API validates and normalizes the URL, runs a safe-browsing check against threat feeds (clean), and requests a code. This deployment uses counter+base62 with range leasing: each API server holds a leased block of 10,000 ids from a coordination service; the server takes the next id in its block — say 987,654,321 — and encodes it to base62, yielding 16Vx8h. It writes 16Vx8h → {url, owner, created} to the sharded metadata store with a conditional put (fails if the code somehow exists, which it cannot under a unique counter, but the guard is cheap insurance). It returns https://sho.rt/16Vx8h. Total: one lease-amortized counter increment and one sharded write, ~10ms.

Now the redirect, ten thousand times. A click hits GET /16Vx8h at an edge POP. The CDN checks its cache: on the first hit it misses, reads through to the in-memory cache (also a miss), reads the mapping from the store shard responsible for 16Vx8h, and populates both caches. It returns a 302 to the long URL and — the critical detail — fires a click event ({code, timestamp, referrer, geo, ua}) onto an analytics queue without waiting for it to be processed. The next 9,999 clicks are pure cache hits at the edge: sub-millisecond, never touching the origin, each emitting a click event to the queue. A downstream consumer aggregates those events into per-link counters and time series. The redirect's p99 stays flat whether the link gets 10 clicks or 10 million, because the hot path is 'cache lookup, emit event, redirect' and nothing on it scales with popularity.

The spike case shows why this split is load-bearing. The link goes viral: 50,000 requests/second. The CDN serves ~99.9% of them from edge caches; the origin store sees only cache-fill misses and occasional revalidation — a few reads per second, not 50,000. The analytics queue buffers the click firehose and the consumer drains it at its own pace, so a temporary processing backlog delays the click count by seconds but never delays a single redirect. Contrast the naive design: redirects served from the relational database with a synchronous UPDATE clicks = clicks + 1 on every hit. That design turns 50,000 redirects/second into 50,000 write transactions/second on one hot row — lock contention detonates, redirects queue, and the service dies precisely because it succeeded. The whole architecture exists to make popularity free.

Two edge cases round it out. An expired link: the store returns nothing (TTL swept it), the edge caches a short-lived negative result, and the user gets a clean 404/410 rather than a redirect to a dead page — and the 404 rate is monitored because a spike means either abuse (scanning for codes) or a botched expiry sweep. A takedown: a phishing destination flagged post-creation gets its mapping replaced with a tombstone/interstitial and the caches purged for that code — one of the rare cache invalidations, justified because safety overrides the immutability convenience.