
September 08, 2026
12 min read
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.
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.
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:
- UNIQUE on
slug— every redirect lookup hits this. - INDEX on
(user_id, created_at)— dashboard listing for authenticated users. - 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.
| Approach | Redirect latency | Collision risk | Ops complexity | Best for |
|---|---|---|---|---|
| Single MySQL + Redis cache | 5–20 ms p99 | Low with unique index | Low | MVPs, internal tools, <10M links |
| MySQL read replicas + Redis | 3–15 ms p99 | Low | Medium | Public marketing campaigns |
| Sharded MySQL by slug prefix | 2–10 ms p99 | Low per shard | High | 100M+ links, global footprint |
| Edge KV (CDN + origin) | <5 ms p99 | Medium without strong origin | High | Viral 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.
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 = 0and 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.
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.
What is the difference between a URL shortener and a link tracker?
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
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.

