Design a URL Shortener
OOP design for a URL shortening service covering short code generation, base62 encoding, collision handling, expiration policies, click analytics, and rate limiting.
The Problem
Your marketing team runs 200 campaigns per month, each with dozens of links shared across email, social media, and paid ads. Raw URLs are long, ugly, and impossible to track. The team currently uses a third-party shortener, but they need click analytics (which link, when, from where), custom expiration policies, and the ability to create vanity aliases like go.company.com/launch2026. The third-party tool offers none of this without an expensive enterprise plan.
A URL shortener maps long URLs to short codes (typically 6 characters), redirects users who visit the short URL, and optionally records click analytics. The core challenge is generating unique short codes efficiently while handling collisions, supporting expiration policies, and keeping redirect latency minimal.
Design the core classes for a URL shortening service that supports pluggable code generation strategies (counter-based, hash-based, random), custom expiration policies, click analytics recording, and rate-limited creation.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core actions, error handling, boundaries, and future extensions.
You: "When a user shortens a URL, should the same long URL always produce the same short code? Or does each request create a new mapping?"
Interviewer: "Each request creates a new mapping. Two people shortening the same URL get different short codes."
Good. That simplifies things. No deduplication lookup needed on creation. Each call to shorten() is independent.
You: "Should we support custom vanity aliases like go.company.com/sale2026?"
Interviewer: "Yes, as an optional feature. If the user provides a custom alias, use it. Otherwise, generate one automatically."
Custom aliases mean we need to validate uniqueness for user-provided codes too. The same collision-check logic applies.
You: "Do short URLs expire? If so, is there a single global TTL or per-URL configuration?"
Interviewer: "Support multiple policies: no expiry, fixed TTL, and sliding window that resets on each click."
Multiple expiration strategies. That is a Strategy pattern. Each URL mapping holds a reference to its expiration policy, and the policy decides whether the mapping is still alive.
You: "What analytics do we need on each redirect? Just a count, or detailed click events?"
Interviewer: "Record each click with timestamp, referrer, user-agent, and anonymized IP. Provide aggregate stats like total clicks and clicks-per-day."
Per-click event recording plus aggregation. The redirect path must be fast, so we record the event and return the redirect immediately. Aggregation can happen lazily.
You: "Should we rate-limit URL creation to prevent abuse?"
Interviewer: "Yes. Limit by client ID. Reject with a clear error when the limit is exceeded."
Rate limiting on creation. We can inject a rate limiter into the service and check before generating a code.
You: "What is the maximum length for the generated short code?"
Interviewer: "Six characters using base62 encoding (a-z, A-Z, 0-9). That gives us 62^6, roughly 56.8 billion unique codes."
Six base62 characters. More than enough for any realistic workload. The encoding alphabet is [a-zA-Z0-9].
You: "Is this in-process only, or do we need distributed coordination for code generation?"
Interviewer: "Design for in-process. Mention distributed coordination as an extension."
In-process keeps the core simple. No need for distributed counters or locks.
Perfect. You have now clarified scope and ruled out unnecessary complexity.
Final Requirements
Functional Requirements:
shorten(longUrl)generates a unique 6-character base62 short code and stores the mappingshorten(longUrl, customAlias)uses a user-provided alias after validating uniquenessredirect(shortCode)returns the original URL, records a click event, and checks expiration- Support three code generation strategies: counter-based (base62 of atomic counter), hash-based (MD5/SHA256 with collision retry), and random (random string with uniqueness check)
- Support three expiration policies: no expiry, fixed TTL, and sliding window (resets on each access)
- Record click analytics per redirect: timestamp, referrer, user-agent, anonymized IP
- Rate-limit URL creation per client ID
Non-Functional Requirements:
- Thread-safe for concurrent shorten and redirect calls
- Sub-millisecond redirect latency on cache hits
- Pluggable code generation and expiration via Strategy pattern so new strategies require zero changes to existing code
Out of Scope:
- Distributed coordination (Zookeeper, Redis-backed counters)
- HTTP layer / REST controller implementation
- Persistent storage (database integration)
- URL validation beyond basic format checks
Interview tip: number your requirements
Writing numbered requirements on the whiteboard shows the interviewer you are methodical. It also gives you reference points: "Requirement 4 drives the Strategy pattern choice for code generation."
Example Inputs and Outputs
Scenario 1: Basic shortening and redirect
- Input:
shorten("https://example.com/very/long/path?param=value") - Expected: Returns
ShortenResult { shortCode: "a3Bf9K", shortUrl: "https://short.url/a3Bf9K" } - Redirect:
redirect("a3Bf9K")returns"https://example.com/very/long/path?param=value"and records a click event - Why: Validates the core happy path for both creation and redirect
Scenario 2: Custom alias with collision
- Input:
shorten("https://store.com/sale", "sale2026") - Expected: Returns
ShortenResult { shortCode: "sale2026", shortUrl: "https://short.url/sale2026" } - Input:
shorten("https://other.com/page", "sale2026") - Expected: Throws
AliasAlreadyExistsExceptionbecause "sale2026" is taken - Why: Validates custom alias uniqueness enforcement
Scenario 3: Expired URL redirect
- Setup: URL "abc123" created with a 1-hour fixed TTL, 2 hours ago
- Input:
redirect("abc123") - Expected: Throws
UrlExpiredExceptionbecause the TTL has elapsed - Why: Validates expiration policy enforcement on redirect
Try It Yourself
Try it yourself
Before reading the solution, spend 15-20 minutes sketching your own class diagram. Focus on how you would make code generation swappable and how expiration policies attach to individual URL mappings. Compare your approach with the walkthrough below.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Look for nouns in your requirements: URL, short code, mapping, code generator, click event, analytics, expiration policy, store, rate limiter. Each noun is a candidate entity with a single clear job.
A common mistake is stuffing everything into a single UrlShortener god class. That class would generate codes, store mappings, check expiration, record analytics, and enforce rate limits. It violates SRP immediately. Good design means each class has one reason to change.
| Entity | Responsibility | Key attributes |
|---|---|---|
UrlShortenerService | Orchestrator. Coordinates shortening, redirect, and analytics. | codeGenerator, store, rateLimiter |
UrlMapping | Stores one long-to-short mapping with metadata. | shortCode, longUrl, createdAt, expirationPolicy |
CodeGenerator | Strategy interface for generating short codes. | (interface) |
Base62Encoder | Utility that converts numbers to base62 strings and back. | alphabet |
ExpirationPolicy | Strategy interface that decides if a mapping is still alive. | (interface) |
ClickEvent | Value object recording a single redirect event. | shortCode, timestamp, referrer, userAgent, ipHash |
ClickAnalytics | Aggregates click events for a given short code. | totalClicks, clicksByDay |
UrlStore | Storage abstraction for URL mappings and click events. | mappings, clickEvents |
ShortenResult | Value object returned from shorten(). Holds the short code and full short URL. | shortCode, shortUrl |
Notice we separated CodeGenerator from UrlShortenerService because the generation algorithm is the most likely thing to change. Counter-based, hash-based, and random strategies each have completely different implementations. Merging them into the service violates OCP.
Step 2: Define Relationships and Class Design
UrlShortenerService
The orchestrator. It coordinates code generation, storage, expiration checks, and analytics. It does not contain any algorithm logic itself.
Deriving state from requirements:
| Requirement | What UrlShortenerService must hold |
|---|---|
| "Generate unique short codes" | A CodeGenerator strategy |
| "Store and retrieve mappings" | A UrlStore reference |
| "Rate-limit creation per client" | A rate limiter or rate check |
| "Record click analytics" | Handled via UrlStore.recordClick |
Deriving methods from needs:
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.