A URL shortening service handling 100M URL creates per day and 1B redirects per day. Covers short code generation, caching strategy, redirect semantics, consistency tradeoffs, and abuse prevention.
| Requirement | Decision |
|---|---|
| Create short URL | Given a long URL, return a unique short alias — e.g. tinyurl.com/abc1234 |
| Redirect short URL | HTTP 301 or 302 redirect to the original long URL |
| Custom alias | In scope — user can optionally specify their own short code |
| URL expiry / TTL | In scope — URLs can have an optional time-to-live |
| Analytics | Out of scope — click counts, geo, referrers deferred |
| User accounts | Out of scope — no auth required to create or use URLs |
| Scale | 100M URLs created/day · 10:1 read:write → 1B redirects/day |
| Latency | Redirect p99 < 50ms — speed is the product |
| Availability | 99.99% — a down TinyURL is very visible |
| Consistency | Eventual OK for reads; write uniqueness must be guaranteed |
| Durability | Short codes must not be lost — permanent links expected |
301 vs 302 is a product decision, not just a technical one. 301 (permanent) is cached by browsers — reduces server load but kills analytics. 302 (temporary) hits the server every time — enables tracking. Since analytics is out of scope here, 302 + aggressive server-side cache gives the best balance.
Write load is moderate (~1K RPS) — manageable without exotic solutions. Read load is heavy (~12K RPS) — the design must be read-optimized with aggressive caching on the redirect path. The 3.5T code space gives us ~97 years of runway at current scale before exhaustion.
REST API. Three endpoints covering the full functional scope.
| Method + Path | Request | Response |
|---|---|---|
| POST /urls | Body: long_url, optional alias, optional ttl_days |
{ short_url, expires_at } — 201 |
| GET /{short_code} | — | HTTP 302 → Location: long_url |
| DELETE /urls/{short_code} | — | 204 No Content |
No auth for MVP. Rate limiting at the load balancer (token bucket per IP) substitutes. Custom alias: if alias is taken, return 409 Conflict — don't silently override. DELETE: soft-delete in DB + cache invalidation, not hard purge, so we can detect reuse attempts.
Single primary entity. Primary access pattern: given a short_code, return long_url. This is a pure key-value lookup — O(1), no joins, no range scans on the hot path.
| Field | Type | Notes |
|---|---|---|
| short_code | VARCHAR(7) | Primary key / partition key. Base62 [a-zA-Z0-9]. |
| long_url | TEXT (≤2048 chars) | Target URL. Validated on write. |
| created_at | TIMESTAMP | UTC. Immutable after creation. |
| expires_at | TIMESTAMP NULL | Null = no expiry. Used by cleanup worker + TTL index. |
| is_deleted | BOOLEAN | Soft delete flag. Prevents code reuse on deleted URLs. |
| user_id | VARCHAR NULL | Reserved for future auth. Null for now. |
Cassandra or DynamoDB. Access is by short_code (point lookup), write throughput is ~1K RPS, schema is simple, and no joins are needed. NoSQL wide-column handles this cleanly with built-in TTL support and horizontal scaling. Could use Postgres at this scale — the write volume is comfortable — but NoSQL removes connection pool pressure at 12K read RPS and gives us clean sharding when we need it.
This section will include the full write path + read path architecture diagram, component table with decisions, and the short code generation approach (hash vs. token service).
Will cover: short code uniqueness at scale, cache warming + thundering herd, 301 vs 302 tradeoff analysis, DB bottleneck at 10× scale, and abuse prevention strategy.
Planned components: Short Code Generation Service (hash + retry vs. token pre-generation), Cache Layer design (write-through, eviction, warming), Cleanup Worker (TTL scanning + soft delete), and Rate Limiter (token bucket per IP at LB layer).