
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Building a URL Shortener System Design is a classic engineering challenge because it appears simple but exposes deep distributed systems trade-offs at scale. While generating a short link is trivial for a single server, handling billions of redirects with low latency requires careful choices around ID generation, caching layers, and database partitioning. This guide moves past high-level theory to the concrete configurations and architectural decisions you need to implement a production-grade service in 2026.
How do you architect the core components when building a URL Shortener System Design?
The foundation of any reliable shortener is separating the write path (link creation) from the read path (redirection). These two workflows have fundamentally different consistency and latency requirements. When deciding between monolith and microservices, I recommend starting with a modular monolith where these paths are distinct modules; only split them into separate services when your write volume exceeds 10K/s or your read cache hit ratio drops below 95%.
Your API server must handle two distinct endpoints. The POST /api/v1/shorten endpoint validates the long URL, generates a unique short code, persists the mapping, and returns the result. This is a write-heavy operation that demands strong consistency. The GET /{shortCode} endpoint handles redirection. This path is extremely read-heavy and should tolerate eventual consistency for analytics updates, but requires strict consistency for the redirect target itself. Never perform database writes on the redirect path; use asynchronous event queues for click tracking to keep p99 latency under 50ms.
Choosing the right data model
A common mistake is over-engineering the schema early. For PostgreSQL or MySQL, a minimal table suffices:
CREATE TABLE url_mappings (
id BIGINT PRIMARY KEY,
short_code VARCHAR(10) NOT NULL UNIQUE,
long_url TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ,
owner_id UUID
);
CREATE INDEX idx_short_code ON url_mappings(short_code);
CREATE INDEX idx_expires_at ON url_mappings(expires_at) WHERE expires_at IS NOT NULL; Note the partial index on expires_at. Most links never expire, so indexing all rows wastes space. If you are evaluating storage engines, my comparison of MariaDB vs MySQL covers performance nuances relevant to this write-once-read-many workload. For NoSQL approaches, DynamoDB or Cassandra work well if you partition by short_code, but relational databases with proper caching often outperform them for this specific access pattern due to simpler operational overhead.
What is the best ID generation strategy for a URL shortener?
ID generation is the most critical decision when building a URL shortener system design. You need unique, compact, URL-safe identifiers without coordination bottlenecks. Avoid UUIDs; their 128-bit entropy produces strings too long for "short" URLs. Instead, use Base62 encoding (a-z, A-Z, 0-9) which yields 62^N combinations. A 7-character Base62 string provides 3.5 trillion unique values—sufficient for most global-scale services.
Implementing the Key Generator Service
For production scale, deploy a dedicated Key Generator Service (KGS) that pre-allocates ID ranges. Each API server requests a batch (e.g., 1 million IDs) and serves them from local memory until exhausted. This eliminates database round-trips during link creation entirely.
// Pseudocode for KGS range allocation
// Database table: key_ranges (id, start_val, end_val, assigned_to, assigned_at)
func AllocateRange(serverID string, batchSize int64) (int64, int64) {
tx := db.Begin()
// Atomically claim next available range using SELECT FOR UPDATE SKIP LOCKED
row := tx.QueryRow(`
UPDATE key_ranges
SET assigned_to = $1, assigned_at = NOW()
WHERE assigned_to IS NULL
ORDER BY start_val ASC
LIMIT 1
RETURNING start_val, end_val
`, serverID)
var start, end int64
row.Scan(&start, &end)
tx.Commit()
return start, end
}
// On API server: convert allocated integer to Base62
func EncodeBase62(num int64) string {
chars := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
result := make([]byte, 0, 7)
for num > 0 {
result = append(result, chars[num % 62])
num /= 62
}
// Reverse and pad to fixed length
reverse(result)
return string(result)
} This approach gives you horizontal scalability without coordination overhead. If an API server crashes with unused IDs, those IDs are simply lost—a perfectly acceptable trade-off given the vast Base62 keyspace. For deeper database tuning to support the KGS backing store, see my MySQL performance tuning guide.
How do you optimize caching and redirection performance?
Redirect traffic is typically 100x–1000x higher than creation traffic. Your cache layer determines whether your system survives viral spikes. Use Redis as your primary cache with a TTL matching your link expiration policy. For links without expiration, use a 24-hour TTL with lazy refresh.
| Caching Strategy | Hit Ratio | Latency Impact | Complexity | Best For |
|---|---|---|---|---|
| Cache-Aside (Lazy Load) | 95–99% | +2ms on miss | Low | General purpose, safe default |
| Read-Through | 98–99.5% | Consistent low | Medium | Predictable latency SLAs |
| Write-Through | 99%+ | +5ms on write | High | Strong consistency required |
| Multi-Tier (L1 Local + L2 Redis) | 99.5%+ | <1ms on L1 hit | High | >100K RPS per node |
For most teams, cache-aside is the correct starting point. Implement it defensively:
func GetRedirect(shortCode string) (string, error) {
// Check cache first
cached, err := redis.Get(ctx, "url:"+shortCode).Result()
if err == nil {
return cached, nil
}
// Cache miss: query database
mapping, err := db.GetByShortCode(shortCode)
if err != nil {
return "", ErrNotFound
}
// Populate cache with TTL
ttl := 24 * time.Hour
if mapping.ExpiresAt != nil {
ttl = time.Until(*mapping.ExpiresAt)
}
redis.Set(ctx, "url:"+shortCode, mapping.LongURL, ttl)
return mapping.LongURL, nil
} Add a local in-process cache (like Ristretto or BigCache in Go) as L1 for hot keys. This eliminates network serialization for frequently accessed links. Monitor your cache hit ratio as a golden signal; if it drops below 95%, investigate cache eviction policies or increase Redis memory immediately.
How do you handle database scaling and analytics separation?
Your redirect database and analytics database have opposing optimization goals. Redirects need fast point lookups with strong consistency. Analytics needs high-throughput writes and complex aggregations. Never mix these workloads in the same database instance.
Sharding the redirect database
When your mapping table exceeds 100 million rows or 50GB, shard by short_code hash. Consistent hashing distributes load evenly and simplifies resharding. Application-level sharding is preferred over database-native sharding for predictable routing:
- Compute
shard = hash(short_code) % Nwhere N is your shard count - Maintain a shard map in configuration or etcd for dynamic rebalancing
- Route queries directly to the correct shard—no scatter-gather
- Pre-warm new shards before migration to avoid cold-cache penalties
For analytics, stream click events to Kafka or SQS asynchronously. Consumers batch-write to ClickHouse or BigQuery. This decouples redirect latency from analytics throughput completely. If you need to set up replication for the redirect database's high availability, refer to my guide on PostgreSQL replication and high availability for battle-tested configurations.
Final recommendations for production deployment
Building a URL Shortener System Design successfully means resisting premature complexity while planning for genuine scale points. Start with a single PostgreSQL instance, Redis cache, and the Key Generator Service. Add sharding only when monitoring proves necessity. Instrument everything with OpenTelemetry from day one—redirect latency percentiles and cache hit ratios are non-negotiable observability metrics. Security matters equally: validate all input URLs against SSRF attacks, implement rate limiting per IP and API key, and never expose internal IDs in short codes. If you are designing this system for a team or preparing for production deployment, reach out to discuss architecture review or implementation support tailored to your traffic profile and compliance requirements.