Designing a URL Shortener¶
A URL shortener maps a compact public code to a destination URL. The redirect looks simple, but a production service must create codes safely, resolve them quickly, support link changes and expiry, withstand hot keys, and prevent its trusted domain from becoming a phishing platform.
This chapter designs a general-purpose service for public links. It focuses on the mapping and redirect path, not on building a complete marketing-attribution platform. Use the existing capacity-estimation guide for detailed workload arithmetic; here, capacity decisions are tied to the read-heavy shape of the service.
1. Clarify the requirements¶
Start by asking what the product actually promises. A minimal service may only create a link and redirect it. A commercial service may also need accounts, custom aliases, expiry, destination edits, analytics, branded domains, and abuse reporting.
| Requirement | Design question |
|---|---|
| Create | Can anonymous users create links, or must every link have an owner? |
| Redirect | Must redirects work globally with very low latency? |
| Custom aliases | Are human-readable aliases supported, and who can reserve them? |
| Lifecycle | Can an owner edit, disable, expire, or delete a link? |
| Analytics | Are approximate click counts enough, or is event-level reporting required? |
| Abuse | How are phishing, malware, spam, and automated link creation handled? |
| Availability | Can cached redirects continue during a database incident? |
| Privacy | Are destination URLs or click events sensitive? |
A reasonable baseline is:
- Create links for absolute HTTP or HTTPS destinations.
- Resolve a short code to one active destination.
- Support owner-controlled expiry and disablement.
- Offer optional custom aliases with conflict detection.
- Record analytics asynchronously, with explicitly stated counting limitations.
- Apply rate limits and an abuse-reporting process.
The redirect path is read-heavy. Creation and management are lower-volume writes. That asymmetry is the main architectural fact: optimize lookup and caching without making link creation or updates difficult to reason about.
2. Define a small API contract¶
A compact API might look like this:
| Method and path | Purpose |
|---|---|
POST /v1/links |
Create a link |
GET /{code} |
Redirect to its destination |
HEAD /{code} |
Return redirect headers without a response body |
PATCH /v1/links/{code} |
Change an owned link's destination or expiry |
DELETE /v1/links/{code} |
Disable or retire an owned link |
Example creation request:
POST /v1/links
Authorization: Bearer <access-token>
Idempotency-Key: create-campaign-link-2026-11
Content-Type: application/json
{
"destination_url": "https://www.example.com/products/widget?campaign=november",
"expires_at": "2027-01-01T00:00:00Z"
}
Return 201 Created with the short URL, code, and lifecycle metadata. If the caller retries after a timeout, an idempotency key can return the original result rather than creating another mapping. Scope that key to the authenticated owner and reject reuse with a different request body. The handbook's HTTP API semantics guide covers the general idempotency contract.
The redirect endpoint should normally accept only GET and HEAD. It is not a proxy: it does not fetch the destination or relay its response. Unsupported methods should receive 405 Method Not Allowed.
3. Use a simple read path and a separate write path¶
CREATE / MANAGE
Client -> API -> authenticate owner
-> parse and validate destination
-> abuse policy
-> generate code / reserve alias
-> primary database
-> populate or invalidate cache
-> return short URL
REDIRECT
Browser -> DNS / CDN / edge
-> lookup code in edge cache
| hit
+--------------------------> 302 / 301 + Location
|
| miss
v
redirect service
-> local cache
-> primary or suitable read store
-> check active / expiry / disablement
-> return redirect response
-> emit analytics event asynchronously
Keep the mapping database authoritative. A cache is an acceleration layer, not the only place a link exists. The create path validates and commits the mapping before returning success. The redirect path performs a bounded lookup and returns a small response; it should not synchronously write a detailed analytics row before redirecting.
For a first version, a relational database plus an in-memory or Redis cache is sufficient. Add a CDN or edge lookup when geography and measured traffic justify it. A globally distributed database, multiple write regions, and a separate analytics warehouse are not automatic requirements.
4. Generate codes without relying on probability alone¶
There are three common approaches:
| Strategy | Advantages | Costs and risks |
|---|---|---|
| Encode a database sequence in Base62 | Compact and collision-free when allocated centrally | Predictable and enumerable; a single allocator can become a coordination point |
| Generate random URL-safe tokens | Harder to enumerate; easy to generate in multiple app instances | Collisions are possible and must be handled |
| Accept custom aliases | Readable and useful for campaigns | Namespace conflicts, squatting, reserved words, and abuse |
For a general public service, a cryptographically secure random token is a sensible default. Python's secrets module provides URL-safe tokens intended for hard-to-guess URLs. A 12-byte input produces 96 bits of randomness and a 16-character Base64URL token. That is not a substitute for a uniqueness constraint: the database must remain the final authority.
The safe creation sequence is:
- Generate a candidate code.
- Attempt one atomic insert protected by a unique constraint.
- If the code already exists, generate another candidate and retry a small, bounded number of times.
- If every attempt collides, fail the request and alert on the unusual condition.
Do not perform a separate “does this code exist?” query before inserting. Two concurrent requests can both observe that a candidate is free. Let the unique index arbitrate the race.
The reference implementation in the Python example demonstrates bounded collision retries. Its insertion callback represents an atomic database operation such as:
INSERT INTO short_links (code, destination_url)
VALUES ($1, $2)
ON CONFLICT (code) DO NOTHING
RETURNING code;
A returned row means the code was reserved. No returned row means the candidate collided. PostgreSQL unique indexes enforce uniqueness under concurrent inserts; application-side pre-checks cannot provide that guarantee.
Randomness also reduces easy enumeration, but it does not provide authorization. A short code is a public identifier. If a link grants access to private information, treat it as a bearer capability with sufficient entropy, expiry, revocation, and careful logging—or do not use a public shortener for it.
5. Store the mapping and lifecycle state¶
A minimal PostgreSQL schema could be:
CREATE TABLE short_links (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
code varchar(16) NOT NULL UNIQUE,
destination_url text NOT NULL,
owner_id uuid,
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz,
disabled_at timestamptz,
redirect_mode text NOT NULL DEFAULT 'temporary'
CHECK (redirect_mode IN ('temporary', 'permanent'))
);
CREATE INDEX short_links_owner_created_idx
ON short_links (owner_id, created_at DESC)
WHERE owner_id IS NOT NULL;
CREATE INDEX short_links_expiry_idx
ON short_links (expires_at)
WHERE expires_at IS NOT NULL;
The unique index on code supports the primary lookup and protects code allocation. The owner index supports a user's link-management page. The expiry index helps a cleanup job find old records; expiry must still be checked during redirect, because cleanup is asynchronous and may run late.
Keep click events out of this row. A popular link may receive many redirects but only one mapping update. Updating a counter on the mapping row for every click creates write contention on the hottest records. Store analytics as a separate event stream or append-only store, then aggregate it independently.
For multi-tenant ownership, include the tenant in management queries and authorization checks. Do not assume that an unguessable code means a user may edit it. Every PATCH or DELETE must verify ownership or an explicit administrative permission.
6. Choose redirect status and cache policy deliberately¶
For a shortener whose destination may change, 302 Found is a practical default. It communicates a temporary redirection and avoids telling clients that the mapping is permanently fixed.
Use 301 Moved Permanently or 308 Permanent Redirect only when the product intends the destination to be stable. Permanent redirects can be cached heuristically by clients and intermediaries; changing the database row later may not change what a browser or CDN does. 307 Temporary Redirect and 308 Permanent Redirect preserve the request method when a client follows the redirect, while 302 and 301 have historical method-rewrite behavior. If the shortener accepts only GET and HEAD, the distinction is less visible, but the semantics should still be intentional.
The response should include a validated Location header. Add explicit Cache-Control directives that match the link's mutability and privacy:
- Short-lived public caching can reduce lookup load for editable links.
- Longer caching is appropriate only for links whose destination is intentionally immutable.
- Use
no-storefor responses that must not be retained by shared or private caches. - Avoid caching a negative lookup for long periods; a newly created code could otherwise continue returning 404 after it exists.
A cache hit is not proof that the link is still active. The design must decide how quickly disablement and abuse blocks take effect. Short TTLs, explicit purges, and versioned cache entries reduce stale redirects, but each adds operational work. HTTP caching rules are defined by RFC 9111; do not assume every browser, proxy, and CDN behaves identically.
7. Handle consistency, hot keys, and dependency failure¶
The redirect path can be fast and still be wrong if it reads stale state.
Read-after-create: If creation commits to the primary but a redirect immediately reads a lagging replica, the new code may appear missing. Populate the cache after commit, route a cache miss to the primary for a short period, or use a consistency-aware read path.
Destination edits and disablement: Update the database first, then invalidate or replace the cache entry. If cache invalidation fails, the old destination can remain active until its TTL expires. For high-risk links, use shorter TTLs or a stronger revocation check.
Hot codes: A viral link can dominate traffic. Cache it near users and avoid sending every hit to one database row. Monitor cache hit ratio and per-code traffic, and protect the origin from cache stampedes.
Cache outage: Use a bounded fallback to the database. If every request falls through at once, the cache incident can become a database incident. Limit concurrent misses and shed excess load rather than creating an unbounded queue.
Database outage: Cached active mappings may continue to redirect if policy allows stale reads. New links and cache misses may fail. Decide whether stale redirects are acceptable; do not silently serve a disabled or known-malicious destination just because the database is unavailable.
Expiry: Check expires_at at lookup time. A cleanup task is for storage maintenance, not correctness. Return a consistent terminal response—commonly 410 for a known expired link or 404 when the product intentionally hides its existence.
8. Treat abuse prevention as a core requirement¶
A shortener is an open-redirect service by design: users intentionally ask your domain to send visitors elsewhere. That makes reputation and abuse response part of the product, not an optional security add-on.
At creation time:
- Parse destinations with a maintained URL parser and allow only HTTP and HTTPS schemes.
- Reject malformed authorities, embedded credentials, control characters, and inputs that the redirect layer would interpret differently.
- Do not validate hostnames with string-prefix checks such as “starts with our trusted domain”.
- Apply per-account and per-IP creation limits, custom-alias quotas, and reserved-name rules.
- Provide a report-abuse route and an operator workflow to disable links quickly.
- Apply reputation checks using an appropriate threat-intelligence service, with a clear timeout and failure policy.
Python's urllib.parse.urlsplit() separates URL components but explicitly does not validate that an input is safe. Parsing is only the first step; enforce the service's own scheme, host, port, and syntax policy. OWASP recommends maintained parsers and careful validation for redirect targets.
Do not fetch arbitrary destinations from the application server just to generate a preview or screenshot unless that feature has a separate SSRF-safe design. A URL can resolve to loopback, private networks, cloud metadata endpoints, or change its DNS answer between validation and connection. If previews are required, isolate the fetcher, restrict egress, validate resolved addresses at connection time, disable automatic redirects or revalidate every hop, and cap response size and time.
If a commercial product uses Google's URL reputation checks, evaluate Web Risk rather than the non-commercial Safe Browsing API. A reputation check is one signal, not a guarantee that a link is safe. Maintain reporting, takedown, audit, and appeal procedures as well.
9. Keep analytics off the critical redirect path¶
A redirect should not wait for a warehouse insert or a third-party analytics API. Emit a small event to a bounded asynchronous pipeline, or rely on edge/server access logs and process them later.
A click event might contain the code, timestamp, coarse geography, referrer category, user-agent class, and a request identifier. Collect only what the product needs. Avoid storing full IP addresses or sensitive query strings by default; define retention and access policies.
Click counts are estimates, not ground truth. Link scanners, email security products, social previews, browser prefetching, bots, retries, and privacy tools can generate requests that look like human clicks. Deduplicating every visitor perfectly would require more identity data and can create privacy costs. Document the metric as observed requests or filtered visits, and make bot filtering rules visible.
10. Failure modes and operational signals¶
Monitor the user-visible redirect service, not just the API that creates links.
| Signal | What it can reveal |
|---|---|
| Redirect p50/p95/p99 latency | Slow edge lookup, cache misses, database waits |
| Cache hit ratio and origin QPS | Whether hot traffic is reaching the database |
| 404, 410, and disabled-link rates | Bad codes, expired links, abuse actions, or stale caches |
| Code collision retries | Generator or namespace problems |
| Cache invalidation failures | Risk of serving an old destination |
| Database pool wait and query latency | Origin saturation |
| Analytics queue age and drops | Reporting lag without blocking redirects |
| Abuse reports and block time | How quickly harmful links are contained |
Test concurrent creation of the same custom alias, random-code collisions, duplicate create requests, expiry boundaries, edits while cached, database failover, cache loss, and high traffic to one code. Also test that a malformed or blocked destination never reaches the Location header.
11. Trade-offs to explain in an interview¶
- Sequential IDs or random tokens: sequential codes are compact and simple but enumerable; random tokens are harder to guess but need collision handling and a uniqueness constraint.
- 302 or 301: temporary redirects preserve the ability to change destinations; permanent redirects may be cached beyond the service's control.
- Database lookup or edge cache: a database is simpler and authoritative; edge caching lowers latency and origin load but makes revocation and edits harder.
- Synchronous or asynchronous analytics: synchronous writes provide immediate persistence but add latency and contention; asynchronous events protect the redirect path but can be delayed or dropped unless the pipeline is durable.
- Single region or multi-region writes: a single writer is easier to keep unique and consistent; multi-region writes improve locality but require a deliberate global code-allocation and conflict strategy.
For many interview scenarios, the strongest answer is a single write region, random codes protected by a unique index, a relational source of truth, and a CDN/cache for the read path. Add distributed write coordination only when the availability or geography requirement justifies it.
12. Interview questions with reasoned answers¶
Q1. Why not use an auto-incrementing ID as the short code?
It is simple and collision-free when allocated by one database, but codes are predictable and easy to enumerate. It can be acceptable for public non-sensitive links with enumeration controls. Random tokens trade slightly more storage and collision handling for less guessability.
Q2. Can two application instances generate the same random code?
Yes. Randomness makes collisions unlikely, not impossible. A unique database constraint resolves concurrent attempts safely. Retry only the specific uniqueness conflict; do not perform a separate existence check.
Q3. Why is a cache useful if the database has an index on code?
An indexed lookup is still a network and database operation. Edge caching can serve popular redirects closer to users and reduce origin QPS. The cost is stale data and a more complex invalidation policy.
Q4. Why choose 302 instead of 301?
A 302 signals that the mapping may change. A 301 is permanent and can be cached heuristically, so a later destination edit may not reach clients that retained the redirect. Use permanent status only when the product contract makes the mapping immutable.
Q5. How do you stop a database outage from taking down every redirect?
Serve eligible cached mappings under a documented stale-data policy, bound database fallbacks, and shed load when the origin is saturated. Do not serve cached entries that are known to be disabled or blocked.
Q6. How do you prevent the shortener from becoming a phishing service?
Validate destinations, apply account and IP quotas, check reputation, expose abuse reporting, and provide rapid disablement. No single blocklist catches every harmful URL, so monitoring and response are necessary.
Q7. Should click analytics be written synchronously?
Usually not on the redirect path. Emit an event or process access logs asynchronously. If a business requirement demands durable click accounting, design a dedicated ingestion guarantee and accept its cost explicitly.
Q8. What changes when the service is multi-region?
Reads can be served close to users, but writes need globally unique codes and a clear consistency model. A single writer with replicated reads is a simpler starting point. If writes are active in several regions, use sufficiently random codes or a coordinated allocator, and define how newly created links become visible everywhere.
Design checklist¶
- Clarify create, redirect, edit, expiry, custom alias, and analytics requirements.
- Keep the mapping database authoritative.
- Use a unique constraint for code allocation and bounded collision retries.
- Separate the redirect path from analytics writes.
- Choose redirect status and cache headers based on mutability.
- Plan for cache invalidation, hot keys, stale replicas, and database failure.
- Validate destinations with a parser and an explicit policy.
- Treat abuse reporting, disablement, and privacy as product requirements.
- Test concurrency, expiry, revocation, and dependency outages.
A good URL shortener is not just a hash function and a redirect. It is a read-optimized mapping service with explicit lifecycle, cache, consistency, and abuse policies. The best interview answer starts with the smallest design that meets the requirements, then adds complexity only when a measured constraint requires it.
Further reading¶
- RFC 3986: URI Generic Syntax — URI components and generic syntax.
- RFC 9110: HTTP Semantics — redirect status codes, method semantics, and the Location field.
- RFC 9111: HTTP Caching — Cache-Control directives and shared-cache behavior.
- PostgreSQL 18: Unique Indexes — enforcing uniqueness under concurrent writes.
- Python: secrets — cryptographically strong random tokens, including URL-safe tokens.
- Python: urllib.parse — URL component parsing and its validation limitations.
- OWASP: Unvalidated Redirects and Forwards — risks and safe redirect handling.
- OWASP: SSRF Prevention — controls for server-side URL fetching.
- Google Cloud: Web Risk — URL threat-intelligence options for production services.
- Redis: Cache-aside — TTLs, invalidation, and cache-stampede controls.