My Solution for Designing a Simple URL Shortening Service: A TinyURL Approach

by solstice_terra887

Requirements


Functional Requirements:

Create short URL from a long URL (optional custom alias)

Redirect short code → original long URL

Optional: expiry/TTL, click analytics


Non-Functional Requirements:

High availability (~99.99%) — shared links must never break

Low redirect latency (<100ms p99)

Read-heavy: reads ≫ writes (~100:1)

Horizontal scalability of the stateless read path



Capacity Estimation

Estimate the scale of the system. Consider daily active users, read/write ratio, storage requirements, bandwidth, and any relevant QPS calculations...




API Design

Create short URL

POST /api/v1/urls Body: { "longUrl": "https://example.com/very/long/path" } Response: 201 { "shortUrl": "https://sho.rt/aB3xK9", "longUrl": "..." } Errors: 400 invalid URL, 409 custom alias taken


GET /{shortCode} Response: 302 Found → Location: https://example.com/very/long/path Errors: 404 unknown code, 410 expired




High-Level Design

Client → Load Balancer / API Gateway — survives TLS termination, routing, distributing traffic

ShortUrlService


— handles POST (create)

--- handles GET (redirect) — this is the hot path

ID generation service — hands out unique short codes (collision-free)

Cache (Redis/Memcached) — sits in front of the DB on the read path

Database — the source of truth for code → long URL

NoSQL


While writing to db, update the redis cache as well.



Database Design

Core table — the mapping:

short_code (PK) VARCHAR(7) -- e.g. aB3xK9 long_url TEXT created_at TIMESTAMP expires_at TIMESTAMP NULL creator_id -- if you track owners

Two things that earn points:

  • Key on short_code — every redirect is a point lookup by code, so that must be the primary access path. Say that out loud.
  • Store only code → long URL. Don't store the full https://sho.rt/aB3xK9; derive the prefix at read time. Saves space and makes the domain configurable.



Detailed Component Design

1. ID generation service — how do you mint unique, collision-free codes?

  • Base62-encode a counter (Snowflake ID or a range-allocated counter): A–Z a–z 0–9, 7 chars ≈ 3.5 trillion codes. No collisions, but codes are predictable/sequential → enable a random offset or shuffle to avoid enumeration.
  • Hash the long URL + handle collisions (check-and-regen): deterministic, dedupes for free.

2. Cache layer — Redis in front of the DB.

  • Eviction: LRU/LFU — hot links stay resident.
  • Thundering herd: a viral link's entry expires and 10k requests miss at once → stampede the DB. Fix with a lock / single-flight or early recompute.
  • Cache ratio: with a small working set you can hit 90%+ hit rate.

3. Read path scaling — stateless read service scales horizontally behind the LB; replicas for DB reads.



Try this problem yourself

Reading a solution is not the same as producing one under interview pressure. Design it on the whiteboard and get scored feedback.


Markdown supported