Practice: Bitly
Design editable, expiring short links for a public URL service. An owner creates and disables links; visitors resolve them without signing in.
Design editable, expiring short links for a public URL service. An owner creates and disables links; visitors resolve them without signing in.
Write your own design before opening hints or the solution review. The numbers below are exercise assumptions, not claims about any company’s production traffic. You may challenge an assumption, but record the replacement and explain which decision changes.
Scenario and constraints
- 100 million stored links; 20,000 peak redirects/s; 200 creates/s.
- Redirect p95 target: 100 ms within the primary region.
- Ordinary destination edits may take up to one minute to propagate.
- Analytics is approximate and must not block redirects.
Your deliverables
- Define create, resolve, edit, and disable contracts including retry identity.
- Show the mapping schema and the atomic code-uniqueness decision.
- Draw both the cached redirect path and its protected origin fallback.
- Calculate origin reads at 95% hits, then explain a full cache outage.
Reason about this sequence
Figure — Owner changes destination → DB advances mapping version → Invalidate controlled cache → Concurrent old fill appears → Reject old version or bound TTL
For every step, annotate what is durable, what the caller knows, and which identity survives a retry. Identify the point where two concurrent actors could disagree. Do not assume a timeout means failure or a cache value grants ownership.
Interviewer follow-ups
- The create response is lost after commit.
- A viral link changes destination while an old cache fill is in flight.
- An abusive link must be disabled faster than ordinary edits.
Answer each follow-up using the same design first. If it breaks, change the smallest boundary that repairs the invariant and explain the new cost. Show whether the change adds latency, storage, coordination, or operational work.
Staged hints
Hint 1 — reveal
Answer: Hint 1 — Separate the durable code-to-destination mapping from optional click analytics; begin with the operation that must never map one code to two intents.
Hint 2 — reveal
Answer: Hint 2 — Generate an unpredictable candidate code and insert it under a unique constraint. On collision, choose another candidate rather than overwrite. Sequential IDs encoded in base62 are another option, but they reveal patterns unless transformed. Namespace custom aliases separately or enforce the same uniqueness boundary. Creation retries use an owner-scoped idempotency key.
Hint 3 — reveal
Answer: Hint 3 — The owner disables a malicious link while a reader refills an old cache entry. Use version-aware updates where the freshness requirement demands them, and bound stale lifetime. For urgent abuse removal, route through an authoritative deny mechanism or purge supported caches. Already cached permanent redirects in external clients may not be retractable. This is why redirect and cache policy are product decisions.
Evidence-based self-review
- Uniqueness and retry identity are enforced at the durable owner.
- Redirect status and cache policy match mutable-link semantics.
- Analytics and cache misses cannot overwhelm resolution.
- The takedown promise acknowledges caches outside service control.
Score each item 0 if absent, 1 if named without an enforceable mechanism, or 2 if the mechanism and a failure are explained. Record evidence from your own diagram beside the score. Then choose one weak decision, revise it, and repeat the relevant follow-up. This rubric is a learning tool, not a hiring forecast.
Reveal the full worked solution
Save your attempt before continuing. ## Interview scope and guarantees
Create an immutable short link, redirect it, and allow its owner to revoke it. Custom aliases and expiry are optional; click analytics must not delay redirects. Target p99 redirect latency below 100 ms within a serving region. Revocation must reach the serving path within an explicitly agreed window.
Capacity worksheet
Assume 10 million creations/day and 1 billion redirects/day: about 116 writes/s and 11,574 reads/s on average. A 5× peak gives 579 writes/s and 57,870 reads/s. At 500 bytes per mapping, a year is 1.825 TB before indexes and replication. With a 95% cache hit rate, peak origin reads are still about 2,894/s; size for a cold cache too.
Concrete API contract
POST /links {url, customAlias?, expiresAt?} -> 201 {code}
GET /{code} -> 302 Location: target
DELETE /links/{code} -> 204
Idempotency-Key scopes create retries to an authenticated owner.Data model and access paths
links(code PK, owner_id, target_url, created_at, expires_at, revoked_at, version)
UNIQUE(code); INDEX(owner_id, created_at, code)
creation_keys(owner_id, request_key, request_hash, code); UNIQUE(owner_id, request_key)Evolve a solution and explain each change
Figure — Three architecture decisions for Bitly, including the pressure each introduces.
Step 1: One durable lookup
Store a unique code and destination; return a temporary redirect after checking expiry. Every redirect hits storage; a popular code concentrates reads.
Step 2: Cache the mapping
Cache immutable targets close to readers; include expiry and revocation state. Cache lifetime can violate the promised revocation interval.
Step 3: Separate analytics
Append bounded click events asynchronously and invalidate serving caches from durable changes. Analytics counts need their own deduplication and loss policy.
Responsibility overview
Figure — Connected responsibilities for Bitly. Trace the authoritative and derived paths separately.
Worked end-to-end scenario
Alice retries creation after a timeout. The API finds her existing idempotency record and returns the same code. A reader follows that code; the service checks expiry, resolves the target, and returns a redirect without fetching the destination. Now Alice revokes it while an edge still holds the mapping. A database update alone cannot prevent that edge from redirecting. Versioned invalidation and a bounded cache lifetime must match the published revocation contract. If the invalidator is down, either accept the stated stale window or put a current deny check on the serving path. The analytics queue can fail without blocking the redirect, provided dropped events are observable.
Why these access paths matter
Code lookup uses the unique primary key. Owner management uses an owner-and-creation index with a stable tie-breaker. A custom alias competes for the same code namespace; enforce uniqueness during insertion. A hot code requires replicated cached copies, whereas adding hash partitions mainly distributes different codes. Cache negative lookups briefly so newly created aliases do not remain invisible for a long time.
Build the baseline first
- Client → link API → unique mapping table.
- Redirect request → lookup → expiry check → 302.
- Owner deletion → durable revocation → invalidation.
Evolve the design under load
- Redirect edge → regional cache → mapping store.
- Mapping change → outbox → cache invalidators.
- Redirect event → bounded analytics stream.
Defend the hardest decision
Eight base62 characters provide 62^8, about 218 trillion possibilities. At one billion occupied codes, a new uniform candidate collides with probability roughly 0.00000458; collisions across the whole history are expected, so a database uniqueness constraint is mandatory. Retry a failed insert with a new candidate. Never check availability and insert as independent operations. Sequential allocation avoids random collisions but exposes enumeration and needs range allocation or another scalable allocator.
Failure and recovery analysis
A revoked link can survive in a cache even after the database is updated. Store a version, invalidate every serving layer, and make the advertised revocation bound consistent with cache lifetime. Do not claim instant revocation while issuing long-lived permanent redirects. For sensitive links, perform a current deny check before redirecting. If analytics fails, preserve redirects and account for dropped or replayed events separately.
Security and privacy boundary
Allow only approved URL schemes, limit link creation, and authorize edits by owner. A redirect service need not fetch the target; adding previews creates an SSRF boundary that needs separate controls.
Interview follow-ups with reasoning
Question: How would custom aliases change uniqueness and abuse protection?
Show answer and explanation
Answer: Require global ownership and reject collisions atomically.
Question: How do you handle a hot code?
Show answer and explanation
Answer: Replicate its cache entry near users; sharding by code alone cannot split its traffic.
Question: What changes for editable targets?
Show answer and explanation
Answer: Version updates and reconsider HTTP caching and redirect status.
Operate and verify the design
Redirect p99, cache miss rate, origin saturation and revocation delay.
Revoke a heavily cached link during an invalidator outage; measure the actual stale-serving interval.
Compare alternatives
| Choice | Benefit | Cost |
|---|---|---|
| Random codes | Less predictable identifiers | Collision retry and sufficient entropy |
| Sequence-derived codes | Simple uniqueness | Predictability and allocator design |
| Cached redirects | Low latency | Edit and takedown propagation |
Design workshop: allocate, resolve, and revoke
Our core flows are create, redirect, and owner revocation. Analytics is asynchronous; accounts and billing are outside the interview baseline. Choose a redirect p95 below 100 ms and a controlled-cache revocation bound of 60 seconds as scenario targets, not measured service claims. If the interviewer requires immediate takedown, cached redirects need an online deny check and a different availability trade-off.
Eight base62 characters give 62^8 = 218,340,105,584,896 candidates. At one billion occupied codes, a fresh uniform candidate collides with probability about 0.00000458. This is the next-insert probability, not the probability that no pair ever collided over the system's life. The unique constraint is the correctness mechanism.
# Pseudocode: randomness proposes; the database decides.
for attempt in range(5):
code = secure_random_base62(8)
try:
with transaction():
claim_creation_key(owner, key, hash_request(body))
insert_link(code, owner, target, expires_at)
save_creation_result(owner, key, code)
return code
except CodeAlreadyExists:
continue
raise RetryableAllocationFailure()A retry first looks up the creation key and compares its fingerprint; the same key with a different URL returns 409. A concurrent uncommitted claim waits or produces a documented retryable response. The claim, mapping and recorded result commit together. A custom alias conflict returns 409 without random retry. Validate supported URL schemes and never shorten a javascript URL.
A counter alternative leases disjoint ID ranges transactionally: worker A receives [1000,1999], B [2000,2999]. Encoding 1000 in base62 repeatedly divides by 62; unused IDs after a crash are holes, not a reason to reuse the range. Counter backup/restore must never move below issued IDs. Random codes reduce enumeration but do not constitute authorization.
Figure — Redirect cache miss and later revocation.
A missing code returns 404; an expired link can return 410. For mutable/revocable targets use temporary redirects and explicit caching policy. A cache entry contains expiry and version; a late version-7 fill must not overwrite a known revocation version 8. Store a short-lived deny/version fence when invalidating, or cap the stale window and admit that guarantee. Origin fallback uses a per-key singleflight and a bounded connection pool, so a cold viral link cannot create unlimited database requests.
Figure — Allocator choices under failure.
At 57,870 peak redirects/s and 95% hits, the ordinary database path sees about 2,894 reads/s before hot-key effects. For 100-byte click events, that redirect peak produces roughly 5.8 MB/s of raw events. Sample analytics only under a stated product policy; use event IDs and bounded deduplication when exact counting is needed. Redirects must still succeed if analytics is unavailable.
Exercise: Range A is issued, the worker crashes after using 1000–1003, and the allocator restores its last durable allocation record. May it reissue 1004?
Show answer and explanation
Answer: No if A still owns the range or its issue history cannot exclude later use. Issue a fresh disjoint range; safe holes are cheaper than identifier reuse. For random allocation, inject repeated collisions and verify the retry cap and unique insert, not a preliminary exists check.
Redis counter command explains a primitive; its durability and failover contract still need separate treatment in this design.
Technical references
Design the behavior when a create response is lost and the client retries. Then explain how disabling a link affects caches.
Your design draft
Clarify assumptions, explain your approach, and test the difficult cases. Save your draft, then compare it with the study notes.
Self-review checklist
Self-guided practice. Automated AI feedback and code execution are not connected.