System designby Learnastra

System-design interview · Core interviews

Design a URL shortener

By Anup Rai

Build a durable code-to-URL service, then justify uniqueness, retries, caching and storage growth from one concrete redirect.

You will learn to

  • Trace creation and browser redirection through a working baseline.
  • Explain uniqueness, retry recovery and the cost of caching.
  • Defend scaling decisions without promising unsupported deletion or failover guarantees.

Practice in this chapter

8 interview questions with model answers and follow-ups.

Go to interview practice

Useful foundations: HTTP APIs and request lifecycle · Caching: cache hits, misses, write policies and invalidation · Idempotency, retries, and timeouts

Workload and timing examples are interview assumptions.

01What the short link actually does

Someone prints an event poster containing a registration address. The original address is long, so our service gives them https://s.example/q7Lm2Ax9. When another person opens that address, the browser should reach the original registration page. The service stores a code-to-destination mapping; it does not compress or host the destination page.

For this interview, support creating a link, resolving it, choosing an optional custom alias, expiring it and deleting it as its owner. Destinations do not change after creation. Redirects are public; creation and owner actions require authentication. Click statistics may arrive late. Private links, billing-grade counts and immediate worldwide revocation are separate requirements to discuss if requested.

Two rules matter immediately. A code must never resolve to another creator's destination, and a successful creation must have a durable stored mapping. An unavailable destination is outside our service: we can return the correct redirect even when the event website is down. Ask how quickly deletion must take effect, because that decision later determines whether cached redirects are acceptable.

02Functional requirements

Agree on these supported actions before selecting components.

  1. Create and manage links. Authenticated owners create immutable destination mappings, optionally request a custom alias and expiry, list their own links, and delete them.

  2. Resolve a public code. An active code returns an HTTP redirect to its saved destination. Missing or expired links return an unavailable-link result; deletion follows the cache-freshness policy below. The destination page is fetched by the browser.

  3. Recover a repeated creation. Repeating the same owner-scoped request and payload returns the same link during the supported retry window. Conflicting aliases or changed payloads are rejected.

  4. Record lightweight statistics. Collect approximate click statistics asynchronously; delayed or lost analytics must not change the redirect result.

03Non-functional requirements

Use these as illustrative interview assumptions to agree with the interviewer. Numerical targets require measurement; they are not claims about an existing product or a proven implementation. p95 (the 95th percentile) means 95% of measured operations finish within the stated time.

  1. Workload. Plan for approximately 965 creates/s and 96,450 redirects/s at peak, with 30 billion claimed codes after five years. These are the illustrative assumptions derived below.

  2. Response time. Target p95 of 50 ms for redirects and 200 ms for creation, measured from regional service ingress to response at the planning peak under normal operation. Internet transit and destination-site loading are outside these measurements.

  3. Durability and availability. A successful creation must survive an application restart or one database-node failure within the serving region. Unsafe writes stop rather than acknowledge an unprotected mapping; regional disaster recovery is a separate requirement.

  4. Identity and retries. Codes never change owners or destinations, including after deletion. Retain creation results for at least 24 hours so a matching retry within that window cannot create another link.

  5. Cache freshness. Expiry is checked on every serving path. Deletion has eventual visibility with best-effort invalidation and one-minute internal cache entries; this is not a hard global one-minute revocation guarantee.

  6. Access and abuse. Owner actions require current authentication and ownership checks. Limit creation abuse and accept only supported destination schemes; a public random code is not a private-access credential.

04Follow one creation and one visit

Begin with one application and one relational database. The application receives a destination URL, generates a candidate code and inserts a row containing that code and destination. The code column has a unique constraint, which means the database rejects a second row with the same code. After the transaction commits, the application returns the short URL.

The visit follows three steps:

  1. The browser requests /q7Lm2Ax9.
  2. The application looks up that exact code, checks that the row is active and unexpired, and returns HTTP 302 with the destination in the Location response header.
  3. The browser makes a second request to the destination website.

The shortener's response contains a header and perhaps a small body; it does not carry the destination page's images or video.

This is already a complete useful service. A missing or expired code returns an unavailable-link response rather than an invented destination. One indexed lookup can serve each redirect. A database transaction publishes a complete mapping before the creator receives success. Next, use the workload to decide when lookup cost or storage growth justifies more components.

Design diagramA shortener redirects the browser

The browser contacts the destination after receiving Location; destination content does not pass through the shortener.

A shortener redirects the browserThe browser contacts the destination after receiving Location; destination content does not pass through the shortener. browser to app: Create or visit code; app to db: Transaction or code lookup; app to browser: Short URL or 302 Location; browser to site: Follow redirectCreate or visit codeTransaction or code lookupShort URL or 302 LocationFollow redirectACTORCreator or visitingbrowserSERVICEShort-link applicationSTOREMappings andrequest resultsEXTERNALDestination websitesync
Read each connection in order
  1. syncCreate or visit codeCreator or visiting browser → Short-link application
  2. syncTransaction or code lookupShort-link application → Mappings and request results
  3. syncShort URL or 302 LocationShort-link application → Creator or visiting browser
  4. syncFollow redirectCreator or visiting browser → Destination website

05Estimate the work that could outgrow the baseline

Assume 500 million creations in a 30-day month and 100 visits per creation. These are interview assumptions, not traffic measured from a named company. There are 2,592,000 seconds in that month, giving about 193 creates and 19,290 redirects per second on average. With a fivefold planning peak, test approximately 965 creates and 96,450 redirects per second.

Quantity Calculation Design implication
Five-year mapping count 500M × 12 × 5 = 30B Retention eventually requires substantial storage
Logical mapping bytes 30B × 500 B = 15 TB Replicas, indexes and backups add to this
Peak redirect payload 96,450 × 500 B ≈ 48.2 MB/s Estimate our redirect bytes, not destination-page bytes
Illustrative cache 10M distinct entries × 700 B = 7 GB Size by unique stored entries, not request count

The calculations identify a read-heavy lookup service. They do not prove that every mapping needs caching or that SQL cannot work. Measure indexed lookup capacity and the distribution of repeated visits. A small number of frequently visited codes can dominate requests even when most stored links are rarely read. That observation motivates a cache more directly than the total row count does.

06Make creation, retry and ownership explicit

The creator sends a destination and optional expiry with an owner-scoped request key.

Creation request

POST /v1/links
Idempotency-Key: create-204
Content-Type: application/json

Example request body

{
  "destination": "https://events.example/registration/design-day",
  "expiresAt": "2030-12-31T23:59:59Z"
}

Idempotency means retrying this same logical request returns its existing result rather than creating another link. Store the key under the authenticated owner and compare the supplied payload before replaying a result. Reusing it with different input is a conflict.

Request Meaning
POST /v1/links Create a mapping; return its code after commit
GET /q7Lm2Ax9 Return the stored destination in a redirect
DELETE /v1/links/q7Lm2Ax9 Owner marks the link deleted
GET /v1/links?after=<cursor> Page through the owner's links

For a custom alias such as design-day, return a conflict if somebody already owns it. Do not silently generate another alias after the caller asked for an exact one. Validate allowed URL schemes and length; preserve the destination's encoded meaning rather than casually rewriting it.

A creation timeout is an unknown outcome: the database may have committed before the response disappeared. The client retries create-204 or retrieves its status. A request key identifies an operation; two intentionally separate creations may legitimately target the same destination and keep different statistics.

07Store the mapping and the answer to a retried request

Use two records so a redirect mapping and its creation result have distinct purposes.

Record Fields Purpose and uniqueness
Link code, ownerId, destination, createdAt, expiresAt, deletedAt Answers a redirect lookup; code is unique.
Request ownerId, requestKey, payloadHash, code Remembers which result belongs to a creation request; (ownerId, requestKey) is unique.

In the baseline, insert both within one database transaction so neither becomes visible alone. If concurrent copies of one request choose different candidate codes, the request-key constraint lets only one transaction commit; the loser rolls back its tentative mapping and returns the winner’s saved result. A code collision from an unrelated request instead chooses another candidate.

The main lookup uses the code's primary-key index. Owner listing instead needs an index ordered by (ownerId, createdAt, code). A pagination cursor records the last time-and-code pair; the code breaks ties between links created at the same timestamp. These are different access patterns, so a fast code index does not automatically make owner listing efficient.

Never reuse a previously issued code for unrelated content. Somebody may still have an old poster or bookmark after the original link expires. Retain a compact claim or deletion marker even if old destination bytes are removed. This consumes some permanent identity storage but avoids giving an old URL a new owner. A scheduled cleanup job can reclaim expired payloads; reads must check expiry independently because that job may run late.

08Generate candidates; let storage decide uniqueness

Eight characters drawn from digits and upper- and lowercase letters give 62^8, about 218 trillion possible codes. With 30 billion permanently claimed codes, approximately 0.0137% of the space is occupied. A random candidate is therefore likely to be unused, but random generation never proves uniqueness.

Suppose two application servers both choose q7Lm2Ax9. Each tries an insert against the same unique code constraint. One succeeds; the other sees a collision, chooses a new random candidate and retries. A prior lookup saying “absent” would not be enough: both servers could read that answer before either writes.

A sequential number encoded in base 62 is another option. It avoids random collision retries but needs a safe allocation mechanism and produces predictable identifiers. A separate service can allocate batches, although that adds another component and failover responsibility. At the assumed creation rate, random candidates plus enforced uniqueness are a defensible starting choice.

Keep successful request results long enough for the documented client retry window. After that window, clients need explicit status recovery or a new intentional operation; do not promise indefinite retry recovery while discarding the record that makes it possible.

09Add a cache for repeated visits, then distribute storage

If measured database capacity is below the redirect peak, place an internal cache in front of lookups. A redirect first checks the cache; a miss reads the database and stores the result. Let simultaneous misses for one code share one database lookup, so a viral link does not trigger hundreds of identical cache refills. LRU eviction removes entries that have not been used recently when memory is full; it is a starting policy to test against actual traffic.

Caching creates a visibility tradeoff. A cached destination may remain after its owner deletes the database row. For this worked design, use fixed one-minute cache entries and best-effort invalidation when deletion commits. This gives eventual deletion visibility, not a hard worldwide one-minute deadline: a delayed old read can refill the cache after invalidation. State that limitation explicitly. Strict deadlines require the advanced freshness protocol. Send Cache-Control: no-store on browser redirects so the browser does not retain an uncontrolled redirect after our internal entry expires.

Replicated database storage protects against configured failures; application replicas allow independent request handling. Add partitions when retained bytes or measured throughput justify them. Hashing the code distributes distinct mappings, but one viral code remains one hot key and needs replicated cache copies. For the scaled worked design, choose a distributed SQL store that supports the same atomic mapping/request transaction across its partitions. This preserves the retry rule while adding distributed-transaction latency and operational cost, which must fit the measured budget. A separate cross-partition reservation workflow is an Advanced alternative; splitting the tables without either mechanism would lose the creation guarantee.

For this failure target, configure the selected SQL store to acknowledge mapping/request commits only after a durable majority of three replicas in independent failure domains within one region. Verify that its failover preserves those commits; merely enabling an asynchronous replica does not meet the requirement.

Design diagramRedirect traffic uses caches; creation still commits atomically

A visit checks the internal cache before reading a mapping. Creation writes the mapping and request result through one distributed SQL transaction. The browser follows the returned Location itself; internal cache invalidation gives the eventual deletion behavior described above.

Redirect traffic uses caches; creation still commits atomicallyA visit checks the internal cache before reading a mapping. Creation writes the mapping and request result through one distributed SQL transaction. The browser follows the returned Location itself; internal cache invalidation gives the eventual deletion behavior described above. browser to app: Create or visit; app to cache: Lookup / refill / invalidate; app to db: Create transaction / cache-miss read; app to browser: Short URL or 302 (no-store); browser to site: Follow LocationCreate or visitLookup / refill / invalidateCreate transaction / cache-missreadShort URL or 302 (no-store)Follow LocationACTORCreator or visitingbrowserSERVICEReplicated short-linkappsCACHEInternal redirectcachesSTOREReplicated SQLpartitionsEXTERNALDestination websitesyncreturn
Read each connection in order
  1. syncCreate or visitCreator or visiting browser → Replicated short-link apps
  2. syncLookup / refill / invalidateReplicated short-link apps → Internal redirect caches
  3. syncCreate transaction / cache-miss readReplicated short-link apps → Replicated SQL partitions
  4. returnShort URL or 302 (no-store)Replicated short-link apps → Creator or visiting browser
  5. syncFollow LocationCreator or visiting browser → Destination website

10Explain a lost response and a lost cache

The creation transaction commits Link(q7Lm2Ax9) and the result for u17/create-204, then the application crashes before replying. On retry, the service finds the saved request result and returns the original short URL. The user receives one logical link even though the network carried two attempts. If the crash happened before commit, neither row is committed and the retry may perform the creation.

A cache failure produces a different problem: extra database load. At a measured 95% cache-hit rate, a peak of 96,450 redirects/s sends about 4,823 reads/s to storage. Losing the cache can send almost the entire peak there, roughly twenty times more. Limit concurrent database fallbacks and return a retryable error when that budget is exhausted. An unlimited queue merely turns overload into long waits and memory exhaustion.

For database failover, acknowledge writes only under the chosen durability policy and direct writes to the current database leader. Read replicas can be useful, but lag can hide a newly created link or retain a deleted one. Explain which reads may tolerate that delay. Backups address accidental deletion and larger disasters; replication is not a substitute for testing a restore.

Request traceRecover a creation after the response is lost

The request key retrieves the committed result instead of producing another link.

Recover a creation after the response is lostThe request key retrieves the committed result instead of producing another link. client to api: Create with key create-204; api to db: Commit mapping and request result; db to api: Committed q7Lm2Ax9; api to client: Response lost; client to api: Retry create-204; api to db: Read saved request result; api to client: Return the same short URLPARTICIPANTCreatorPARTICIPANTApplicationPARTICIPANTDatabase1. Create with key create-2042. Commit mapping and request result3. Committed q7Lm2Ax94. Response lost5. Retry create-2046. Read saved request result7. Return the same short URLsyncreturnblocked
Read each connection in order
  1. syncCreate with key create-204Creator → Application
  2. syncCommit mapping and request resultApplication → Database
  3. returnCommitted q7Lm2Ax9Database → Application
  4. blockedResponse lostApplication → Creator
  5. syncRetry create-204Creator → Application
  6. syncRead saved request resultApplication → Database
  7. returnReturn the same short URLApplication → Creator

11Protect the service and measure the user experience

Public short links attract spam, malicious destinations and enumeration. Rate-limit creation by authenticated account and apply separate read-abuse controls. A hard-to-guess code is not authorization for private content. If threat scanning is required, run outbound fetching in an isolated service with destination restrictions; the redirect worker should not fetch arbitrary URLs into the application network.

Measure creation and redirect latency separately, cache-hit rate, database fallback load, collision retries, unavailable-link responses and replication or recovery lag. Test a viral link and total cache loss rather than only uniform random traffic. Watch deletion behavior as well as raw availability: quickly returning an invalid link is not a successful user outcome.

Emit click statistics asynchronously so a popular link does not update one contested counter on every request. Under the chosen approximate contract, events may be delayed or lost within a documented buffer policy. Keep personal click data minimal. Cost is driven by retained mapping/index bytes, replicated cache memory, lookup work and the small redirect responses. The target website's delivery bill is not ours.

12Check the design against the requirements

Use the agreed lists to check the finished design. The tests below still need to establish the targets; a proposed mechanism is not a measured result. FR refers to the numbered functional requirements above; NFR refers to the numbered non-functional requirements.

Requirement Design mechanism Validation and remaining limit
FR1–3: creation, aliases, listing and retry Unique code and owner/request constraints; atomic mapping/result commit; owner/time index. Race two alias claims and lose a create response; recover one result within 24 hours. Check cross-owner listing isolation.
FR2 + NFR2: correct fast redirect Indexed code lookup and replicated internal caches; browser follows Location. Load-test the 96,450/s peak with hot keys and cache loss. Measure p95; the diagram alone does not establish 50 ms.
NFR3: survive one database-node failure Synchronous replicated commits, with unsafe writes refused. Kill a database node after acknowledged creation and confirm the mapping and retry result remain. Regional loss is not covered.
NFR5–6: expiry, deletion and ownership Serving-time expiry and owner checks; no-store browser redirects; best-effort cache invalidation. Test expired cache entries and delayed refill after deletion. State the remaining eventual-deletion limit.
FR4: approximate statistics Bounded asynchronous click processing. Interrupt analytics and verify redirects continue; report count delay or loss rather than claiming exact totals.

13Rapid revision

Remember: Randomness supplies candidates; the unique insert prevents two owners from claiming one code.

Question to answer Interview answer Limit to state
What does a visit do? Look up the code and return 302 with Location; browser requests the target We do not host the target page
What makes a code unique? Atomic unique insert, with random retry on collision Large namespace alone is insufficient
What happens after a lost create response? Return that owner’s saved request result Commit mapping and result together, or recover partial work
Why cache? Repeated visits avoid database reads Cached deletes lag; cache failure can overload storage
Why partition? Spread mappings and requests across partitions Popular codes and owner listings need separate handling
Why separate cleanup? Delete expired records outside redirect handling Reads still enforce expiry
What can be approximate? Delayed click statistics Code ownership and destinations must be correct

A concise spoken answer should follow the browser through one creation and one visit, justify the unique constraint, use the traffic estimate to introduce caching, and then explain what happens when the cache or response disappears. Name stronger revocation and cross-partition retry requirements as follow-up work rather than silently claiming they are solved.

Practise the interview questions

Say your answer aloud before opening the model answer. Then answer the follow-up and compare the reasoning.

Foundation · Question 1

Does the shortener download the destination page?

Reveal a model answer

No. It returns a redirect response containing the destination URL. The browser then contacts that website separately.

What the answer must demonstrate: Separates the redirect response from the browser’s destination request.

Applied · Question 2

Why is a large random code space not enough?

Reveal a model answer

Two requests can still generate the same candidate. The database’s unique constraint accepts only one competing insert; the loser chooses another code.

What the answer must demonstrate: Names an atomic uniqueness constraint and the check-then-write race.

Applied · Question 3

How does a retry avoid creating a second link?

Reveal a model answer

Scope a request key to the authenticated owner and store its payload identity and result atomically with the mapping. Replay a matching completed request.

What the answer must demonstrate: Keeps request identity, payload validation and mapping commit connected.

Applied · Question 4

Why can caching change deletion behavior?

Reveal a model answer

The database can contain a deletion while a cache still holds the prior mapping. Serving the copy therefore needs an agreed freshness policy.

What the answer must demonstrate: States a deletion visibility contract instead of treating invalidation as guaranteed.

Applied · Question 5

Why does hashing codes not fix one viral link?

Reveal a model answer

Hashing distributes different codes. Every lookup for the same code still targets its owner.

What the answer must demonstrate: Distinguishes distribution of different keys from repeated reads of one key.

Foundation · Question 6

Can a delayed cleanup job extend a link lifetime?

Reveal a model answer

No. Every serving path checks expiry. Cleanup removes obsolete storage later.

What the answer must demonstrate: Separates expiry enforcement from physical deletion and permanent claims.

Follow-up · Question 7

What must be reconsidered after splitting storage?

Reveal a model answer

The mapping and request result may no longer share one local transaction. Retain transactional support or specify a recoverable creation workflow.

What the answer must demonstrate: Recognizes which transaction boundary the new storage layout removes.

Follow-up · Question 8

What changes for private links?

Reveal a model answer

Authenticate readers and evaluate current access before returning a destination; code secrecy does not establish permission.

What the answer must demonstrate: Separates possession of a URL from current reader authorization.

Blank-page exercise · 45 minutes

Build the answer yourself

Design a public URL shortener for 500M creations per month and 100 redirects per creation. Establish the working baseline, estimate its load, then defend your caching and retry choices.

  • Use 5 minutes to agree the numbered functional and non-functional requirements, including aliases, owner listing, latency, durability and deletion visibility.
  • Use 8 minutes to trace creation and a browser redirect.
  • Use 7 minutes to calculate throughput, retained bytes and cache assumptions.
  • Use 10 minutes to define API, records, uniqueness and retry behavior.
  • Use 10 minutes to add scale and examine lost responses, hot keys and cache failure.
  • Use 5 minutes to check the final design against the numbered FR/NFR lists, state unmeasured targets and eventual-deletion limits, then summarize tradeoffs.

Check that each component and design decision follows from your requirements and workload.

Recall the key ideas

Answer from memory before opening each card. Explain why the choice works and what it costs. Revisit missed cards tomorrow.

Design a URL shortenerTwo servers generate q7Lm2Ax9 at the same time. Which step decides who owns it?Recall first, then reveal

Both attempt the unique insert. Only one commits; the other chooses a new candidate. A preliminary “absent” lookup would not prevent the race.

Generate, then claim.

Return to lesson
Design a URL shortenerCreation succeeds, but its response is lost. What must the retry reuse?Recall first, then reveal

Reuse the same request key for that owner; the server returns its saved creation result.

Retry the operation, recover its result.

Return to lesson
Design a URL shortenerAn expired link still occupies storage because cleanup is late. May it redirect?Recall first, then reveal

No. The redirect handler checks expiry during the read; cleanup delay does not extend the link’s lifetime.

Expiry first; reclamation later.

Return to lesson

Final revision

Summary and interview notes

A shortener saves a code and destination, then redirects visits. A unique insert prevents duplicate codes; a saved creation result lets the caller recover a lost response.

Remember these points

  • The browser fetches the target separately.
  • Read-heavy traffic motivates a measured cache.
  • A deleted or expired code must not acquire a new owner.
  • Sharding must preserve the creation/retry boundary.

Interview tips

  • Explain one request before naming extra services.
  • Connect each scaling component to measured work.

Important qualifications

  • Exact global revocation deadlines and cross-partition retry proofs belong to explicit follow-up contracts.

Continue after the core interview

Explore the advanced version

The advanced lesson keeps the full detailed design. Use these sections when you want to examine the stronger requirements and failure cases.

Technical references

Practice marks stay in this browser.