· 7 min read

Designing a URL Shortener: From Requirements to Production Trade-offs

A practical walkthrough of how I would structure a URL-shortener system-design discussion, from requirements and data modelling to redirects, caching and abuse controls.

  • System design
  • API design
  • PostgreSQL
  • Caching

A URL shortener sounds simple: accept a long URL, return a short one, and redirect visitors later.

That simplicity makes it a useful system-design exercise. The difficult part is not writing the redirect endpoint. It is deciding what the first version must guarantee, where correctness matters, and which complexity should wait until there is evidence that it is needed.

This is how I would approach the problem in a system-design discussion.

1. Clarify the product before drawing the architecture

I would start by confirming the smallest useful scope.

Required for the first version

  • A user submits a valid HTTP or HTTPS URL.
  • The system generates a unique short code.
  • Visiting the short URL redirects to the original URL.
  • The mapping remains available after a restart or deployment.
  • Every short code resolves unambiguously to at most one active mapping.

Questions that change the design

  • Can users choose a custom alias?
  • Can links expire?
  • Can a destination be edited later?
  • Is authentication required?
  • Do we need click analytics?
  • What traffic should the system support?
  • Are links publicly accessible, unlisted, or protected by authentication?

For an interview-sized first version, I would support creation and redirection, then describe custom aliases, expiry and analytics as extensions.

2. Keep the critical path small

One Next.js application with separate create, redirect and analytics flows
URL shortener architectureA link creator submits a long URL to a Next.js application, which validates it and stores the mapping in PostgreSQL. A visitor opens a short URL, and the application checks Redis before falling back to PostgreSQL. Click events are published to a queue and processed asynchronously.URL shortenerLink creatorExternal actorVisitorExternal actorNext.js applicationUI + API + redirectsPostgreSQLSource of truthRedis cacheOptional cacheEvent queueAsync bufferAnalytics workerAsync processorsubmits original URLvisits short URLcreates mapping1. cache lookup2. DB fallback3. populate cacheclick eventconsume
Read the architecture as text
  • Single application: one Next.js deployment contains the creation UI, the POST /api/links handler and the short-link redirect route.
  • Create: a link creator submits an original URL. The Next.js handler validates it, generates a code and stores the mapping in PostgreSQL.
  • Redirect: a visitor reaches the Next.js redirect route. The application checks Redis first and queries PostgreSQL on a cache miss, then populates the cache.
  • Analytics: the redirect route publishes a click event to a queue. A separate worker consumes it asynchronously, so analytics processing does not delay the redirect response.

There are two different workloads:

  1. Create path — validates a URL, generates a code and writes a mapping.
  2. Redirect path — reads a mapping and returns a redirect as quickly as possible.

The redirect path is normally more sensitive because every extra dependency adds latency and another failure point. Analytics should therefore happen asynchronously rather than blocking the redirect.

3. Define the API contract

A minimal create endpoint could be:

POST /api/links
Content-Type: application/json

{
  "url": "https://example.com/a/very/long/path"
}

Successful response:

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

The redirect endpoint is public:

GET /aZ91kQ

Possible responses:

  • 302 Found with a Location header when destinations may change
  • 301 Moved Permanently when mappings are immutable and long-term caching is desirable
  • 404 Not Found when the code does not exist
  • 410 Gone when the link existed but has expired

I would begin with 302 unless the product explicitly guarantees immutable destinations. It gives the system more control if links need to be corrected, disabled or moderated later.

4. Use a small, explicit data model

A PostgreSQL table could begin with:

CREATE TABLE links (
  id BIGSERIAL PRIMARY KEY,
  code VARCHAR(16) NOT NULL UNIQUE,
  original_url TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  expires_at TIMESTAMPTZ NULL,
  created_by BIGINT NULL
);

The most important constraint is the uniqueness requirement on code. Application-level checks alone are not sufficient because two requests may generate the same candidate at the same time. In PostgreSQL, the UNIQUE constraint automatically creates the supporting unique index.

The redirect query stays simple:

SELECT original_url, expires_at
FROM links
WHERE code = $1;

The optional created_by field would only be populated if authenticated link ownership is part of the product scope.

5. Choose the short-code strategy deliberately

Short-code strategy trade-offs
StrategyStrengthTrade-off
Random Base62Large, shareable namespace; database constraint handles collisions.Retry on an unlikely collision.
Database sequence + Base62Simple uniqueness and compact codes.Sequential codes are easier to enumerate.
Hash of original URLDeterministic for a given input.Truncation can collide; identical URLs need not share a link.

For the first version, I would use a random Base62 code and let the database unique constraint protect against collisions.

Base62 uses:

  • a-z
  • A-Z
  • 0-9

A six-character code provides approximately 56.8 billion possible values, although the appropriate length should account for expected volume, collision probability and retry rates.

Creation flow:

  1. Generate a random code.
  2. Attempt to insert it.
  3. If the unique constraint fails, generate a new code and retry.
  4. Limit retries and return an internal error if the unlikely collision loop cannot complete.

A database sequence encoded as Base62 is also practical, but sequential codes make enumeration easier. Hashing the original URL does not eliminate collisions once the hash is shortened, and it couples code generation to deduplication semantics that the product may not want.

6. Separate validation from fetching

The service should validate that the submitted destination:

  • parses as a URL
  • uses http or https
  • stays within the maximum accepted length
  • does not use unsupported schemes such as javascript: or data:
  • does not create an obvious self-referential redirect loop

A basic URL shortener does not need to fetch the destination during creation. Avoiding that network call keeps the create path smaller and avoids introducing unnecessary server-side request risks.

A production service would still need abuse controls because short links can hide phishing, malware or spam destinations.

7. Add caching only where it helps

The redirect workload is read-heavy, so a cache can reduce repeated database reads.

A simple read-through flow:

  1. Look up code in Redis.
  2. On a cache hit, redirect immediately.
  3. On a miss, query PostgreSQL.
  4. Cache the result with a suitable TTL.
  5. Return the redirect.

The application controls the fallback to PostgreSQL. Redis does not query the database itself, and PostgreSQL remains the source of truth.

I would not add Redis automatically for a small internal product. A unique index and a healthy PostgreSQL instance may already be enough. The cache becomes valuable when measurements show that redirect traffic or database load justifies another operational dependency.

If destinations can be edited, disabled or expired, the corresponding cache entry must be invalidated or updated during the write operation. The cache TTL acts only as a fallback, not as the primary consistency mechanism.

Negative caching can also help with repeated requests for unknown codes, but the TTL should be short so a newly created code does not remain incorrectly cached as missing.

8. Keep analytics off the redirect path

Click analytics are useful, but recording them synchronously can slow every redirect.

A better design is:

  1. Resolve the destination.
  2. Publish a lightweight click event to a queue.
  3. Return the redirect.
  4. Process analytics separately.

The event might contain:

  • short code
  • timestamp
  • coarse referrer information
  • privacy-safe location or device categories when permitted

The redirect should still succeed when the analytics consumer is delayed. Analytics are valuable, but they are not more important than the redirect itself.

9. Plan for failure and abuse

The first production review should cover:

Reliability

  • timeout and fallback behaviour for database or cache failures
  • retry behaviour for non-user-facing background work
  • expired and disabled links
  • backward-compatible database migrations
  • health checks, redirect latency metrics and error-rate monitoring

Abuse controls

  • rate limiting on link creation
  • maximum URL length
  • reserved custom aliases
  • disabling malicious links
  • reporting and moderation workflow
  • protection against automated enumeration
  • logs that avoid unnecessary personal data

Security

  • validate schemes
  • encode output correctly
  • avoid open administration endpoints
  • protect authenticated link-management routes
  • keep secrets outside the repository
  • apply least-privilege database access

10. Scale in measured steps

I would explain scaling as a sequence rather than designing the largest possible system on day one.

Stage 1 — Simple application

  • one application service
  • PostgreSQL
  • indexed code lookup
  • basic monitoring

Stage 2 — Read optimisation

  • Redis read-through cache
  • connection pooling
  • separate create and redirect metrics
  • rate limiting

Stage 3 — Higher traffic

  • multiple stateless service instances
  • load balancer
  • asynchronous analytics queue
  • cache replication or managed cache
  • database read strategy based on measured load

Stage 4 — Global redirect traffic

  • regional or edge caching
  • careful cache invalidation
  • globally unique code generation
  • operational tooling for abuse and link takedowns

The important interview point is not to predict every future component. It is to show which measurement or product requirement would justify each one.

11. State the trade-offs clearly

My first-version decision would be:

  • PostgreSQL as the source of truth
  • random Base62 codes
  • a database unique constraint with collision retry
  • 302 redirects while destinations remain changeable
  • no cache until traffic justifies it
  • asynchronous analytics through a queue
  • explicit validation and creation rate limits

This is intentionally simple. It gives the product a correct and maintainable base while leaving clear extension points for caching, analytics, expiry, custom aliases and global delivery.

12. How I would present it in an interview

I would use this order:

  1. Restate the goal.
  2. Confirm functional requirements.
  3. Ask about traffic, mutability, expiry and analytics.
  4. Write confirmed answers under headings such as users, requirements, constraints and scale.
  5. Draw create and redirect paths separately.
  6. Define the API and data model.
  7. Choose a code-generation strategy.
  8. Explain correctness through the unique constraint.
  9. Discuss caching only after the basic design.
  10. Cover failure, abuse and security.
  11. End with trade-offs and the next scaling step.

That structure matters as much as the final diagram. It helps the interviewer follow the reasoning and makes it easier to recover when a requirement changes.