Design URL Shortener


Patterns, Technologies, and Concepts

URL Shortener

Key Takeaways

Requirements

You initially set latency targets that were too loose for the redirection path, but corrected this on your second attempt by differentiating between creation and redirection latency requirements.

  • URL redirection latency should target under 100ms, not 200ms. Redirection is the hot path that every user experiences on every single click, so it needs a tighter performance budget than URL creation, which is a less frequent and less time-sensitive operation.

  • When defining latency NFRs, differentiate between read and write paths. In a URL shortener, reads (redirects) happen far more often than writes (shortening), so reads get a stricter latency target. This pattern applies broadly to read-heavy systems.

Core Entities

You demonstrated a solid understanding of the core data entities needed for a URL shortener, correctly identifying the short URL, long URL, and User as key components of the data model.

  • In a URL shortener data model, the three essential fields are the short code (the unique key), the original long URL (the value it maps to), and a creation timestamp. The timestamp is important for expiration logic and analytics, so always include it even if not explicitly asked.

  • Linking a URL record to a User entity enables ownership features like listing all links a user created, allowing them to delete or edit links, and enforcing per-user rate limits. This is a common production requirement worth mentioning proactively.

API

You demonstrated a strong understanding of REST API design for a URL shortener, correctly handling optional fields, path parameters, and HTTP redirect semantics on your first attempt.

  • Use HTTP 302 for URL shortener redirects because it is a temporary redirect, meaning browsers and caches will always check back with your server before redirecting. This lets you update or expire short URLs without clients caching stale destinations. Use 301 only when the redirect is truly permanent and you never want to change it.

  • When designing POST endpoints that create resources, mark fields as optional in your request body when the system has sensible defaults or the feature is user-driven. For example, a custom short code and expiration time in a URL shortener are optional because most users just want a quick link without customization.

High Level Design

You demonstrated a solid grasp of the core URL shortener architecture across both attempts, with only minor gaps around expiration handling and HTTP redirect semantics.

  • Always wire your expiration field into the read path, not just the data model. When the redirection service looks up a short code, it should check if expirationTime has passed and return a 410 Gone response if so. Storing the field without using it in the lookup logic leaves the feature incomplete.

  • Use HTTP 302 for URL shorteners instead of 301. A 301 is permanent and browsers cache it forever, meaning future changes to the destination URL will be ignored by returning visitors. A 302 is temporary, so every redirect goes through your server, giving you control and accurate analytics.

  • When describing your database, explicitly state the access pattern it serves. For a URL shortener the key insight is that the short code is the primary key and the long URL is the value, so every read is a direct key lookup with no joins. Saying this out loud signals to the interviewer that you understand why the data model fits the query pattern.

Deep Dives

You demonstrated strong overall system design instincts and improved quickly when gaps were pointed out, with your main stumble being an incomplete explanation of the cache-aside pattern during the thundering herd discussion.

  • The cache-aside pattern has two steps that must both happen: on a cache miss, fetch from the database AND then write the result back into the cache before returning it. If you skip the write-back step, every future request for that key still hits the database. This is the core mechanism that makes caching actually reduce database load.

  • To prevent a thundering herd (many requests hitting the database at once for the same uncached key), use request coalescing. This means only one request fires the database call while the others wait. Once the result comes back, write it to the cache and serve all waiting requests from that single result simultaneously. No individual waiting request should touch the database.

  • When invalidating a cache entry after a database write, always invalidate AFTER the database write succeeds, not before. If you invalidate first and the write fails, you now have an empty cache and stale or missing data in the database, causing unnecessary misses and potential confusion about the true state.

  • When promoting a Redis replica to primary during failover, bump the counter by a fixed offset (like 10,000) before serving any new requests. This avoids reusing counter values that may have already been issued but not yet replicated. That offset bump should also be persisted to disk (AOF or RDB) immediately so that even if the new primary crashes right after promotion, the next failover still starts from a safe non-overlapping range.