Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Building a URL Shortener System Design

By Kokil Thapa | Last reviewed: September 2026

Building a URL shortener system design is the kind of problem that looks trivial in a whiteboard sketch and painful in production. You need sub-50ms redirects, collision-safe slug generation, durable storage, and analytics that do not lie when traffic spikes. On real client projects—campaign links for florists, signed booking confirmations on legal portals, QR codes on printed brochures—I treat short links as infrastructure tied to REST API design best practices, not a throwaway PHP script. This guide covers schema, encoding, caching, scaling tiers, and operational guardrails you can ship with Laravel 13, Redis 8.10, and MySQL 9.7.

What are the core components when building a URL shortener system design?

A production URL shortener has four cooperating layers. Each layer has a single job, and mixing jobs is how weekend prototypes become 3 a.m. incidents.

The write path accepts a long URL, optional custom alias, expiry, and metadata. It validates the target, generates or verifies a slug, persists the mapping, and warms cache. The read path is redirect-only: resolve slug, enforce expiry and blocklists, emit the correct status code, log the click. The analytics path records events asynchronously so a traffic spike never blocks redirects. The control plane covers admin UI, API keys, quotas, and abuse review.

URL Shortener ArchitectureClient AppPOST /api/linksWrite APIValidate + slugMySQL 9.7links tableRedisCache warmBrowserGET /{slug}Redirect Svc301 or 302Redis 8.10Hot slugsAnalytics QueueClicks to worker, not redirect path
Core architecture for building a URL shortener system design: separate write, redirect, and analytics paths.

Keep redirects stateless at the edge when you can. Put session logic, A/B tests, and geo rules behind a thin resolver service. That pattern mirrors what I use on Laravel eCommerce platforms with tracked campaign links where marketing needs UTM preservation without slowing checkout flows.

If you are greenfield on PHP, Laravel 13 with PHP 8.3+ gives you Form Requests, queued jobs, and route model binding out of the box. Symfony 8.1 works too, but needs PHP 8.4.1 minimum. Pick the stack your team already operates; a URL shortener is not the place to learn a new framework under load.

How do you generate short codes without collisions?

Slug generation is the heart of building a URL shortener system design. You have three common strategies, and each fails differently.

Base62 from auto-increment ID

Insert a row, read the numeric ID, encode it in Base62 (a–z, A–Z, 0–9). This is deterministic, compact, and easy to debug. A 64-bit ID fits in roughly 11 characters. Collisions are impossible if IDs are unique. The trade-off is predictable slugs—competitors can scrape sequential codes—so add rate limits and optional random salts for public APIs.

Hash-and-truncate with retry

Hash the long URL with SHA-256, take the first 7–8 Base62 chars, insert, retry on unique-key violation. Works without exposing sequence order. Expect birthday-paradox collisions once volume grows; keep a retry loop with a max attempt cap.

Custom aliases

Users pick your.dom/nepal. Validate charset, length, reserved words, and profanity. Always enforce uniqueness at the database layer with a unique index, not only in application code.

Slug Generation PipelineLong URLValidateHTTPS onlyInsert rowGet IDBase62Encode IDSlugLaravel 13 example (PHP 8.3+)id=482910 → Base62 → "1Lf3"Store slug UNIQUE, long_url, user_id, expires_atWarm Redis: SET link:1Lf3 → target URLReject javascript:, data:, file: schemes
Base62 encoding from numeric IDs is a proven slug strategy when building a URL shortener system design.

Test encoding edge cases with a Base64 encoder and decoder tool when you prototype hash outputs. Base62 is not Base64, but the exercise helps teams reason about charset size and collision math before they commit to a scheme.

Example Laravel service method for Base62 encoding:

public function encodeBase62(int $id): string
{
    $alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $base = strlen($alphabet);
    $encoded = '';

    while ($id > 0) {
        $encoded = $alphabet[$id % $base] . $encoded;
        $id = intdiv($id, $base);
    }

    return $encoded ?: '0';
}

Pair this with a database transaction on insert. Never generate the slug in JavaScript alone and hope the server agrees. Server-side validation is non-negotiable, same as any RESTful API built with Laravel.

What database schema works best for a URL shortener?

Start relational. MySQL 9.7 or PostgreSQL 18 both handle billions of rows with sane indexing. MongoDB fits only if you already run it for other reasons and accept trickier uniqueness guarantees under concurrency.

A practical links table:

  • id — BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
  • slug — VARCHAR(16) NOT NULL with UNIQUE index
  • long_url — TEXT NOT NULL (or VARCHAR(2048) if you cap length)
  • user_id — nullable FK for multi-tenant SaaS
  • expires_at — nullable DATETIME indexed for purge jobs
  • is_active — TINYINT for soft blocks without deleting history
  • created_at / updated_at — standard audit columns

Add a separate link_clicks table or stream clicks to columnar storage once daily aggregates exceed what OLTP should carry. I've seen teams choke production MySQL by writing one click row per redirect synchronously. That belongs in a queue, as covered in background jobs versus cron design choices.

Index strategy matters early:

  1. UNIQUE on slug — every redirect lookup hits this.
  2. INDEX on (user_id, created_at) — dashboard listing for authenticated users.
  3. INDEX on expires_at — nightly purge of expired links.

Read MySQL index design before you add composite indexes you never query. Wrong indexes slow writes and waste buffer pool memory on a hot redirect table.

For schema pitfalls—nullable slugs, missing charset on custom aliases, storing clicks inline—see common database schema mistakes. A URL shortener punishes sloppy uniqueness constraints fast.

ApproachRedirect latencyCollision riskOps complexityBest for
Single MySQL + Redis cache5–20 ms p99Low with unique indexLowMVPs, internal tools, <10M links
MySQL read replicas + Redis3–15 ms p99LowMediumPublic marketing campaigns
Sharded MySQL by slug prefix2–10 ms p99Low per shardHigh100M+ links, global footprint
Edge KV (CDN + origin)<5 ms p99Medium without strong originHighViral consumer apps

How should you handle redirects at scale?

The read path must be boringly fast. Target p99 redirect latency under 50 ms at origin; under 20 ms if marketing runs paid traffic.

Cache-first resolution

On GET /{slug}, check Redis first: GET link:{slug}. Cache hit → validate expiry → return redirect. Miss → query MySQL by slug → populate Redis with TTL → redirect. Use a short TTL (5–60 minutes) plus explicit invalidation on update or block.

Redis 8.10 supports the familiar string commands you already know from earlier versions. Official command reference lives at redis.io GET documentation. Treat cache as an accelerator, not the source of truth—MySQL remains authoritative.

Choose 301 vs 302 deliberately

HTTP 301 means permanent redirect; browsers and search engines cache aggressively. Use 301 for stable marketing links where you want SEO credit to pass. HTTP 302 is temporary; use it when slugs may change or when you need click analytics without sticky client caching. The MDN redirect guide at MDN HTTP redirection documents client behavior that bites teams who pick the wrong code.

Separate analytics from redirects

Push a lightweight event to a queue after you send the redirect response. Record slug, timestamp, referrer, user-agent hash, and country from GeoIP. Never block the redirect on analytics write success. Laravel queues with Redis driver fit this pattern on projects I've deployed alongside Redis-backed Laravel features.

Redirect Read PathRequestCDN EdgeRedisMySQL302/301Cache hit: skip MySQL entirelyAsync click event → queue → workerRedirect response never waits on analytics
Scale redirects with cache-first reads and asynchronous click logging in your URL shortener system design.

Route definition in Laravel stays minimal—resolve slug, redirect, dispatch job:

Route::get('/{slug}', function (string $slug) {
    $target = Cache::remember("link:{$slug}", 3600, fn () =>
        Link::query()
            ->where('slug', $slug)
            ->where('is_active', true)
            ->where(fn ($q) => $q->whereNull('expires_at')
                ->orWhere('expires_at', '>', now()))
            ->value('long_url')
    );

    abort_if(! $target, 404);

    dispatch(new RecordClickJob($slug, request()->headers->all()));

    return redirect()->away($target, 302);
})->where('slug', '[A-Za-z0-9]+');

For temporary signed links—invoice downloads, document portals—compare this pattern with Laravel signed URLs for temporary access. Short slugs and signed tokens solve different problems; do not merge them blindly.

How do you prevent abuse in a URL shortener?

Public shorteners become phishing infrastructure within hours if you skip guardrails. Even private shorteners on law-firm or eCommerce domains get probed by bots.

  • URL allow/deny lists — block javascript:, data:, raw IPs, and known malware domains.
  • Rate limits — per IP and per API key on create; per slug on redirect if you see scan patterns.
  • CAPTCHA or auth — require login for custom aliases; anonymous creates get stricter quotas.
  • Preview mode — optional interstitial for flagged domains builds user trust.
  • Report and revoke — admin flag sets is_active = 0 and purges Redis keys immediately.

Log creation metadata: IP, user-agent, API key ID. Correlate with redirect spikes to catch credential leaks. Webhook notifications on abuse reports mirror patterns from reliable webhook design—retry with backoff, verify signatures, idempotent handlers.

On Nepal-facing projects, budget for intermittent mobile networks and shared IPs. A hard per-IP cap of five creates per minute blocks legitimate café users. Combine API keys for apps with softer IP limits for browsers.

Scaling Decision TreeTraffic growing?Tier 1<1K rps, single VMTier 2Redis + read replicaTier 3Shard + CDN edgeHold Tier 3 until p99 latency breaks SLOPremature sharding adds ops cost without user benefitSee capacity planning before you split databases
Scale tiers when building a URL shortener system design—avoid sharding until metrics prove you need it.

Capacity math belongs in your runbook. Estimate peak redirects per second, average slug length, Redis memory per key, and click queue throughput. Walk through capacity planning for growing systems before you buy a second database cluster for a problem Redis solves.

What API and deployment choices matter for production?

Expose a small public API: create link, read stats, revoke link. Version it (/api/v1/links), paginate list endpoints, and return stable JSON error shapes. Document idempotency for create if clients retry on timeout—accept a client-supplied Idempotency-Key header stored with a 24-hour TTL.

Deploy with zero-downtime releases. Symlinked Deployer 7 releases work well on Ubuntu 22/24 with PHP-FPM 8.3 or 8.4. Warm Redis after deploy or accept a short cache-miss window. Reload PHP-FPM so opcache picks up new code; stale opcache after deploy is a redirect bug I've debugged more than once.

If you productize the shortener as SaaS, study multi-tenant SaaS patterns in Laravel and why Laravel fits Nepal SaaS products. Tenant isolation on slug namespaces prevents one customer's marketing typo from overwriting another's link.

For public developer platforms, publish OpenAPI specs and SDK notes—see SDK design for public APIs. Interview candidates often sketch this system; system design interview prep for web developers complements hands-on build guides.

Need implementation help rather than theory alone? A focused API development engagement or custom software project covers schema, deploy pipeline, and abuse controls end to end. Validate performance under load through testing and optimization before you aim paid traffic at short domains.

MySQL reference documentation for InnoDB indexing lives at MySQL 9.7 InnoDB index types. Cross-check unique constraints and row formats there when you tune the links table.

Key Takeaways

  • Split write, redirect, and analytics paths so click logging never blocks redirects.
  • Use Base62-from-ID or hash-with-retry slugs, always enforced by a UNIQUE database index on slug.
  • Resolve redirects cache-first in Redis 8.10, with MySQL 9.7 as the source of truth.
  • Pick HTTP 301 for permanent marketing links and 302 when targets may change or analytics must stay fresh.
  • Ship rate limits, URL scheme blocking, and instant revoke before opening a public create API.
  • Scale vertically and cache aggressively through Tier 2; shard only when metrics—not guesswork—demand it.

People Also Ask

How many characters should a short URL code be?

Six to eight Base62 characters cover tens of billions of combinations. Eleven characters encode full 64-bit IDs with room to grow. Shorter codes look cleaner on print materials but collide sooner if you use random generation without enough entropy.

Is a URL shortener a good system design interview question?

Yes. It tests encoding, database indexing, caching, redirect semantics, and analytics separation in one bounded problem. Interviewers expect you to state assumptions—read-heavy, custom aliases optional, global latency target—and trade off 301 versus 302 explicitly.

Can you build a URL shortener with WordPress?

You can for low-volume marketing redirects using redirect plugins or custom rewrite rules on WordPress 7.1. High-QPS public shorteners belong outside WordPress admin bootstrapping. Use WordPress for content; run the redirect service on Laravel or a thin edge worker.

A shortener optimizes for compact URLs and fast redirects. A tracker optimizes for attribution data—UTM parameters, conversion pixels, cohort reports. Production systems combine both but keep the redirect path minimal while analytics runs asynchronously.

Ship a URL shortener you can operate in production

Building a URL shortener system design teaches the same lessons as larger platforms: fast reads, safe writes, async side effects, and boring infrastructure you can debug at 2 a.m. Start with Laravel 13, MySQL, and Redis on a single well-tuned VM. Add replicas and CDN caching when metrics justify the cost—not when a blog post says you should. If you want this built, hardened, and deployed on infrastructure you control, contact us to scope a production-ready short-link service tied to your campaigns, portals, or public API.

Frequently Asked Questions

It is a production architecture with a write API storing long URLs and unique short codes, a cache-first read path returning HTTP 301 or 302 redirects, async analytics, plus rate limits, TTL rules, and abuse controls.

A production shortener has four layers that must stay separate. The write path validates targets, generates or verifies slugs, persists mappings, and warms cache. The read path resolves slugs, enforces expiry and blocklists, and redirects without waiting on analytics. The analytics path records clicks asynchronously so traffic spikes never block redirects. The control plane covers admin UI, API keys, quotas, and abuse review. Mixing redirect logic with click logging is a common cause of late-night incidents on real campaign and portal projects.

Three strategies appear in most designs, each with different failure modes. Base62 encoding from an auto-increment ID is deterministic and collision-free if IDs are unique, though sequential slugs are predictable. Hash-and-truncate with SHA-256 and retry on unique-key violation hides sequence order but needs a capped retry loop as volume grows. Custom aliases require charset, length, reserved-word, and profanity validation. In all cases, enforce uniqueness with a database UNIQUE index on slug, generate slugs server-side inside a transaction, and never rely on client-side generation alone.

Start relational with MySQL 9.7 or PostgreSQL 18. A practical links table uses BIGINT id, VARCHAR(16) slug with UNIQUE index, TEXT long_url, nullable user_id for multi-tenant SaaS, indexed expires_at, is_active for soft blocks, and standard timestamps. Store clicks in a separate link_clicks table or columnar storage once daily aggregates exceed OLTP capacity. Index slug for every redirect lookup, user_id plus created_at for dashboards, and expires_at for purge jobs. MongoDB fits only if you already operate it and accept trickier uniqueness under concurrency.

Target p99 redirect latency under 50 ms at origin, or under 20 ms when paid marketing traffic depends on it. Use cache-first resolution: check Redis for link:{slug}, validate expiry on hit, query MySQL on miss, populate Redis with a 5–60 minute TTL, then redirect. Treat Redis 8.10 as an accelerator, not the source of truth. After sending the redirect response, dispatch a lightweight queue job with slug, timestamp, referrer, user-agent hash, and GeoIP country. Never block redirects on analytics write success. Keep the Laravel route minimal: resolve slug, redirect, dispatch job.

Choose deliberately because client caching behavior differs sharply. HTTP 301 means permanent redirect; browsers and search engines cache aggressively, so use it for stable marketing links where you want SEO credit to pass to the destination. HTTP 302 is temporary; use it when slug targets may change or when you need click analytics without sticky client-side caching interfering. Picking the wrong status code is a recurring production mistake on tracked campaign links where marketing expects fresh redirect behavior after updates.

Public shorteners become phishing infrastructure quickly without guardrails. Block javascript:, data:, raw IPs, and known malware domains via allow/deny lists. Apply rate limits per IP and API key on create, and per slug on redirect if scan patterns appear. Require login or CAPTCHA for custom aliases; give anonymous creates stricter quotas. Offer optional preview interstitials for flagged domains. On abuse reports, set is_active to zero and purge Redis keys immediately. Log creation metadata—IP, user-agent, API key ID—and correlate with redirect spikes. On Nepal-facing projects, avoid hard per-IP caps that block café users on shared mobile networks.

Six to eight Base62 characters cover tens of billions of combinations. Eleven characters encode full 64-bit IDs with room to grow.

The article outlines four tiers matched to load and ops capacity. Single MySQL 9.7 plus Redis cache suits MVPs and internal tools under roughly 10M links at 5–20 ms p99 with low ops complexity. MySQL read replicas plus Redis handle public marketing campaigns at 3–15 ms p99. Sharded MySQL by slug prefix targets 100M-plus links at 2–10 ms p99 but adds high ops complexity. Edge KV with CDN plus origin can reach sub-5 ms p99 for viral consumer apps. Avoid sharding until metrics prove you need it; vertical scaling and aggressive caching often suffice through Tier 2.

A shortener optimizes compact URLs and fast redirects. A tracker optimizes attribution—UTM parameters, conversion pixels, cohort reports. Production systems combine both but keep the redirect path minimal while analytics runs asynchronously.

WordPress 7.1 works for low-volume marketing redirects using redirect plugins or custom rewrite rules. High-QPS public shorteners do not belong inside WordPress admin bootstrapping. Use WordPress for content pages and run the redirect service on Laravel or a thin edge worker where sub-50 ms reads and separate analytics matter under campaign load.

Yes. It tests encoding, database indexing, caching, redirect semantics, and analytics separation in one bounded problem.

Expose a small versioned public API—create link, read stats, revoke link—under paths like /api/v1/links. Paginate list endpoints and return stable JSON error shapes. Support idempotency for create by accepting a client-supplied Idempotency-Key header stored with a 24-hour TTL so retries after timeout do not duplicate links. For SaaS products, isolate tenant slug namespaces so one customer's alias cannot overwrite another's. Publish OpenAPI specs if you expose a developer platform. These patterns mirror REST API best practices used on production Laravel campaign-link services.

Use zero-downtime releases with symlinked Deployer 7 on Ubuntu 22 or 24 and PHP-FPM 8.3 or 8.4. Warm Redis after deploy or accept a short cache-miss window where redirects hit MySQL more often. Reload PHP-FPM after the symlink swap so opcache picks up new code; stale opcache after deploy is a redirect bug seen repeatedly in production. Validate performance under load before aiming paid traffic at the short domain. Start on a single well-tuned VM with Laravel 13, MySQL, and Redis; add replicas and CDN caching only when metrics justify the cost.

Separate them from day one. Writing one click row per redirect synchronously chokes production MySQL under traffic spikes. Push events to a Laravel queue with the Redis driver after the redirect response is sent, recording slug, timestamp, referrer, user-agent hash, and country from GeoIP. This keeps the read path stateless and fast while dashboards and aggregates consume click data on a separate async pipeline. The same split applies whether you run internal campaign links on an eCommerce platform or signed booking confirmations on a legal portal where reliability matters more than real-time stats.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: