Skip to main content
Towards the end of the interview, expect follow-up questions that probe how you would change your design. Below are three common follow-ups and concise, practical answers you can use in the interview. Each section explains the problem, a recommended solution, example flows, and operational trade-offs.
Short answers first: use atomic increments (or DB sequences) to avoid duplicate codes, hand out ID blocks to avoid a single counter bottleneck, and store an expiry timestamp to support link expiration. Read the details below for examples and trade-offs.

Follow-up one — concurrent create requests

Question: Two create requests arrive at the same instant. Can they get the same short code? Short answer: No — they should not. Use an atomic mechanism so only one request reads/increments/persists the counter at a time. What to do:
  • Prefer storage-level atomic operations: Redis INCR, database SERIAL/SEQUENCE, or other built-in atomic counters.
  • If your store lacks atomic increments, use distributed locks (e.g., based on etcd/Zookeeper or a Redis-based lock) — note this often performs worse than using native atomic ops.
  • Ensure the read+increment+persist is a single atomic operation or transaction so no two requests observe the same value.
Example logical flow (endpoints mapped to short codes):
Typical atomic-sequence steps for each create request:
  1. Atomically read-and-increment the counter (or acquire lock and increment).
  2. Persist the new counter value (if needed by your allocator).
  3. Convert the numeric ID to a short code (base62-like encoding) and return it.
Only after step 2 completes for Request A can Request B obtain the next counter value. Useful references:

Follow-up two — counter as a single point of failure

Question: What happens when the counter service goes down? Short answer: Making the counter a single, constantly-hit service is risky. Let each application server obtain a block of IDs (a range) from the counter once, and then allocate IDs locally from that block. This reduces contention and the dependency on the central counter. How block allocation works:
  • The central allocator grants non-overlapping numeric ranges to each server (for example, blocks of 10,000 IDs).
  • Each server uses its local range to generate many short codes without contacting the allocator until the block is exhausted.
  • If the central allocator is temporarily unavailable, servers continue issuing IDs from their local ranges.
A dark-themed architecture diagram of a distributed ID/counter system for a URL shortener: a central counter hands out numeric ranges to multiple servers (e.g., 1,000,000→1,010,000 and 1,010,001→1,020,000) which generate short codes stored in a database table mapping short_code to long_url. It also shows different request paths (/courses/linux, /pricing/pro) routed to different servers.
Example allocator grants:
Sample mapping (generated by servers using their ranges): Operational tips and trade-offs:
  • Block size selection:
    • Larger blocks → fewer allocator calls, better throughput, but higher potential ID wastage if a server fails before using its block.
    • Smaller blocks → less wasted ID space, but more frequent central allocator calls.
  • Persist each server’s current position inside its block (e.g., local durable offset) so it can resume allocation after restarts.
  • Consider periodic heartbeats and rebalancing if servers die with unused ID ranges (to avoid wasted ranges when that matters).
  • Use a consensus-backed allocator (e.g., leader-managed increment with replication) if you need strong durability for the allocator itself.
If you use blocks, plan for server crashes: decide whether unused IDs in a server’s block are acceptable wastage or if you need reclamation. Reclaiming ranges increases allocator complexity and coordination.
Question: We want links to expire after a certain time. How would you implement that? Short answer: Store an expires_at timestamp on each short-code row and check it at redirect time. Optionally use TTL indexes or background cleanup to reclaim storage. Implementation options:
  • Synchronous check on redirect:
    • Add an expires_at column to the URL mapping table.
    • On each redirect request, compare current time to expires_at. If now > expires_at, return 404 (or a custom expired response).
  • Asynchronous cleanup:
    • Use a background job to delete expired rows.
    • If your datastore supports TTL/expire indexes (e.g., MongoDB TTL, some cloud stores), rely on that to automatically remove expired entries.
  • Caching considerations:
    • For high-read paths, cache resolved redirects but ensure you include expiry in cache metadata.
    • Cache entries must be invalidated or set to expire at the same time as expires_at to avoid serving stale redirects.
A stylized system diagram of a URL shortener showing servers, a global counter, and a Postgres table of short_codes with long URLs and expiry dates. One entry is highlighted as expired, leading to a 404 "This page doesn't exist" error.
Example URL mapping with expiry: Additional considerations:
  • If you allow custom expiry per user, validate inputs and enforce maximum/minimum expiration windows.
  • For cache-heavy systems, include expiry metadata in cache keys or set TTLs that align with the mapping row’s expires_at.
  • Use background pruning to reclaim storage if retention matters for cost or performance.

Final notes — availability, consistency, operational complexity

When applying these changes, consider trade-offs:
  • Using an atomic central counter gives strictly sequential IDs (simple), but can be a performance bottleneck.
  • ID block allocation improves availability and throughput at the cost of potential ID wastage and more complex allocator logic.
  • Expiry checks are cheap (single timestamp comparison) but must be integrated into caching and background cleanup strategies.
Links and references: Take a moment to internalize these approaches and be prepared to discuss the trade-offs (availability vs. consistency vs. complexity) in the interview.

Watch Video