Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
โฑ๏ธ 25 min read

URL Shortener

Difficulty: Intermediate Patterns: Strategy, Repository, Factory Asked at: Amazon, Google, Microsoft, Uber, Flipkart


Functional Requirements

  1. Shorten URLs - generate short codes via Base62 encoding of atomic counter
  2. Custom aliases - register user-specified short codes with conflict checking
  3. Resolve URLs - redirect short code to long URL with click tracking
  4. URL expiration - TTL-based expiry with auto-cleanup on resolve
  5. Usage statistics - track click count, creation time, expiration status

Non-Functional Requirements

  1. Thread-safety - atomic counter increment and collision-free alias registration
  2. Extensibility - swappable encoding strategies without changing service logic
  3. Uniqueness - short codes are guaranteed unique (counter-based or collision-checked)
  4. Validation - URL format and alias constraints enforced at the service boundary

Core Entities

Entity Description
UrlMapping Domain model - short code, long URL, user, click count, timestamps, expiration
UrlStats Read-only stats snapshot - code, URL, clicks, creation time, expiry status
EncodingStrategy Strategy interface - converts a numeric ID into a short code string
Base62Strategy Deterministic encoding - maps counter value to compact Base62 string
RandomStrategy Non-deterministic encoding - generates random alphanumeric codes
UrlRepository Repository interface - abstracts storage (save, find, exists, delete)
UrlShortenerService Core service - atomic counter, encoding, alias registration, resolution
AliasAlreadyExistsException Thrown when a custom alias is already taken
UrlNotFoundException Thrown when a short code has no mapping
UrlExpiredException Thrown when a resolved URL has passed its TTL

Class Diagram

classDiagram
    class UrlMapping {
        -String shortCode
        -String longUrl
        -String userId
        -long clickCount
        -LocalDateTime createdAt
        -LocalDateTime expiresAt
        +incrementClicks()
        +isExpired() boolean
        +getClickCount() long
    }

    class EncodingStrategy {
        <<interface>>
        +encode(long id) String
    }

    class Base62Strategy {
        -String ALPHABET
        +encode(long id) String
    }

    class RandomStrategy {
        -int length
        -SecureRandom random
        +encode(long id) String
    }

    class UrlRepository {
        <<interface>>
        +save(UrlMapping)
        +findByShortCode(String) UrlMapping
        +existsByShortCode(String) boolean
        +delete(String)
    }

    class InMemoryUrlRepository {
        -ConcurrentHashMap store
        +save(UrlMapping)
        +findByShortCode(String) UrlMapping
        +existsByShortCode(String) boolean
        +delete(String)
    }

    class UrlShortenerService {
        -AtomicLong counter
        -UrlRepository repository
        -EncodingStrategy strategy
        -ReentrantLock aliasLock
        +shorten(String longUrl) String
        +shortenWithAlias(String longUrl and String alias) String
        +resolve(String shortCode) String
        +delete(String shortCode)
        +getStats(String shortCode) UrlStats
    }

    class UrlStats {
        -String shortCode
        -String longUrl
        -long clickCount
        -LocalDateTime createdAt
        -LocalDateTime expiresAt
        -boolean expired
    }

    class AliasAlreadyExistsException {
        -String alias
    }

    class UrlNotFoundException {
        -String shortCode
    }

    class UrlExpiredException {
        -String shortCode
    }

    UrlShortenerService --> UrlRepository
    UrlShortenerService --> EncodingStrategy
    UrlRepository <|.. InMemoryUrlRepository
    EncodingStrategy <|.. Base62Strategy
    EncodingStrategy <|.. RandomStrategy
    InMemoryUrlRepository --> UrlMapping
    UrlShortenerService --> UrlStats

Design Patterns

Pattern Where Why
Strategy EncodingStrategy with Base62/Random Swap encoding algorithm without changing service logic; add new strategies without modifying existing code
Repository UrlRepository with InMemory impl Abstracts storage - swap to Redis or DynamoDB for production without touching business logic
Factory UrlMapping creation in service Centralized initialization of timestamps, click counts, and expiration ensures consistency

How It All Fits Together

Hereโ€™s what happens when a client shortens a URL:

  1. Client calls shorten(longUrl) on UrlShortenerService
  2. Service validates the URL format (must start with http/https)
  3. Atomically increments the counter to get a unique ID
  4. Delegates to the EncodingStrategy to convert the ID to a short code
  5. Checks for collision (only possible with RandomStrategy); retries if collision found
  6. Creates a UrlMapping with the short code, long URL, timestamps, and TTL
  7. Persists to the repository and returns the short code

๐Ÿ’ก With Base62Strategy, collisions are impossible because the counter is monotonically increasing and the encoding is bijective. The collision loop only activates for RandomStrategy where two different IDs could produce the same random string.


Complete Code

UrlMapping and UrlStats

UrlMapping is the core domain entity representing a shortened URL. It tracks the short code, original URL, click count (atomically incremented), and expiration. UrlStats is a read-only snapshot for reporting.

import java.util.concurrent.atomic.AtomicLong;
import java.time.LocalDateTime;

class UrlMapping {
    private final String shortCode;
    private final String longUrl;
    private final String userId;
    private final AtomicLong clickCount;
    private final LocalDateTime createdAt;
    private final LocalDateTime expiresAt;

    public UrlMapping(String shortCode, String longUrl, String userId, LocalDateTime expiresAt) {
        this.shortCode = shortCode;
        this.longUrl = longUrl;
        this.userId = userId;
        this.clickCount = new AtomicLong(0);
        this.createdAt = LocalDateTime.now();
        this.expiresAt = expiresAt;
    }

    public void incrementClicks() {
        clickCount.incrementAndGet();
    }

    public boolean isExpired() {
        return expiresAt != null && LocalDateTime.now().isAfter(expiresAt);
    }

    public String getShortCode() { return shortCode; }
    public String getLongUrl() { return longUrl; }
    public String getUserId() { return userId; }
    public long getClickCount() { return clickCount.get(); }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public LocalDateTime getExpiresAt() { return expiresAt; }
}

class UrlStats {
    private final String shortCode;
    private final String longUrl;
    private final long clickCount;
    private final LocalDateTime createdAt;
    private final LocalDateTime expiresAt;
    private final boolean expired;

    public UrlStats(UrlMapping mapping) {
        this.shortCode = mapping.getShortCode();
        this.longUrl = mapping.getLongUrl();
        this.clickCount = mapping.getClickCount();
        this.createdAt = mapping.getCreatedAt();
        this.expiresAt = mapping.getExpiresAt();
        this.expired = mapping.isExpired();
    }

    @Override
    public String toString() {
        return "UrlStats{code=" + shortCode + " url=" + longUrl
               + " clicks=" + clickCount + " expired=" + expired + "}";
    }
}
import threading
from dataclasses import dataclass
from datetime import datetime
from typing import Optional


class UrlMapping:
    def __init__(self, short_code: str, long_url: str, user_id: Optional[str],
                 expires_at: Optional[datetime]):
        self.short_code = short_code
        self.long_url = long_url
        self.user_id = user_id
        self._click_count = 0
        self._click_lock = threading.Lock()
        self.created_at = datetime.now()
        self.expires_at = expires_at

    def increment_clicks(self) -> None:
        with self._click_lock:
            self._click_count += 1

    def is_expired(self) -> bool:
        return self.expires_at is not None and datetime.now() > self.expires_at

    @property
    def click_count(self) -> int:
        with self._click_lock:
            return self._click_count


@dataclass
class UrlStats:
    short_code: str
    long_url: str
    click_count: int
    created_at: datetime
    expires_at: Optional[datetime]
    expired: bool

    def __str__(self) -> str:
        return (f"UrlStats{{code={self.short_code} url={self.long_url} "
                f"clicks={self.click_count} expired={self.expired}}}")
#include <string>
#include <atomic>
#include <chrono>

using Clock = std::chrono::system_clock;
using TimePoint = std::chrono::time_point<Clock>;

class UrlMapping {
private:
    std::string shortCode_;
    std::string longUrl_;
    std::string userId_;
    std::atomic<long> clickCount_;
    TimePoint createdAt_;
    TimePoint expiresAt_;

public:
    UrlMapping(const std::string& shortCode, const std::string& longUrl,
               const std::string& userId, TimePoint expiresAt)
        : shortCode_(shortCode), longUrl_(longUrl), userId_(userId),
          clickCount_(0), createdAt_(Clock::now()), expiresAt_(expiresAt) {}

    void incrementClicks() { clickCount_.fetch_add(1); }
    bool isExpired() const { return Clock::now() > expiresAt_; }

    const std::string& getShortCode() const { return shortCode_; }
    const std::string& getLongUrl() const { return longUrl_; }
    long getClickCount() const { return clickCount_.load(); }
    TimePoint getCreatedAt() const { return createdAt_; }
    TimePoint getExpiresAt() const { return expiresAt_; }
};
class UrlMapping {
    #shortCode; #longUrl; #userId; #clickCount; #createdAt; #expiresAt;

    constructor(shortCode, longUrl, userId, expiresAt) {
        this.#shortCode = shortCode;
        this.#longUrl = longUrl;
        this.#userId = userId;
        this.#clickCount = 0;
        this.#createdAt = new Date();
        this.#expiresAt = expiresAt;
    }

    incrementClicks() { this.#clickCount++; }
    isExpired() { return this.#expiresAt && new Date() > this.#expiresAt; }

    get shortCode() { return this.#shortCode; }
    get longUrl() { return this.#longUrl; }
    get userId() { return this.#userId; }
    get clickCount() { return this.#clickCount; }
    get createdAt() { return this.#createdAt; }
    get expiresAt() { return this.#expiresAt; }
}

Custom Exceptions

Domain-specific exceptions provide clear error semantics for alias conflicts, missing URLs, and expired URLs. Each carries the offending identifier for debugging.

class AliasAlreadyExistsException extends RuntimeException {
    public AliasAlreadyExistsException(String alias) {
        super("Alias already taken: " + alias);
    }
}

class UrlNotFoundException extends RuntimeException {
    public UrlNotFoundException(String shortCode) {
        super("URL not found for code: " + shortCode);
    }
}

class UrlExpiredException extends RuntimeException {
    public UrlExpiredException(String shortCode) {
        super("URL has expired: " + shortCode);
    }
}
class AliasAlreadyExistsError(Exception):
    def __init__(self, alias: str):
        super().__init__(f"Alias already taken: {alias}")


class UrlNotFoundError(Exception):
    def __init__(self, short_code: str):
        super().__init__(f"URL not found for code: {short_code}")


class UrlExpiredError(Exception):
    def __init__(self, short_code: str):
        super().__init__(f"URL has expired: {short_code}")
#include <stdexcept>

class AliasAlreadyExistsException : public std::runtime_error {
public:
    AliasAlreadyExistsException(const std::string& alias)
        : std::runtime_error("Alias already taken: " + alias) {}
};

class UrlNotFoundException : public std::runtime_error {
public:
    UrlNotFoundException(const std::string& shortCode)
        : std::runtime_error("URL not found for code: " + shortCode) {}
};

class UrlExpiredException : public std::runtime_error {
public:
    UrlExpiredException(const std::string& shortCode)
        : std::runtime_error("URL has expired: " + shortCode) {}
};
class AliasAlreadyExistsError extends Error {
    constructor(alias) {
        super(`Alias already taken: ${alias}`);
        this.name = 'AliasAlreadyExistsError';
    }
}

class UrlNotFoundError extends Error {
    constructor(shortCode) {
        super(`URL not found for code: ${shortCode}`);
        this.name = 'UrlNotFoundError';
    }
}

class UrlExpiredError extends Error {
    constructor(shortCode) {
        super(`URL has expired: ${shortCode}`);
        this.name = 'UrlExpiredError';
    }
}

Encoding Strategy

The strategy interface and its implementations. Base62Strategy provides deterministic, compact encoding. RandomStrategy generates unpredictable codes using secure randomness.

๐Ÿ’ก Strategy pattern = define a family of algorithms, encapsulate each one, and make them interchangeable. The service calls strategy.encode(id) without knowing which algorithm is active. Adding a new encoding (e.g., HashStrategy) requires zero changes to UrlShortenerService.

import java.security.SecureRandom;

interface EncodingStrategy {
    String encode(long id);
}

class Base62Strategy implements EncodingStrategy {
    private static final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    @Override
    public String encode(long id) {
        if (id == 0) return String.valueOf(ALPHABET.charAt(0));
        StringBuilder sb = new StringBuilder();
        while (id > 0) {
            sb.append(ALPHABET.charAt((int)(id % 62)));
            id /= 62;
        }
        return sb.reverse().toString();
    }
}

class RandomStrategy implements EncodingStrategy {
    private static final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    private final int length;
    private final SecureRandom random = new SecureRandom();

    public RandomStrategy(int length) {
        this.length = length;
    }

    @Override
    public String encode(long id) {
        StringBuilder sb = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            sb.append(ALPHABET.charAt(random.nextInt(ALPHABET.length())));
        }
        return sb.toString();
    }
}
from abc import ABC, abstractmethod
import secrets
import string


class EncodingStrategy(ABC):
    @abstractmethod
    def encode(self, id_val: int) -> str:
        pass


class Base62Strategy(EncodingStrategy):
    ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

    def encode(self, id_val: int) -> str:
        if id_val == 0:
            return self.ALPHABET[0]
        result = []
        while id_val > 0:
            result.append(self.ALPHABET[id_val % 62])
            id_val //= 62
        return "".join(reversed(result))


class RandomStrategy(EncodingStrategy):
    def __init__(self, length: int = 8):
        self._length = length
        self._alphabet = string.ascii_letters + string.digits

    def encode(self, id_val: int) -> str:
        return "".join(secrets.choice(self._alphabet) for _ in range(self._length))
#include <string>
#include <random>

class EncodingStrategy {
public:
    virtual ~EncodingStrategy() = default;
    virtual std::string encode(long id) = 0;
};

class Base62Strategy : public EncodingStrategy {
    static constexpr const char* ALPHABET =
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
public:
    std::string encode(long id) override {
        if (id == 0) return std::string(1, ALPHABET[0]);
        std::string result;
        while (id > 0) {
            result = ALPHABET[id % 62] + result;
            id /= 62;
        }
        return result;
    }
};

class RandomStrategy : public EncodingStrategy {
    int length_;
    std::mt19937 rng_;
    static constexpr const char* ALPHABET =
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
public:
    RandomStrategy(int length) : length_(length), rng_(std::random_device{}()) {}

    std::string encode(long id) override {
        std::string result(length_, ' ');
        std::uniform_int_distribution<int> dist(0, 61);
        for (int i = 0; i < length_; ++i) {
            result[i] = ALPHABET[dist(rng_)];
        }
        return result;
    }
};
class Base62Strategy {
    #alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

    encode(id) {
        if (id === 0) return this.#alphabet[0];
        let result = '';
        while (id > 0) {
            result = this.#alphabet[id % 62] + result;
            id = Math.floor(id / 62);
        }
        return result;
    }
}

class RandomStrategy {
    #length;
    #alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

    constructor(length = 8) { this.#length = length; }

    encode(id) {
        let result = '';
        for (let i = 0; i < this.#length; i++) {
            result += this.#alphabet[Math.floor(Math.random() * this.#alphabet.length)];
        }
        return result;
    }
}

UrlRepository

The repository interface and its in-memory implementation. Abstracts all storage operations behind a clean interface so the service never knows whether itโ€™s talking to a HashMap, Redis, or DynamoDB.

๐Ÿ’ก Repository pattern = swap storage without touching business logic. For production, implement UrlRepository backed by Redis (for low-latency lookups) or DynamoDB (for durability) โ€” the service code stays identical.

import java.util.concurrent.ConcurrentHashMap;

interface UrlRepository {
    void save(UrlMapping mapping);
    UrlMapping findByShortCode(String shortCode);
    boolean existsByShortCode(String shortCode);
    void delete(String shortCode);
}

class InMemoryUrlRepository implements UrlRepository {
    private final ConcurrentHashMap<String, UrlMapping> store = new ConcurrentHashMap<>();

    @Override
    public void save(UrlMapping mapping) {
        store.put(mapping.getShortCode(), mapping);
    }

    @Override
    public UrlMapping findByShortCode(String shortCode) {
        return store.get(shortCode);
    }

    @Override
    public boolean existsByShortCode(String shortCode) {
        return store.containsKey(shortCode);
    }

    @Override
    public void delete(String shortCode) {
        store.remove(shortCode);
    }
}
class UrlRepository:
    def __init__(self):
        self._store: dict[str, UrlMapping] = {}
        self._lock = threading.Lock()

    def save(self, mapping: UrlMapping) -> None:
        with self._lock:
            self._store[mapping.short_code] = mapping

    def find_by_short_code(self, short_code: str) -> Optional[UrlMapping]:
        return self._store.get(short_code)

    def exists_by_short_code(self, short_code: str) -> bool:
        return short_code in self._store

    def delete(self, short_code: str) -> None:
        with self._lock:
            self._store.pop(short_code, None)
#include <unordered_map>
#include <mutex>
#include <memory>

// In the C++ version, the repository is embedded in the service
// using an unordered_map with mutex protection.
// For a standalone repository interface:

class UrlRepositoryBase {
public:
    virtual ~UrlRepositoryBase() = default;
    virtual void save(std::unique_ptr<UrlMapping> mapping) = 0;
    virtual UrlMapping* findByShortCode(const std::string& shortCode) = 0;
    virtual bool existsByShortCode(const std::string& shortCode) = 0;
    virtual void remove(const std::string& shortCode) = 0;
};
class UrlRepository {
    #store = new Map();
    save(mapping) { this.#store.set(mapping.shortCode, mapping); }
    findByShortCode(code) { return this.#store.get(code) || null; }
    existsByShortCode(code) { return this.#store.has(code); }
    delete(code) { this.#store.delete(code); }
}

UrlShortenerService

The main service orchestrating URL shortening, resolution, deletion, and statistics. Uses an atomic counter for ID generation, delegates encoding to the strategy, and protects alias registration with a lock.

๐Ÿ’ก AtomicLong guarantees that no two threads ever get the same counter value โ€” even under heavy concurrency. Combined with Base62โ€™s bijective mapping, this makes short code collisions impossible without any locking on the hot path.

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.time.LocalDateTime;

class UrlShortenerService {
    private final AtomicLong counter;
    private final UrlRepository repository;
    private EncodingStrategy strategy;
    private final ReentrantLock aliasLock = new ReentrantLock();
    private final long defaultTtlHours;

    public UrlShortenerService(UrlRepository repository, EncodingStrategy strategy,
                               long startCounter, long defaultTtlHours) {
        this.repository = repository;
        this.strategy = strategy;
        this.counter = new AtomicLong(startCounter);
        this.defaultTtlHours = defaultTtlHours;
    }

    public void setEncodingStrategy(EncodingStrategy strategy) {
        this.strategy = strategy;
    }

    public String shorten(String longUrl) {
        return shorten(longUrl, null, null);
    }

    public String shorten(String longUrl, String userId) {
        return shorten(longUrl, userId, null);
    }

    public String shortenWithAlias(String longUrl, String customAlias) {
        return shortenWithAlias(longUrl, customAlias, null);
    }

    public String shortenWithAlias(String longUrl, String customAlias, String userId) {
        validateUrl(longUrl);
        validateAlias(customAlias);

        aliasLock.lock();
        try {
            if (repository.existsByShortCode(customAlias)) {
                throw new AliasAlreadyExistsException(customAlias);
            }
            LocalDateTime expiresAt = LocalDateTime.now().plusHours(defaultTtlHours);
            UrlMapping mapping = new UrlMapping(customAlias, longUrl, userId, expiresAt);
            repository.save(mapping);
            return customAlias;
        } finally {
            aliasLock.unlock();
        }
    }

    public String resolve(String shortCode) {
        UrlMapping mapping = repository.findByShortCode(shortCode);
        if (mapping == null) {
            throw new UrlNotFoundException(shortCode);
        }
        if (mapping.isExpired()) {
            repository.delete(shortCode);
            throw new UrlExpiredException(shortCode);
        }
        mapping.incrementClicks();
        return mapping.getLongUrl();
    }

    public void delete(String shortCode) {
        if (!repository.existsByShortCode(shortCode)) {
            throw new UrlNotFoundException(shortCode);
        }
        repository.delete(shortCode);
    }

    public UrlStats getStats(String shortCode) {
        UrlMapping mapping = repository.findByShortCode(shortCode);
        if (mapping == null) {
            throw new UrlNotFoundException(shortCode);
        }
        return new UrlStats(mapping);
    }

    private String shorten(String longUrl, String userId, LocalDateTime expiresAt) {
        validateUrl(longUrl);
        if (expiresAt == null) {
            expiresAt = LocalDateTime.now().plusHours(defaultTtlHours);
        }

        long id = counter.incrementAndGet();
        String shortCode = strategy.encode(id);

        // Handle potential collision for random strategy
        aliasLock.lock();
        try {
            while (repository.existsByShortCode(shortCode)) {
                id = counter.incrementAndGet();
                shortCode = strategy.encode(id);
            }
            UrlMapping mapping = new UrlMapping(shortCode, longUrl, userId, expiresAt);
            repository.save(mapping);
        } finally {
            aliasLock.unlock();
        }
        return shortCode;
    }

    private void validateUrl(String url) {
        if (url == null || url.isBlank()) {
            throw new IllegalArgumentException("URL cannot be empty");
        }
        if (!url.startsWith("http://") && !url.startsWith("https://")) {
            throw new IllegalArgumentException("URL must start with http:// or https://");
        }
    }

    private void validateAlias(String alias) {
        if (alias == null || alias.isBlank()) {
            throw new IllegalArgumentException("Alias cannot be empty");
        }
        if (alias.length() < 3 || alias.length() > 20) {
            throw new IllegalArgumentException("Alias must be 3-20 characters");
        }
        if (!alias.matches("[a-zA-Z0-9_-]+")) {
            throw new IllegalArgumentException("Alias must be alphanumeric with hyphens and underscores only");
        }
    }
}
from datetime import timedelta
import re


class UrlShortenerService:
    def __init__(self, repository: UrlRepository, strategy: EncodingStrategy,
                 start_counter: int = 10000, default_ttl_hours: int = 24):
        self._repository = repository
        self._strategy = strategy
        self._counter = start_counter
        self._counter_lock = threading.Lock()
        self._alias_lock = threading.Lock()
        self._default_ttl_hours = default_ttl_hours

    def set_encoding_strategy(self, strategy: EncodingStrategy) -> None:
        self._strategy = strategy

    def shorten(self, long_url: str, user_id: Optional[str] = None) -> str:
        self._validate_url(long_url)
        expires_at = datetime.now() + timedelta(hours=self._default_ttl_hours)

        with self._counter_lock:
            self._counter += 1
            current_id = self._counter

        short_code = self._strategy.encode(current_id)

        with self._alias_lock:
            while self._repository.exists_by_short_code(short_code):
                with self._counter_lock:
                    self._counter += 1
                    current_id = self._counter
                short_code = self._strategy.encode(current_id)

            mapping = UrlMapping(short_code, long_url, user_id, expires_at)
            self._repository.save(mapping)

        return short_code

    def shorten_with_alias(self, long_url: str, custom_alias: str,
                           user_id: Optional[str] = None) -> str:
        self._validate_url(long_url)
        self._validate_alias(custom_alias)

        with self._alias_lock:
            if self._repository.exists_by_short_code(custom_alias):
                raise AliasAlreadyExistsError(custom_alias)

            expires_at = datetime.now() + timedelta(hours=self._default_ttl_hours)
            mapping = UrlMapping(custom_alias, long_url, user_id, expires_at)
            self._repository.save(mapping)

        return custom_alias

    def resolve(self, short_code: str) -> str:
        mapping = self._repository.find_by_short_code(short_code)
        if mapping is None:
            raise UrlNotFoundError(short_code)
        if mapping.is_expired():
            self._repository.delete(short_code)
            raise UrlExpiredError(short_code)
        mapping.increment_clicks()
        return mapping.long_url

    def delete(self, short_code: str) -> None:
        if not self._repository.exists_by_short_code(short_code):
            raise UrlNotFoundError(short_code)
        self._repository.delete(short_code)

    def get_stats(self, short_code: str) -> UrlStats:
        mapping = self._repository.find_by_short_code(short_code)
        if mapping is None:
            raise UrlNotFoundError(short_code)
        return UrlStats(
            short_code=mapping.short_code,
            long_url=mapping.long_url,
            click_count=mapping.click_count,
            created_at=mapping.created_at,
            expires_at=mapping.expires_at,
            expired=mapping.is_expired()
        )

    def _validate_url(self, url: str) -> None:
        if not url or not url.strip():
            raise ValueError("URL cannot be empty")
        if not url.startswith("http://") and not url.startswith("https://"):
            raise ValueError("URL must start with http:// or https://")

    def _validate_alias(self, alias: str) -> None:
        if not alias or not alias.strip():
            raise ValueError("Alias cannot be empty")
        if len(alias) < 3 or len(alias) > 20:
            raise ValueError("Alias must be 3-20 characters")
        if not re.match(r'^[a-zA-Z0-9_-]+$', alias):
            raise ValueError("Alias must be alphanumeric with hyphens and underscores only")
class UrlShortenerService {
private:
    std::atomic<long> counter_;
    std::unordered_map<std::string, std::unique_ptr<UrlMapping>> store_;
    EncodingStrategy* strategy_;
    std::mutex mtx_;
    int defaultTtlHours_;

    void validateUrl(const std::string& url) {
        if (url.empty()) throw std::invalid_argument("URL cannot be empty");
        if (url.substr(0, 7) != "http://" && url.substr(0, 8) != "https://")
            throw std::invalid_argument("URL must start with http:// or https://");
    }

    void validateAlias(const std::string& alias) {
        if (alias.empty()) throw std::invalid_argument("Alias cannot be empty");
        if (alias.length() < 3 || alias.length() > 20)
            throw std::invalid_argument("Alias must be 3-20 characters");
        std::regex pattern("^[a-zA-Z0-9_-]+$");
        if (!std::regex_match(alias, pattern))
            throw std::invalid_argument("Alias must be alphanumeric with hyphens and underscores");
    }

public:
    UrlShortenerService(EncodingStrategy* strategy, long startCounter = 10000,
                        int defaultTtlHours = 24)
        : counter_(startCounter), strategy_(strategy), defaultTtlHours_(defaultTtlHours) {}

    void setEncodingStrategy(EncodingStrategy* s) { strategy_ = s; }

    std::string shorten(const std::string& longUrl) {
        validateUrl(longUrl);
        auto expiresAt = Clock::now() + std::chrono::hours(defaultTtlHours_);

        long id = counter_.fetch_add(1) + 1;
        std::string shortCode = strategy_->encode(id);

        std::lock_guard<std::mutex> guard(mtx_);
        while (store_.count(shortCode)) {
            id = counter_.fetch_add(1) + 1;
            shortCode = strategy_->encode(id);
        }
        store_[shortCode] = std::make_unique<UrlMapping>(shortCode, longUrl, "", expiresAt);
        return shortCode;
    }

    std::string shortenWithAlias(const std::string& longUrl, const std::string& alias) {
        validateUrl(longUrl);
        validateAlias(alias);

        std::lock_guard<std::mutex> guard(mtx_);
        if (store_.count(alias)) throw AliasAlreadyExistsException(alias);

        auto expiresAt = Clock::now() + std::chrono::hours(defaultTtlHours_);
        store_[alias] = std::make_unique<UrlMapping>(alias, longUrl, "", expiresAt);
        return alias;
    }

    std::string resolve(const std::string& shortCode) {
        std::lock_guard<std::mutex> guard(mtx_);
        auto it = store_.find(shortCode);
        if (it == store_.end()) throw UrlNotFoundException(shortCode);
        if (it->second->isExpired()) {
            store_.erase(it);
            throw UrlExpiredException(shortCode);
        }
        it->second->incrementClicks();
        return it->second->getLongUrl();
    }

    void deleteUrl(const std::string& shortCode) {
        std::lock_guard<std::mutex> guard(mtx_);
        if (!store_.count(shortCode)) throw UrlNotFoundException(shortCode);
        store_.erase(shortCode);
    }

    long getClickCount(const std::string& shortCode) {
        std::lock_guard<std::mutex> guard(mtx_);
        auto it = store_.find(shortCode);
        if (it == store_.end()) throw UrlNotFoundException(shortCode);
        return it->second->getClickCount();
    }
};
// JS is single-threaded; operations are atomic within a single event loop tick.
// For async patterns, use sequential await or a mutex library.

class UrlShortenerService {
    #counter;
    #repository;
    #strategy;
    #defaultTtlMs;

    constructor(repository, strategy, startCounter = 10000, defaultTtlHours = 24) {
        this.#repository = repository;
        this.#strategy = strategy;
        this.#counter = startCounter;
        this.#defaultTtlMs = defaultTtlHours * 60 * 60 * 1000;
    }

    setEncodingStrategy(strategy) { this.#strategy = strategy; }

    shorten(longUrl, userId = null) {
        this.#validateUrl(longUrl);
        const expiresAt = new Date(Date.now() + this.#defaultTtlMs);

        this.#counter++;
        let shortCode = this.#strategy.encode(this.#counter);

        // Handle collision
        while (this.#repository.existsByShortCode(shortCode)) {
            this.#counter++;
            shortCode = this.#strategy.encode(this.#counter);
        }

        const mapping = new UrlMapping(shortCode, longUrl, userId, expiresAt);
        this.#repository.save(mapping);
        return shortCode;
    }

    shortenWithAlias(longUrl, customAlias, userId = null) {
        this.#validateUrl(longUrl);
        this.#validateAlias(customAlias);

        if (this.#repository.existsByShortCode(customAlias)) {
            throw new AliasAlreadyExistsError(customAlias);
        }

        const expiresAt = new Date(Date.now() + this.#defaultTtlMs);
        const mapping = new UrlMapping(customAlias, longUrl, userId, expiresAt);
        this.#repository.save(mapping);
        return customAlias;
    }

    resolve(shortCode) {
        const mapping = this.#repository.findByShortCode(shortCode);
        if (!mapping) throw new UrlNotFoundError(shortCode);
        if (mapping.isExpired()) {
            this.#repository.delete(shortCode);
            throw new UrlExpiredError(shortCode);
        }
        mapping.incrementClicks();
        return mapping.longUrl;
    }

    delete(shortCode) {
        if (!this.#repository.existsByShortCode(shortCode)) {
            throw new UrlNotFoundError(shortCode);
        }
        this.#repository.delete(shortCode);
    }

    getStats(shortCode) {
        const mapping = this.#repository.findByShortCode(shortCode);
        if (!mapping) throw new UrlNotFoundError(shortCode);
        return {
            shortCode: mapping.shortCode,
            longUrl: mapping.longUrl,
            clickCount: mapping.clickCount,
            createdAt: mapping.createdAt,
            expiresAt: mapping.expiresAt,
            expired: mapping.isExpired(),
            toString() {
                return `UrlStats{code=${this.shortCode} url=${this.longUrl} ` +
                       `clicks=${this.clickCount} expired=${this.expired}}`;
            }
        };
    }

    #validateUrl(url) {
        if (!url || !url.trim()) throw new Error('URL cannot be empty');
        if (!url.startsWith('http://') && !url.startsWith('https://')) {
            throw new Error('URL must start with http:// or https://');
        }
    }

    #validateAlias(alias) {
        if (!alias || !alias.trim()) throw new Error('Alias cannot be empty');
        if (alias.length < 3 || alias.length > 20)
            throw new Error('Alias must be 3-20 characters');
        if (!/^[a-zA-Z0-9_-]+$/.test(alias))
            throw new Error('Alias must be alphanumeric with hyphens and underscores only');
    }
}

Main / Demo

End-to-end demonstration exercising all features: basic shortening, resolution, click tracking, custom aliases, duplicate detection, deletion, strategy switching, and concurrent access.

import java.util.*;
import java.util.concurrent.*;

public class URLShortenerDemo {
    public static void main(String[] args) {
        UrlRepository repository = new InMemoryUrlRepository();
        EncodingStrategy base62 = new Base62Strategy();
        UrlShortenerService service = new UrlShortenerService(repository, base62, 10000, 24);

        // Basic shortening
        System.out.println("=== Shorten URLs ===");
        String code1 = service.shorten("https://www.example.com/very/long/path/to/resource");
        String code2 = service.shorten("https://docs.oracle.com/javase/tutorial");
        String code3 = service.shorten("https://github.com/user/repo/pull/123");
        System.out.println("Shortened: " + code1);
        System.out.println("Shortened: " + code2);
        System.out.println("Shortened: " + code3);

        // Resolve
        System.out.println("\n=== Resolve URLs ===");
        System.out.println(code1 + " -> " + service.resolve(code1));
        System.out.println(code2 + " -> " + service.resolve(code2));

        // Multiple resolves to increment clicks
        service.resolve(code1);
        service.resolve(code1);

        // Stats
        System.out.println("\n=== URL Stats ===");
        System.out.println(service.getStats(code1));

        // Custom alias
        System.out.println("\n=== Custom Alias ===");
        String alias = service.shortenWithAlias("https://myportfolio.com", "my-site");
        System.out.println("Custom alias: " + alias);
        System.out.println("Resolves to: " + service.resolve(alias));

        // Duplicate alias
        System.out.println("\n=== Duplicate Alias ===");
        try {
            service.shortenWithAlias("https://another.com", "my-site");
        } catch (AliasAlreadyExistsException e) {
            System.out.println("Caught: " + e.getMessage());
        }

        // Delete
        System.out.println("\n=== Delete URL ===");
        service.delete(code3);
        try {
            service.resolve(code3);
        } catch (UrlNotFoundException e) {
            System.out.println("Caught: " + e.getMessage());
        }

        // Not found
        System.out.println("\n=== Not Found ===");
        try {
            service.resolve("nonexistent");
        } catch (UrlNotFoundException e) {
            System.out.println("Caught: " + e.getMessage());
        }

        // Switch to random strategy
        System.out.println("\n=== Random Strategy ===");
        service.setEncodingStrategy(new RandomStrategy(8));
        String randomCode = service.shorten("https://random-strategy.example.com");
        System.out.println("Random code: " + randomCode);
        System.out.println("Resolves to: " + service.resolve(randomCode));

        // Concurrent click counting
        System.out.println("\n=== Concurrent Click Test ===");
        String concurrentCode = service.shorten("https://popular-page.com");
        ExecutorService executor = Executors.newFixedThreadPool(10);
        List<Future<?>> futures = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            futures.add(executor.submit(() -> service.resolve(concurrentCode)));
        }
        for (Future<?> f : futures) {
            try { f.get(); } catch (Exception ignored) {}
        }
        executor.shutdown();
        System.out.println("After 1000 concurrent resolves: " + service.getStats(concurrentCode));
        System.out.println("Expected click count: 1000");
    }
}
def main():
    repository = UrlRepository()
    base62 = Base62Strategy()
    service = UrlShortenerService(repository, base62, start_counter=10000, default_ttl_hours=24)

    # Basic shortening
    print("=== Shorten URLs ===")
    code1 = service.shorten("https://www.example.com/very/long/path/to/resource")
    code2 = service.shorten("https://docs.python.org/3/tutorial")
    code3 = service.shorten("https://github.com/user/repo/pull/123")
    print(f"Shortened: {code1}")
    print(f"Shortened: {code2}")
    print(f"Shortened: {code3}")

    # Resolve
    print("\n=== Resolve URLs ===")
    print(f"{code1} -> {service.resolve(code1)}")
    print(f"{code2} -> {service.resolve(code2)}")

    # Multiple resolves
    service.resolve(code1)
    service.resolve(code1)

    # Stats
    print("\n=== URL Stats ===")
    print(service.get_stats(code1))

    # Custom alias
    print("\n=== Custom Alias ===")
    alias = service.shorten_with_alias("https://myportfolio.com", "my-site")
    print(f"Custom alias: {alias}")
    print(f"Resolves to: {service.resolve(alias)}")

    # Duplicate alias
    print("\n=== Duplicate Alias ===")
    try:
        service.shorten_with_alias("https://another.com", "my-site")
    except AliasAlreadyExistsError as e:
        print(f"Caught: {e}")

    # Delete
    print("\n=== Delete URL ===")
    service.delete(code3)
    try:
        service.resolve(code3)
    except UrlNotFoundError as e:
        print(f"Caught: {e}")

    # Not found
    print("\n=== Not Found ===")
    try:
        service.resolve("nonexistent")
    except UrlNotFoundError as e:
        print(f"Caught: {e}")

    # Switch strategy
    print("\n=== Random Strategy ===")
    service.set_encoding_strategy(RandomStrategy(8))
    random_code = service.shorten("https://random-strategy.example.com")
    print(f"Random code: {random_code}")
    print(f"Resolves to: {service.resolve(random_code)}")

    # Concurrent click counting
    print("\n=== Concurrent Click Test ===")
    service.set_encoding_strategy(base62)
    concurrent_code = service.shorten("https://popular-page.com")
    threads = []
    for _ in range(1000):
        t = threading.Thread(target=service.resolve, args=(concurrent_code,))
        threads.append(t)
        t.start()
    for t in threads:
        t.join()

    stats = service.get_stats(concurrent_code)
    print(f"After 1000 concurrent resolves: {stats}")
    print(f"Expected click count: 1000")


if __name__ == "__main__":
    main()
#include <iostream>
#include <thread>
#include <vector>
#include <regex>

int main() {
    Base62Strategy base62;
    UrlShortenerService service(&base62, 10000, 24);

    std::cout << "=== Shorten URLs ===" << std::endl;
    std::string code1 = service.shorten("https://www.example.com/very/long/path");
    std::string code2 = service.shorten("https://docs.cppreference.com");
    std::string code3 = service.shorten("https://github.com/user/repo");
    std::cout << "Shortened: " << code1 << std::endl;
    std::cout << "Shortened: " << code2 << std::endl;
    std::cout << "Shortened: " << code3 << std::endl;

    std::cout << "\n=== Resolve URLs ===" << std::endl;
    std::cout << code1 << " -> " << service.resolve(code1) << std::endl;
    std::cout << code2 << " -> " << service.resolve(code2) << std::endl;

    service.resolve(code1);
    service.resolve(code1);
    std::cout << "\n=== Click Count ===" << std::endl;
    std::cout << code1 << " clicks: " << service.getClickCount(code1) << std::endl;

    std::cout << "\n=== Custom Alias ===" << std::endl;
    std::string alias = service.shortenWithAlias("https://myportfolio.com", "my-site");
    std::cout << "Custom: " << alias << " -> " << service.resolve(alias) << std::endl;

    std::cout << "\n=== Duplicate Alias ===" << std::endl;
    try {
        service.shortenWithAlias("https://another.com", "my-site");
    } catch (const AliasAlreadyExistsException& e) {
        std::cout << "Caught: " << e.what() << std::endl;
    }

    std::cout << "\n=== Delete ===" << std::endl;
    service.deleteUrl(code3);
    try {
        service.resolve(code3);
    } catch (const UrlNotFoundException& e) {
        std::cout << "Caught: " << e.what() << std::endl;
    }

    std::cout << "\n=== Random Strategy ===" << std::endl;
    RandomStrategy randomStrategy(8);
    service.setEncodingStrategy(&randomStrategy);
    std::string rCode = service.shorten("https://random.example.com");
    std::cout << "Random: " << rCode << " -> " << service.resolve(rCode) << std::endl;

    // Concurrent test
    std::cout << "\n=== Concurrent Click Test ===" << std::endl;
    service.setEncodingStrategy(&base62);
    std::string popCode = service.shorten("https://popular.com");
    std::vector<std::thread> threads;
    for (int i = 0; i < 1000; ++i) {
        threads.emplace_back([&service, &popCode]() { service.resolve(popCode); });
    }
    for (auto& t : threads) t.join();
    std::cout << "After 1000 concurrent resolves, clicks: "
              << service.getClickCount(popCode) << std::endl;
    std::cout << "Expected: 1000" << std::endl;

    return 0;
}
function main() {
    const repository = new UrlRepository();
    const base62 = new Base62Strategy();
    const service = new UrlShortenerService(repository, base62, 10000, 24);

    console.log('=== Shorten URLs ===');
    const code1 = service.shorten('https://www.example.com/very/long/path/to/resource');
    const code2 = service.shorten('https://developer.mozilla.org/en-US/docs');
    const code3 = service.shorten('https://github.com/user/repo/pull/123');
    console.log(`Shortened: ${code1}`);
    console.log(`Shortened: ${code2}`);
    console.log(`Shortened: ${code3}`);

    console.log('\n=== Resolve URLs ===');
    console.log(`${code1} -> ${service.resolve(code1)}`);
    console.log(`${code2} -> ${service.resolve(code2)}`);

    service.resolve(code1);
    service.resolve(code1);

    console.log('\n=== URL Stats ===');
    console.log(service.getStats(code1).toString());

    console.log('\n=== Custom Alias ===');
    const alias = service.shortenWithAlias('https://myportfolio.com', 'my-site');
    console.log(`Custom alias: ${alias}`);
    console.log(`Resolves to: ${service.resolve(alias)}`);

    console.log('\n=== Duplicate Alias ===');
    try {
        service.shortenWithAlias('https://another.com', 'my-site');
    } catch (e) {
        console.log(`Caught: ${e.message}`);
    }

    console.log('\n=== Delete URL ===');
    service.delete(code3);
    try {
        service.resolve(code3);
    } catch (e) {
        console.log(`Caught: ${e.message}`);
    }

    console.log('\n=== Not Found ===');
    try {
        service.resolve('nonexistent');
    } catch (e) {
        console.log(`Caught: ${e.message}`);
    }

    console.log('\n=== Random Strategy ===');
    service.setEncodingStrategy(new RandomStrategy(8));
    const randomCode = service.shorten('https://random-strategy.example.com');
    console.log(`Random code: ${randomCode}`);
    console.log(`Resolves to: ${service.resolve(randomCode)}`);

    console.log('\n=== Sequential Click Test (single-threaded JS) ===');
    service.setEncodingStrategy(base62);
    const popCode = service.shorten('https://popular-page.com');
    for (let i = 0; i < 1000; i++) {
        service.resolve(popCode);
    }
    console.log(`After 1000 resolves: ${service.getStats(popCode).toString()}`);
    console.log('Expected click count: 1000');
}

main();

Follow-up Questions

  1. How would you scale to billions of URLs? Shard by short code prefix (consistent hashing). Use a distributed counter (Snowflake ID or UUID-based) instead of a single atomic counter. Store mappings in a distributed KV store (DynamoDB, Cassandra) with the short code as partition key.

  2. How would you handle hot URLs (caching)? Add a read-through cache (Redis) in front of the repository. Popular URLs stay cached with a TTL. Use write-through for new entries. For extremely hot URLs, replicate across multiple cache nodes.

  3. How would you implement rate limiting? Token bucket or sliding window per user/IP. Store counters in Redis with TTL. Return HTTP 429 when limit exceeded. Different limits for authenticated vs anonymous users.

  4. How would you build an analytics pipeline? Emit a ClickEvent on every resolve (async, non-blocking). Stream events to Kafka, process with Flink for real-time dashboards. Batch to S3 for historical analytics. Track referrer, geo, device, timestamp.

  5. How would you shard the storage? Hash-based sharding on short code (first 2 chars determine shard). Use consistent hashing for elastic scaling. Keep a lookup service or encode shard info in the short code itself.

  6. How would you handle custom alias conflicts at scale? Bloom filter for fast negative lookups (alias definitely not taken). On positive match, confirm with the actual store. For high-traffic registration, use a distributed lock or optimistic concurrency with retry.

  7. How would you implement URL previews and safety checks? On shorten: async job to fetch the page, extract title/description/thumbnail, and run malware/phishing checks. Store preview metadata alongside the mapping. Flag suspicious URLs and show interstitial warnings on resolve.



Scale this design past a single process and these are the concepts it runs into:

Discussion

Newest first
You

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access