Skip to main content
Scenario Article

LLD for URL Shortener: Designing TinyURL

Why Base62 and Not MD5?

Abstract Algorithmsβ€’Mar 9, 2026
XLinkedInReddit
πŸ’‘ Executive Summary

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

1

Stage 1 β€” The Locker Room Analogy

Action / Decision:

A locker room assigns locker numbers.

Component Behavior:

The attendant doesn't randomly pick numbers β€” she uses the next available sequential number.

Observable Output: You get "Locker 5" β†’ you can find it instantly.
Read Full Stage in Deep Dive ↓
πŸ“–

Deep Dive

Detailed Architectural Analysis

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 .

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit