URL Shortener — Trade-offs & Decisions
Every significant architectural decision in the URL shortener involves a real trade-off, workload-dependent rather than universally correct. This page revisits the core decisions with explicit alternatives, consequences, and selection criteria — the format expected in a Staff Engineer interview.
ID Generation Strategy
How short codes are generated determines uniqueness handling, predictability, and coordination complexity. The right choice is workload-dependent — there is no single universally correct answer.
| Approach | Advantages | Trade-offs |
|---|---|---|
| Sequence + Base62 | Zero random-collision probability; simple; compact, predictably-growing code length | Sequential values are trivially enumerable; central sequence is a single allocation dependency (though a sequence can sustain substantial concurrent allocation — for example PostgreSQL's CACHE option lets each session pre-allocate a block of values, at the cost of gaps and out-of-order values across sessions; no throughput figure is claimed here) |
| Random Base62 | Non-enumerable; no cross-node coordination needed; large keyspace at 7+ characters | Non-zero per-attempt collision probability requiring bounded retry; retry rate rises as namespace occupancy grows |
| Hash-derived code | Deterministic — same input always produces the same code, if that is a desired product property | Truncated-hash collisions are meaningful and need conflict detection; determinism is unwanted if the same URL should be allowed different codes for different owners/campaigns |
| Snowflake-style + Base62 | Decentralized unique generation without a DB round-trip; rough time-ordering useful for debugging | Requires worker-ID allocation and clock-rollback handling; Base62-encoding does not make the value unpredictable — see Architecture Deep Dive |
Selection criteria
Weigh: uniqueness guarantee strength required, predictability/enumerability requirements, coordination overhead the team is willing to operate, write throughput, whether generation must happen independently across multiple regions, resulting code length, collision/retry complexity, and general operational complexity.
Permanent vs Temporary Redirect
The redirect status is a product decision with infrastructure consequences — not a fixed 'correct' choice, and not simply 'one caches, one doesn't.'
| Choice | Favors | Costs |
|---|---|---|
| 301 Moved Permanently | Lower repeat-request server load if the client/intermediary actually caches it (status + caching headers + client behavior together determine this) | Reduced visibility into repeat clicks from a caching client; risky if destinations can change after creation, since a caching client may not re-check |
| 302 Found (temporary) | Every request can reach the service — full analytics visibility and safe support for destinations that can change | More requests reach the server for popular/repeat-visited links, compared to a client that fully caches a 301 |
| 307 Temporary Redirect | Same intent as 302, but explicitly preserves the original HTTP method — relevant if the original request was not a GET | Same considerations as 302 |
What actually drives the decision
Compare: repeat-request server load, client/intermediary caching behavior (governed by status plus explicit Cache-Control/Expires headers, not status alone), analytics visibility needs, whether destinations are mutable after creation, and how rollback/change behavior should work. A design that needs exact click analytics or supports editable destinations should lean toward a temporary-redirect posture; a design with genuinely immutable, high-traffic destinations can lean toward permanent-redirect caching to reduce load. Avoid stating either as universally correct — see Architecture Deep Dive → Redirect Semantics and HTTP Caching.
Cache Architecture
How much caching to add, and where, trades latency and database offload against consistency and operational complexity.
| Option | Favors | Costs |
|---|---|---|
| No cache | Simplest possible system; one source of truth to reason about | Database absorbs full read volume directly; does not scale past a comparatively modest read rate |
| Distributed cache only | Large database-offload with a single shared, consistent cache tier | Every distributed-cache round-trip still adds a network hop; a single very hot key still concentrates load on one cache node/shard |
| Local (process) + distributed cache | Lowest possible latency for the hottest keys; spreads hot-key load across app instances instead of one shared cache node | Adds a second staleness layer that needs its own expiry-awareness; more moving parts operationally |
Consistency needs defensive correctness, not just a long TTL
A long TTL with explicit invalidation on mutation is a strong, efficient default — but it does not achieve perfect consistency. Failed/delayed invalidation, a local cache layer that wasn't invalidated, races between a write and a concurrent read repopulating the cache, and ordinary expiry all still require defensive correctness (bounding cache TTL by mapping expiry, treating the database as authoritative on any doubt). See Architecture Deep Dive → Cache Hierarchy and Cache Expiry Correctness.
Analytics Collection
Analytics collection adds work on every redirect. How and where that work happens has real consequences for redirect latency and database health, and the design must state explicitly whether analytics loss is acceptable.
| Strategy | Favors | Costs |
|---|---|---|
| Synchronous write on redirect | Simple single code path; no additional infrastructure; no event loss | Adds write latency directly to the redirect path; a high redirect rate becomes a proportional write rate against the same database serving lookups — this competes with, and can starve, the read path |
| Asynchronous durable queue | Redirect path is not affected by analytics write latency; queue absorbs traffic bursts; failures in the analytics pipeline don't affect redirect availability | Analytics becomes eventually consistent; queue/worker infrastructure to operate; requires an explicit policy for producer failure |
| Best-effort async / sampled emission | Lowest overhead on the redirect path; simplest failure behavior (drop and move on) | Not suitable if the product requires exact, lossless click counts |
Database Choice
The URL mappings table has a simple schema and a single dominant access pattern — primary-key lookup by short code — which narrows, but does not eliminate, the database decision.
| Option | Favors | Costs |
|---|---|---|
| Relational (e.g. PostgreSQL/MySQL) | Simple key lookup and uniqueness constraints map directly onto the workload; transactions; mature replication tooling; modest mapping volume under the assumed scale | Requires schema management; horizontal write scaling needs more deliberate design than a natively-partitioned store |
| Distributed key-value / NoSQL | Horizontal partitioning as a first-class capability; strong fit for very large global datasets or multi-region active-active writes | Conditional-write-based uniqueness/collision detection is often more complex to implement correctly than a native UNIQUE constraint; for a comparatively modest dataset with a good cache, provides no proportionate advantage over a well-tuned relational database |
Deterministic Deduplication vs Multiple Codes per URL
Whether the same long URL should always produce the same short code is a genuine product decision, not an automatic optimization — and it is missing from a purely technical design discussion.
| Option | Advantages | Costs |
|---|---|---|
| Same long URL → same short code | Fewer stored mappings; creation looks idempotent to the caller | Ownership, campaign, and expiry semantics become complicated when multiple owners/campaigns want the same destination; concurrent creation of the 'same' URL needs explicit uniqueness/dedup handling to avoid a race |
| Every creation can produce a new code | Independent analytics/campaign attribution per code; independent TTL/owner/policy per mapping; simpler creation semantics (no dedup lookup needed) | Duplicate destinations consume additional namespace and storage |
If global deduplication is required, define precisely which fields participate in the identity check (e.g. long_url alone, or long_url plus owner_id) rather than blindly enforcing uniqueness on a raw URL hash — different owners, campaigns, or expiry policies for the same destination are common, legitimate product requirements that a naive dedup rule would break.
Random Codes vs Enumerable Codes
Uniqueness and predictability are separate concerns and deserve a separate decision — treating Snowflake-vs-sequence as the answer to enumeration conflates the two.
| Scheme | Predictability | Cost |
|---|---|---|
| Sequential / Base62-encoded counter | Simple and compact, but the sequence is directly enumerable — anyone can iterate through codes | None beyond the enumeration risk itself; uniqueness is free |
| Random codes | Not enumerable by iteration | Requires collision handling (UNIQUE constraint + bounded retry) since uniqueness is no longer structurally guaranteed |
| Obfuscated/permuted sequential IDs | Unique like a sequence, without exposing the obvious underlying sequence to an observer | Introduces a permutation/key-management scheme that must itself be operated correctly and kept effectively secret |
Read from Primary vs Read Replica on Cache Miss
Once a request misses the cache, it must read from somewhere authoritative — and whether that read goes to the primary or a replica is its own trade-off.
| Target | Favors | Costs |
|---|---|---|
| Primary | Strongest freshness — always sees the latest committed write | Competes directly with the write path for the same database capacity |
| Read replica | Isolates cache-miss read load from the write path entirely | May be momentarily stale if replication is asynchronous, which can surface as a false-negative for a very recently created mapping |
Eager cache population immediately after a successful write makes the replica-staleness window rare in practice — most reads for a brand-new mapping are served from cache before they would ever reach a replica. The residual risk is a cache miss (eviction, cold start, or population failure) landing on a replica that has not yet replayed a very recent write. See database scaling for the general read-replica mechanics, Architecture Deep Dive → Database Access & Indexing, and Failure & Scale → Read Replica Lag for how this is detected and mitigated.