Step 1 — Clarify requirements
Only ask clarifying questions that would change the design. Useful questions include:-
Can users pick custom short links (e.g.,
shortlink.io/kodekloud) or must the system generate an opaque code?- If custom links are allowed, you need to check for collisions, reserve names, and possibly rate limit creations for a chosen string.
-
Do short links expire, or are they permanent?
- If they expire, you need a cleanup pipeline and extra checks at redirect time.
-
Do you need analytics (click counts, geolocation, timestamps)?
- Analytics introduces additional writes on redirects and often requires asynchronous logging to avoid slowing redirects.
- Custom links are NOT allowed (system-generated only).
- Links do NOT expire (they live indefinitely).
- Analytics are optional / nice-to-have (we’ll design core system first).
Step 2 — Back-of-the-envelope estimation
Estimate only the numbers that affect design choices. Question 1 — how many new links per day?Assume 1,000,000 new links per day.

Assume each created link gets ~1,000 clicks over its lifetime. This implies a read-to-write ratio of roughly 1000:1, i.e., the system is heavily read-biased.

Because reads dominate writes by several orders of magnitude, prioritize optimizing the redirect path: reduce latency, add caching (CDN or in-memory), and design for horizontal read scaling.
At 1,000,000 new links/day for 10 years, you approach ~3.65 billion records. That influences storage choice, indexing, and sharding strategy. Quick summary of design-relevant numbers:
Step 3 — Define the API and data model
At minimum the service needs two endpoints:- Create a short link (POST): client sends a long URL, server returns a short code or full shortened URL.
- Redirect (GET): browser requests the short URL, service responds with an HTTP 301/302 redirect to the long URL.

- Access pattern: single-key lookup on short_code -> long_url. No complex joins or transactions needed.
- Start simple: a relational database (Postgres) with an indexed
short_codecolumn is reasonable for moderate scale: durability, secondary indexes, and easy operations. - For large scale (billions of rows, huge read traffic): consider sharding by code prefix or migrating reads to a distributed key-value store (e.g., DynamoDB, Cassandra) and adding caching layers.
-
Short-code generation
- Fixed-length Base62 (alphanumeric) codes give compact URLs.
- Options:
- Random codes (check for collisions; regenerate if collision).
- Sequential IDs encoded in base62 (no collisions but predictable).
- Hashing the long URL (handle collisions and length).
- Ensure collision handling and consider partitioning strategy for generation if distributed.
-
Redirect performance
- Cache hot mappings in-memory (Redis or in-process LRU) and/or use a CDN/edge-cache for redirects.
- Serve redirects from edge locations when possible to minimize latency.
-
Analytics
- Don’t write synchronous counters on redirect (this can throttle redirects). Instead:
- Emit lightweight events to a queue (e.g., Kafka) and process asynchronously.
- Aggregate click counts into a separate analytics store.
- Don’t write synchronous counters on redirect (this can throttle redirects). Instead:
-
Consistency and durability
- Writes (create short link) should be durable; on create return only after persistence to avoid losing mappings.
- Redirect reads can be eventually consistent if you accept a brief window where a newly created link might not immediately redirect (but typically prefer immediate availability).
-
Scaling and availability
- Read-heavy workloads: scale read replicas or use a cache tier.
- Partition data (shards) once single DB throughput becomes a bottleneck.
- Use monitoring and autoscaling for the redirect service to handle flash crowds.
- PostgreSQL Documentation
- Designing Data-Intensive Applications (Martin Kleppmann)
- Base62 encoding overview