File: url-shortener/url_shortener.py

Date: 2026-06-05

Time: 14:05

url-shortener/url_shortener.py

Purpose

This file is a self-contained, in-memory URL shortening service — the kind you'd sketch on a whiteboard in a system design interview and then implement as a working prototype. It owns the full lifecycle of a shortened URL: creation (with two generation strategies), redirection with click tracking, analytics aggregation, expiration, and deletion. Everything lives in a single process with no external dependencies — the storage is a Python dict, not a database.

Key Components

Base62

A stateless utility for encoding integers into URL-safe strings using [0-9a-zA-Z]. The encode/decode pair is bijective — every positive integer maps to a unique string and back. This is the alphabet that turns a monotonic counter or hash digest into a short code like 0000001 or a3Bf9x2.

URLEntry

A dataclass holding everything known about a single shortened URL: the mapping itself (shortcodelongurl), metadata (creation time, expiration, creator), and analytics state (clickcount, clickhistory). The click_history list is bounded to the last 1000 events — an implicit design decision that trades completeness for memory.

AnalyticsReport

A read-only view computed on demand from a URLEntry. Aggregates clicks per day, recent clicks (last 10), and referrer frequency. This is a projection, not stored state — it's recomputed on every get_analytics call.

URLShortener

The service itself. Constructor parameters control the domain prefix, default TTL, short code length, generation strategy ("counter" vs "hash"), and rate limit threshold. The two strategies reflect a real design tradeoff:

Patterns

Strategy pattern for code generation — the strategy field selects between counter and hash at construction time, with dispatch in shorten() via a simple if/else. No formal interface; it's a lightweight variant.

Injectable time — every method that touches timestamps accepts an optional current_time parameter. This avoids mocking time.time() in tests and makes the service fully deterministic when you control the clock. This is a common pattern in SDI implementations where you need to test expiration and rate limiting.

Sliding window rate limitingcheckrate_limit keeps a list of timestamps per creator, prunes entries older than 60 seconds, and rejects if the window is full. This is a fixed-window approximation (the window slides on each call, but the pruning is eager, not lazy).

Bounded historyclick_history is capped at 1000 entries with tail retention ([-1000:]). This prevents unbounded memory growth per URL but means early click data is silently dropped.

Dependencies

Imports: All stdlib — hashlib for SHA-256 in hash strategy, time for wall-clock defaults, collections.defaultdict for sparse counters, dataclasses for structured data, datetime for UTC day formatting in analytics, urllib.parse for URL validation.

Imported by: testurlshortener.py — the test suite is the only consumer. This module has no downstream dependents in the repo.

Flow

A typical lifecycle:

1. shorten(longurl) — Validates the URL (scheme + domain), checks rate limits if a creatorid is provided, generates a short code via the configured strategy, wraps it in a URLEntry, stores it in _urls[code], and returns the full short URL string (domain/code).

2. redirect(shortcode) — Looks up the code in urls, checks expiration against the current time, increments the click counter, appends a click event (with timestamp and optional referrer), trims history if it exceeds 1000 entries, and returns the original long URL. Returns None for missing or expired entries — no exception, no distinction between "never existed" and "expired."

3. getanalytics(shortcode) — Iterates the click history to compute per-day counts and referrer frequencies. Returns None for unknown codes.

Invariants

Error Handling

All validation errors raise ValueError with descriptive messages — invalid URLs, bad custom aliases, rate limit violations, duplicate aliases. These are the only exceptions the module produces.

redirect and get_analytics return None for missing/expired entries instead of raising — the caller is expected to check. delete returns a boolean. There's no error type hierarchy or custom exceptions; ValueError is the single error channel.

Notably, there's no error handling for the hash collision loop — if the keyspace were exhausted, generatehash_code would loop forever. This is fine for an SDI prototype but would need a max-attempts guard in production.

Topics to Explore

Beliefs