Back

Cheatsheet to the URL Shortener Design Interview Question

May 31, 2026·7 min read

A beginner-friendly systems design walkthrough for building and scaling a URL shortener.

How much engineering could possibly hide behind turning this:

https://example.com/articles/system-design-for-beginners

into this?

https://sho.rt/aZ91x

At first glance, a URL shortener feels like a small dictionary: store a long URL under a short code, then look it up later. That is the right starting point. The interesting design decisions appear when millions of people start clicking links at the same time.

Define the Basics

A URL shortener has two main jobs:

  • Shorten: Accept a long URL and return a compact link.
  • Redirect: Look up a short code and send the browser to the original URL.

A few terms will help:

  • Short code: The unique identifier at the end of the URL, such as aZ91x.
  • Redirect: An HTTP response that tells a browser to visit another URL.
  • Cache: Fast temporary storage for frequently accessed data.
  • Collision: Two URLs accidentally receiving the same short code.

Requirements

Before drawing boxes, clarify the product.

Functional Requirements

  • Create a short URL from a long URL.
  • Redirect visitors to the original URL.
  • Optionally support custom aliases such as /my-resume.
  • Optionally expire links after a configured date.
  • Record basic click counts.

Non-Functional Requirements

  • Redirects should be fast.
  • Short codes must be unique.
  • The system should remain available during traffic spikes.
  • Reading a link will happen much more often than creating one.

That last point shapes the architecture. This is a read-heavy system.

Note: Start with the smallest useful feature set. Link previews, advanced analytics, abuse detection, and user accounts can be added later.

API Design

The service only needs two essential endpoints.

Create a Short URL

POST /api/v1/urls
Content-Type: application/json

{
  "url": "https://example.com/articles/system-design-for-beginners",
  "expiresAt": "2027-01-01T00:00:00Z"
}

Response:

{
  "shortUrl": "https://sho.rt/aZ91x",
  "code": "aZ91x"
}

Redirect to the Original URL

GET /aZ91x

Response:

HTTP/1.1 302 Found
Location: https://example.com/articles/system-design-for-beginners

A 302 redirect is a sensible default because it allows the service to continue observing clicks. A 301 permanent redirect may encourage browsers and other systems to cache the destination more aggressively.

Generating Short Codes

A short code must be compact, URL-friendly, and unique.

One approachable strategy is:

  1. Generate a numeric ID.
  2. Encode it using Base62.
  3. Store the resulting code with the original URL.

Base62 uses:

0-9  a-z  A-Z

With six characters, Base62 provides:

62^6 = 56,800,235,584 combinations

That is more than 56 billion possible codes.

Here is a simplified implementation:

const alphabet =
  "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

function encodeBase62(id: number): string {
  if (id === 0) return alphabet[0];

  let result = "";

  while (id > 0) {
    result = alphabet[id % 62] + result;
    id = Math.floor(id / 62);
  }

  return result;
}

console.log(encodeBase62(125)); // "21"

Why Not Use the First Few Characters of a Hash?

// Risky: truncation can create collisions.
const code = hash(url).slice(0, 6);

A hash is useful, but shortening it increases the chance of collisions. You would still need to check the database and retry when a duplicate appears.

A database-generated ID encoded as Base62 is easier to explain and operate in an entry-level design.

Tip: Predictable IDs are acceptable for a learning exercise. If people must not guess nearby links, use randomized codes or add a reversible scrambling step.

Data Model

A relational database is a practical starting point.

CREATE TABLE short_urls (
  id BIGINT PRIMARY KEY,
  code VARCHAR(12) NOT NULL UNIQUE,
  original_url TEXT NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  expires_at TIMESTAMP NULL
);

CREATE INDEX idx_short_urls_code ON short_urls(code);

The code index matters because redirects happen frequently:

SELECT original_url, expires_at
FROM short_urls
WHERE code = 'aZ91x';

Without an index, the database may scan many rows to find one link. That is like searching every apartment in a city instead of checking the address directory.

High-Level Architecture

Browser or API Client
          |
          v
    Load Balancer
          |
          v
  URL Shortener API ------> Cache
          |
          +---------------> Primary Database
          |
          +---------------> Event Queue ------> Analytics Worker
                                                    |
                                                    v
                                             Analytics Store

Request Flow: Creating a Link

  1. The client sends a long URL.
  2. The API validates the URL.
  3. The database allocates a unique numeric ID.
  4. The API converts the ID to Base62.
  5. The API stores the short code and original URL.
  6. The API returns the short URL.

Request Flow: Opening a Link

  1. A browser requests /aZ91x.
  2. The API checks the cache.
  3. If the cache contains the destination, the API redirects immediately.
  4. If the cache misses, the API reads from the database.
  5. The API stores the result in the cache.
  6. The API returns the redirect.

Scaling the Reads

Popular links may be opened thousands of times per second. Reading the same row repeatedly from the database wastes work.

A cache keeps frequently requested mappings close to the application:

aZ91x -> https://example.com/articles/system-design-for-beginners

Pseudocode:

async function redirect(code: string) {
  const cachedUrl = await cache.get(code);

  if (cachedUrl) {
    return redirectTo(cachedUrl); // Fast path
  }

  const record = await database.findByCode(code);

  if (!record || isExpired(record)) {
    return notFound();
  }

  await cache.set(code, record.originalUrl, { ttl: 3600 });
  return redirectTo(record.originalUrl);
}

The cache is helpful, but it is not the source of truth. If it disappears, the database still contains every link.

Keep Analytics Off the Critical Path

Click counts are useful, but a redirect should not wait for analytics processing.

// Slower redirect: the visitor waits for the counter update.
await database.incrementClickCount(code);
return redirectTo(url);

Instead, publish an event and process it asynchronously:

// The redirect remains fast.
await eventQueue.publish({
  type: "url.clicked",
  code,
  clickedAt: new Date().toISOString(),
});

return redirectTo(url);

A background worker can aggregate counts later. If analytics briefly falls behind, links still work.

Common Culprits

A few design mistakes show up quickly as traffic grows.

1. Hitting the Database for Every Redirect

Popular links create unnecessary database load.

Fix: Cache frequently accessed mappings.

2. Updating Click Counts During Redirects

Analytics writes slow down the user-facing path.

Fix: Send click events to a queue and process them asynchronously.

3. Ignoring Invalid or Dangerous URLs

A URL shortener can hide misleading links.

Fix: Validate URL schemes, rate-limit link creation, and add abuse reporting.

function isAllowedUrl(value: string): boolean {
  const url = new URL(value);
  return url.protocol === "http:" || url.protocol === "https:";
}

4. Forgetting Expiration

Expired links should not redirect forever.

Fix: Check expires_at after cache misses and use a cache time-to-live (TTL).

5. Overengineering Too Early

Sharding, globally distributed databases, and elaborate ID services add operational cost.

Fix: Begin with one database, stateless API servers, a cache, and a queue. Add complexity when measured traffic requires it.

A Practical First Version

For an entry-level implementation, build this:

  • A stateless HTTP API.
  • A relational database with a unique index on code.
  • Base62 encoding for short codes.
  • A cache for redirect lookups.
  • A queue for click events.
  • A background worker for analytics.
  • Rate limiting and basic URL validation.

When traffic grows, consider:

  • Read replicas for database reads.
  • Multiple API instances behind a load balancer.
  • Cache replication.
  • Database partitioning by short code.
  • Regional deployments for lower redirect latency.
  • More advanced abuse detection.

Conclusion

A URL shortener starts as a dictionary:

short code -> original URL

The architecture becomes more interesting when we protect the database from repeated reads and keep optional work away from the redirect path.

The core lessons are broadly useful:

  1. Design for the dominant traffic pattern.
  2. Add indexes for common lookups.
  3. Cache frequently accessed data.
  4. Move non-essential work to background processing.
  5. Keep the first version simple enough to operate confidently.

That is a solid systems design answer: begin with a small reliable service, explain the bottlenecks clearly, and scale each part only when the traffic justifies it.


-Caleb