LLD for URL Shortener: Designing TinyURL
Why Base62 and Not MD5?
Approach A β Hashing (MD5/SHA): - Hash the long URL: MD5("https://...") = 3b94d2a8...
π― System Scenario & Requirements
System Goal
Approach A β Hashing (MD5/SHA): - Hash the long URL: MD5("https://...") = 3b94d2a8...
Actors & Components
- Edge Ingress
- Stateless Microservices
- Datastore & Coordination
- Web / Mobile Client
- API Gateway
- Creation Service
- Redirect Service
- ZooKeeper Coordinator
Functional Requirements
- Approach A β Hashing (MD5/SHA): - Hash the long URL: MD5("https://...") = 3b94d2a8...
- - Take first 7 chars: 3b94d2a .
- - Problem: Two different URLs might produce the same first 7 chars β collision β wrong redirect.
Constraints & Assumptions
- Users sometimes want custom codes: tiny.url/my-blog .
- Check if my-blog exists in the DB.
β‘ Execution Stages & Decision Trace
Stage 1 β The Locker Room Analogy
A locker room assigns locker numbers.
The attendant doesn't randomly pick numbers β she uses the next available sequential number.
Deep Dive
Scenario context
flowchart LR
Client -->|"shorten(longUrl)"| SVC[UrlShortenerService]
SVC -->|"save(longUrl)"| Store["CodeStore (RedisStore / InMemoryStore)"]
Store -->|shortCode| SVC
SVC -->|"encode(id)"| Encoder["UrlEncoder (Base62Encoder)"]
Encoder -->|"cb"| SVC
SVC -->|shortCode| Client
Observable outcome: Who implements each interface, who calls it, and what it abstracts: Runtime collaboration β how the components interact during a shorten() call: Structural relationships β ownership and implementation: This class diagraβ¦
Stage 4 β OOP Pillars Applied to the URL Shortener
- Each classical OOP pillar maps directly to a concrete design decision in this system.
- Encapsulation β Base62Encoder owns its alphabet The 62-character alphabet ( a-z A-Z 0-9 ) and the modular-arithmetic encoding loop are private implementation details inside Base62Encoder .
- Callers invoke encode(125) and receive "cb" β they never see the CHARS constant or the while (id 0) loop.
- Swapping the alphabet (say, to a URL-safe Base64 variant) requires editing exactly one class, nowhere else.
Observable outcome: Swapping the alphabet (say, to a URL-safe Base64 variant) requires editing exactly one class, nowhere else.
Stage 5 β SOLID Principles in the URL Shortener Design
- SOLID is not an abstract checklist β each principle appears as a specific, verifiable choice here.
- Single Responsibility (SRP) β Each class has exactly one reason to change.
- Base62Encoder changes only if the encoding algorithm changes.
- A CodeStore implementation changes only if the persistence technology changes.
public class UrlShortenerService {
private final UrlEncoder encoder;
private final CodeStore store;
private final AnalyticsTracker tracker;
public UrlShortenerService(UrlEncoder encoder, CodeStore store, AnalyticsTracker tracker) {
this.encoder = encoder;
this.store = store;
this.tracker = tracker;
}
}
Observable outcome: A CodeStore implementation changes only if the persistence technology changes.
Stage 6 β Interface Contracts: The Four Boundaries of the URL Shortener
- Each interface is a stable boundary.
- Anything inside that boundary can change freely; anything outside can only call the methods listed here.
- These four contracts are everything UrlShortenerService needs from the outside world.
- Any Spring @Bean that satisfies an interface can be wired in β production configs, test configs, and migration configs all swap implementations without modifying the service itself.
/**
* Converts a numeric counter ID into a compact short-code string.
* Implementations: Base62Encoder, CustomAliasEncoder, Base58Encoder
*/
interface UrlEncoder {
String encode(long id); // e.g. encode(125) β "cb"
}
/**
* Persists and retrieves short-code β long-URL mappings.
* Implementations: InMemoryStore (test), RedisStore (staging), DistributedStore (prod)
*/
interface CodeStore {
String save(String longUrl); // persists mapping, returns generated short code
Optional<String> resolve(String code); // returns long URL for code, or empty if not found
boolean exists(String code); // returns true if the code is already in use
}
/**
* Records analytics for every redirect event.
* Implementations: NoOpTracker (default), DatabaseTracker (analytics-on)
*/
interface AnalyticsTracker {
void recordClick(String code, String referrer, Instant timestamp);
}
/**
* Validates user-supplied custom aliases before storage.
* Implementations: CustomAliasValidator
*/
interface AliasValidator {
boolean isValid(String customAlias); // false if blank, reserved, non-alphanumeric, or taken
}
classDiagram
class UrlEncoder {
<>
+encode(id long) String
+decode(code String) long
}
class CodeStore {
<>
+save(longUrl String) String
+resolve(code String) Optional~String~
+exists(code String) boolean
}
class AnalyticsTracker {
<>
+recordClick(code String, referrer String, ts Instant) void
}
class AliasValidator {
<>
+isValid(customAlias String) boolean
}
class Base62Encoder {
-String CHARS
+encode(id long) String
+decode(code String) long
}
class CustomAliasEncoder {
+encode(id long) String
}
class InMemoryStore {
-Map~String,String~ store
+save(longUrl String) String
+resolve(code String) Optional~String~
+exists(code String) boolean
}
class RedisStore {
+save(longUrl String) String
+resolve(code String) Optional~String~
+exists(code String) boolean
}
class NoOpTracker {
+recordClick(code, referrer, ts) void
}
class DatabaseTracker {
+recordClick(code, referrer, ts) void
}
class CustomAliasValidator {
+isValid(customAlias String) boolean
}
class UrlShortenerService {
-UrlEncoder encoder
-CodeStore store
-AnalyticsTracker tracker
-AliasValidator validator
+shorten(longUrl String) String
+resolve(shortCode String) String
}
Base62Encoder ..|> UrlEncoder : implements
CustomAliasEncoder --|> Base62Encoder : extends
InMemoryStore ..|> CodeStore : implements
RedisStore ..|> CodeStore : implements
NoOpTracker ..|> AnalyticsTracker : implements
DatabaseTracker ..|> AnalyticsTracker : implements
CustomAliasValidator ..|> AliasValidator : implements
UrlShortenerService o-- UrlEncoder : uses (injected)
UrlShortenerService o-- CodeStore : uses (injected)
UrlShortenerService o-- AnalyticsTracker : uses (injected)
UrlShortenerService o-- AliasValidator : uses (injected)
Observable outcome: Any Spring @Bean that satisfies an interface can be wired in β production configs, test configs, and migration configs all swap implementations without modifying the service itself.
End-to-end outcome
Approach A β Hashing (MD5/SHA): - Hash the long URL: MD5("https://...") = 3b94d2a8...
Alternatives considered
Decision Guide: Choosing the Right URL Shortener Architecture
Start with a single DB + auto-increment.
Failure and degraded modes
Trade-offs & Failure Modes: Custom Aliases and Collision Handling
- Detection: Users sometimes want custom codes: tiny.url/my-blog .
- User or operator impact: Users sometimes want custom codes: tiny.url/my-blog .
