Prompt and Scope
Design a service that accepts an HTTP or HTTPS URL and returns a short link. Visiting the short link must redirect the user to the stored destination. The base design supports an optional custom alias and expiration time. Click analytics, custom domains, account management, and link previews are follow-ups rather than core requirements.
Use these case assumptions so every capacity claim is reproducible:
- 1 million new links and 100 million redirects per day;
- traffic peaks at 10 times the daily average;
- links are retained for five years unless they expire or are disabled;
- the redirect path targets 99.99% monthly availability and p99 service latency below 100 ms;
- a stored mapping averages 500 bytes before indexes and replication.
These are interview assumptions, not measurements from a named product. They imply a read-heavy system, but the design must still preserve uniqueness during concurrent creation, return a newly created link reliably after ambiguous timeouts, and stop serving expired or abusive links within a defined propagation window.
What the Interviewer Evaluates
The first signal is requirement control. A useful answer separates link creation and redirection from analytics and account features, defines whether destinations can change, and asks how expiration and custom aliases behave. Adding a queue, search engine, or graph database before defining those contracts weakens the design.
The second signal is whether scale estimates change decisions. The assumed workload averages about 12 creates and 1,200 redirects per second, with peaks near 120 and 12,000 per second. Five years of creation yields about 1.8 billion mappings and roughly 0.9 TB of raw mapping data. Replication, indexes, storage overhead, and headroom make the provisioned footprint several times larger. These numbers justify a partitionable durable store and a cache, but they do not justify every possible distributed component.
The third signal is identifier correctness. An eight-character Base62 code has 62^8, or about 218 trillion, possible values. At 1.8 billion retained links, occupancy is below 0.001%. A new random draw therefore has a tiny collision probability, yet the probability that the system has ever seen some collision becomes large after enough draws. Randomness reduces predictability; it does not guarantee uniqueness. The durable write must atomically assert that the code does not exist and retry a random collision.
The fourth signal is read-path and failure reasoning. The cache is an optimization, not the source of truth. Expiration must be checked during reads instead of depending on a cleanup job. A hot key, cache outage, database timeout, duplicate POST, regional replication lag, analytics backlog, and emergency takedown each need an explicit behavior.
Finally, a strong answer treats security as a core redirect requirement. Short links can disguise phishing destinations and predictable codes can enable enumeration. URL parsing, allowed schemes, rate limits, reputation checks, abuse reporting, rapid disablement, and non-sequential public codes belong in the design rather than in a generic “add security later” box.
Clarifying Questions Before Answering
- Can a destination change after creation? The base mapping is immutable. Immutability simplifies
caching and audit history. If editing is required, add versioning and a strict invalidation SLO.
- Should equal long URLs share one code? No. Different owners, campaigns, expiration times, and
policies may need distinct links. Deduplication can be an explicit option, not an accidental side effect of hashing the destination.
- Are custom aliases required? They are optional and unique in the selected domain. A conflict
returns 409; the service never silently changes a requested alias.
- What happens at expiration? A known expired or disabled code returns
410; an unknown code
returns 404. Reads check expires_at, while asynchronous deletion only reclaims storage.
- Which redirect status is expected? Use
302by default because mappings may be disabled and the
service may need every request for policy or analytics. Offer 301 only for immutable links whose owner accepts long-lived client and intermediary caching.
- What consistency is required? Code reservation and custom-alias creation require strong
uniqueness. Existing redirects favor availability, but a successful create must be readable immediately through cache population or a read-after-write path.
- Must analytics be lossless? It is outside the base path. If added, define acceptable loss and
freshness separately so a delayed analytics pipeline does not block redirects.
- Does the service fetch destination content? The redirect path does not. Any preview or malware
scanner that fetches URLs runs in an isolated asynchronous service with SSRF defenses.
30-Second Answer Framework
“I will keep creation and redirection as the two core flows. With one million creates and one hundred million redirects per day, the average is about 12 writes and 1,200 reads per second, with 10-times peaks. I will generate cryptographically random eight-character Base62 codes and reserve them with an atomic insert-if-absent; custom aliases use the same condition. The durable mapping store is partitioned by a hash of the code and remains the source of truth. Redirect servers use cache-aside, check status and expiration, then return a 302 Location response. Creation is idempotent, cache entries never outlive link expiration, and updates or takedowns push invalidations. I will scale the hot read path independently, protect cache misses from stampedes, and keep analytics asynchronous. I will validate uniqueness races, response-loss retries, hot keys, cache and database failures, expiration boundaries, and abuse-disable propagation against explicit SLOs.”
Step-by-Step Deep Dive
Begin with a small capacity ledger:
Creates: 1,000,000 / 86,400 ≈ 12/s average, ≈ 120/s at 10x peak
Redirects: 100,000,000 / 86,400 ≈ 1,200/s average, ≈ 12,000/s at 10x peak
Mappings: 1,000,000 × 365 × 5 = 1.825 billion
Raw data: 1.825 billion × 500 bytes ≈ 0.9 TB before overhead and replicas
Code space: 62^8 = 218,340,105,584,896; occupancy remains below 0.001%The core API can stay narrow:
POST /v1/links
Idempotency-Key: client-generated-key
{ "url": "https://example.com/a", "customAlias": null, "expiresAt": null }
-> 201 { "code": "aZ3kP9qR", "shortUrl": "https://sho.rt/aZ3kP9qR" }
GET /{code}
-> 302 Location: https://example.com/a
-> 404 when the code never existed
-> 410 when it is expired or disabledThe primary access pattern is a point lookup by code, so one durable record needs only the fields that serve creation, redirection, and lifecycle policy:
links
code primary key
long_url
owner_id
status ACTIVE | DISABLED
created_at
expires_at nullable
version
create_requests
owner_id + idempotency_key unique key
request_fingerprint
code
status
expires_atUse a cryptographically secure random generator for eight Base62 characters. A seven-character space already contains about 3.5 trillion values, but the eighth character leaves more headroom and makes online enumeration harder at negligible URL cost. A random generator avoids a centralized numeric allocator and predictable sequences. It still needs an atomic conditional write: insert the mapping only if code is absent. If the condition fails for a generated code, draw again with a bounded retry count. If a custom alias conflicts, return 409 because changing it would violate the caller's contract.
A counter encoded as Base62 is a valid alternative. It guarantees unique compact values if the allocator is correct, and range leasing can reduce coordination. Its costs are allocator recovery, range loss, regional ownership, and predictable enumeration. Hashing the long URL is not a free solution: truncation can collide, equal destinations may need separate links, and resolving a collision still requires storage. State which property matters before choosing among random codes, leased counters, and hashes.
Creation proceeds in this order:
- Authenticate where required, rate-limit the caller, parse the URL, allow only
httpandhttps,
enforce length and policy limits, and normalize the custom alias.
- Check the idempotency key. Reusing it with a different request fingerprint is a conflict; reusing
it with the same request returns the original result.
- Generate or accept a code. In one transaction, conditionally reserve the code and persist the
idempotency record. The transaction prevents two creators from winning the same alias and prevents a lost response from creating a different link on retry.
- After the durable commit, populate or invalidate cache state and enqueue asynchronous reputation
scanning. Never return a code that exists only in cache.
If the write times out, the client retries with the same idempotency key. The service first reads the request record and returns the committed result if present. Blindly generating another code turns an ambiguous response into duplicate durable state. If the store cannot prove whether the transaction committed, report an in-progress or retryable outcome rather than claiming failure and creating a new mapping.
For redirection, an edge or stateless redirect service checks a rapidly propagated denylist, then looks up code in a distributed cache. A hit still checks status and expires_at. A miss performs a point read from the durable store, validates the same lifecycle fields, and caches the mapping. Set the cache TTL to no later than expires_at; add small jitter to broad TTLs so many entries do not expire together. Cache unknown codes briefly to absorb scans, but invalidate a negative entry when a custom alias with that code is created.
Return 302 with a Location header by default. HTTP semantics define 302 as a temporary location, so clients continue using the short URL on future requests. A 301 indicates a permanent new URI and is heuristically cacheable; it can remove traffic from the service but also delays revocation, destination changes, and request-level analytics. Redirect status and Cache-Control are product contracts, not a performance toggle hidden inside the service.
The durable store can be a key-value database or a relational database partitioned by a hash of code. The core requirement is atomic create-if-absent, durable replication, point reads, backups, and a tested restore path. Random codes naturally distribute normal traffic, although one viral code is still a hot key. Replicate the cache, add a small local cache for extreme hot links, and coalesce concurrent misses so one expiration does not send thousands of identical reads to the database.
Failure policy must preserve the source-of-truth boundary:
- If the cache is unavailable, use a circuit breaker, bounded direct reads, local hot entries, and
admission control. Unbounded cache bypass can turn a cache incident into a database incident.
- If the database read path is unavailable, serve a bounded stale positive cache entry only when the
product accepts that risk. Never extend an expired link or bypass a takedown denylist.
- If the durable write path cannot guarantee uniqueness, fail creation. Availability does not justify
issuing two destinations for one code.
- If analytics is delayed, redirects continue and click events buffer, sample, or drop according to
the separately stated analytics contract.
- If cleanup stops, reads still enforce expiration. Storage grows, but expired links are not served.
Security validation starts at creation and continues after it. Reject non-HTTP schemes and malformed URLs with a real parser. Rate-limit by account, network, and risk signal; scan destinations asynchronously; maintain reporting and appeal flows; and propagate confirmed takedowns to the redirect path quickly. Codes should not grant access to private content. If the destination needs authorization, the destination system must enforce it; obscurity in a short code is not access control.
Verification should exercise properties and failures. Race many creators for one custom alias and confirm exactly one succeeds. Lose the first POST response and confirm the idempotent retry returns the same code. Test one second before, at, and after expiration. Create a code immediately after a negative-cache lookup. Generate enough random codes to exercise conditional conflicts artificially. Load the 10-times peak with both a broad working set and a single hot code, then fail cache nodes, throttle the database, delay invalidations, and stop the cleanup and analytics workers. Measure redirect success, p99 latency, cache hit rate, database miss load, conditional conflicts, stale-link serving, and takedown propagation instead of reporting only average throughput.
High-Quality Sample Answer
“I would scope the base system to create and resolve short links, with optional custom aliases and expiration. I would clarify that destinations are immutable, equal long URLs may receive different codes, known expired links return 410, and analytics does not block redirects.
Using the case assumptions, creation averages about 12 requests per second and peaks near 120; redirects average about 1,200 and peak near 12,000. Five years retains about 1.8 billion mappings, or roughly 0.9 TB raw at 500 bytes each. I would therefore use a durable store that supports point reads, partitioning, replication, and atomic conditional inserts, with code as the partition key.
For generated links, I would draw an eight-character Base62 code with cryptographic randomness. The space is about 218 trillion values, so the per-insert collision chance remains tiny at our scale, but randomness does not prove uniqueness. I reserve the code with insert-if-absent and retry a generated collision. A custom-alias conflict returns 409. The POST also carries an idempotency key; the mapping and request record commit together so a lost response can return the same code.
The redirect service checks a takedown denylist and a cache. On a cache miss it performs a point read, checks active status and expiration, caches no longer than the remaining lifetime, and returns a 302 with Location. I use 302 by default because the mapping may be disabled and the service may need request-level policy or analytics; immutable links can opt into 301 and stronger caching. Negative entries get short TTLs, and creating a custom alias invalidates any negative cache entry.
The durable mapping is the source of truth. Cache failure degrades to bounded database reads with admission control, database failure may use bounded stale positive entries only under policy, and creation fails if uniqueness cannot be guaranteed. Hot keys use layered caching and miss coalescing. Analytics and reputation scanning are asynchronous, while confirmed abuse disables the link through a rapidly propagated control path.
I would prove the design with concurrent alias races, response-loss retries, expiration boundaries, negative-cache invalidation, hot-key and broad-set load, cache loss, database throttling, cleanup failure, and takedown propagation. Acceptance is tied to 99.99% redirect availability, p99 below 100 ms under the stated peak, no duplicate code winners, no served expired links, and a measured disable propagation window.”
Common Mistakes
- Hashing the long URL and assuming uniqueness → truncation collides and equal URLs may need
separate policies → Use an atomic reservation and define whether deduplication is desired.
- Using random codes without a conditional write → probability is mistaken for a guarantee →
Insert only when the code is absent and retry generated collisions.
- Returning a new code after a write timeout → one client action creates multiple links → **Bind
retries to a persisted idempotency key and recover the original result.**
- Writing cache before durable storage → a successful-looking link disappears on eviction → **Commit
the source of truth first, then populate cache.**
- Depending on a deletion job for expiration → a delayed job serves expired links → **Check
expires_at on every resolution path and use cleanup only to reclaim space.**
- Calling 301 “faster” and 302 “uncached” → cache behavior and mutability are oversimplified →
Choose redirect semantics and explicit cache controls from the product contract.
- Caching 404 indefinitely → a newly created custom alias remains unreachable → **Use a short
negative TTL and invalidate it on creation.**
- Sending every cache miss directly to the database → a hot-key expiry creates a stampede → **Use
request coalescing, TTL jitter, and layered hot-key caching.**
- Making analytics synchronous → a non-core pipeline outage breaks redirects → **Emit events after
resolving and define analytics loss/freshness separately.**
- Treating a short code as authorization → enumeration or sharing exposes protected content →
Require authorization at the destination and use the code only as a locator.
Follow-Ups and How to Handle Them
Follow-up 1: How would you add near-real-time click analytics?
Emit a click event after the redirect decision with code, event time, request ID, and only the privacy-approved dimensions. Partition the stream by code for ordered per-link aggregation, but salt or split exceptionally hot codes if one partition saturates. Consumers update minute and daily aggregates idempotently. Define acceptable loss, duplication, freshness, retention, bot filtering, and consent before choosing acknowledgements; the redirect must not wait for the analytical store.
Follow-up 2: How would you deploy active-active across regions?
Keep reads local through regional caches and replicas. Code creation still needs global uniqueness: use a globally conditional store, allocate disjoint random or numeric namespaces per region, or route creation to a home region. A successful creation needs a read-after-write strategy until replication catches up. Takedown metadata needs a faster, separately measured propagation path than ordinary mapping replication.
Follow-up 3: What changes for editable destinations?
Add versioned conditional updates, an audit record, owner authorization, and cache invalidation keyed by code and version. Define whether already cached 301 responses can remain stale; if rapid edits or revocation matter, default to 302 with bounded cache freshness. Concurrent updates require an expected version so one editor does not silently overwrite another.
Follow-up 4: How do you handle one link receiving millions of requests per second?
Serve it from CDN or edge cache, regional caches, and a small in-process cache, with consistent takedown checks. Replicate the hot value instead of trying to shard one key by its code. Coalesce refreshes, refresh before expiry, and isolate hot-key traffic so it cannot consume the full cache or database connection budget. Load-test invalidation because a cached viral link is also the hardest link to revoke quickly.
Follow-up 5: How do custom domains affect the key and routing model?
Uniqueness becomes (domain, code), not just code. Verify domain ownership, provision certificates, route by host, and keep tenant quotas and abuse policy. The cache key and database partition key must include the domain. If the same alias exists on two domains, neither should overwrite or invalidate the other.
Follow-up 6: What if legal deletion must erase a destination immediately?
Separate serving disablement from physical erasure. First mark the record disabled, update the denylist, invalidate caches, and verify that every region returns 410 within the takedown SLO. Then delete or cryptographically erase the durable record, backups, analytics dimensions, and search or scanner copies according to retention policy. An asynchronous deletion job alone cannot prove that the link stopped resolving.
Follow-up 7: How would you migrate from eight-character to longer codes?
Make the resolver accept a versioned range of lengths before writers change. New writers can issue longer codes while old mappings continue to resolve unchanged. Partitioning must not depend on a fixed character position that the new format removes. Monitor resolver errors and cache-key parsing, then retire old writer versions; never rewrite existing public codes merely to standardize length.