Caching Strategy for a URL Shortener: TTL, Warmup, Priming, and Graceful Degradation
A deep dive into our Redis caching architecture — how we keep 99% of redirects cache-hot, validate stale data, and survive Redis outages.
Every redirect at hrva.cc goes through Redis first. The database is the fallback, not the primary path. Here's how we designed our caching layer to be fast, correct, and resilient.
Why Cache at All
The redirect endpoint (GET /{short}) is the hottest path in the system — every click
on every short URL hits it. Without caching, every redirect requires two database queries: one
to check if the URL exists and is active, and another to read the destination URL. With caching,
a single Redis lookup replaces both queries. The database is only touched when the cache is cold
or the cached data is stale.
Cache Layout
We use a single Redis cache named urls. Each entry maps a short URL code to a
serialized UrlResponse object containing the destination URL, active status,
expiration date, visit count, and visit limit. The cache is configured with a 7-day TTL —
entries expire after a week regardless of access.
Validation on Every Cache Hit
A long TTL means entries can become stale. Every cache hit runs isUrlRedirectValid(),
which checks three things from the cached data:
- Is the URL still active? (wasn't deactivated by owner or scheduler)
- Has the expiration date passed?
- Has the visit limit been reached?
If any check fails, the entry is evicted from cache and the request falls through to the database. This gives us the performance of a 7-day TTL with the correctness of real-time validation.
Cache Warmup
When the application starts, the CacheWarmup component loads the 20 most visited
URLs, the 20 most recently created, and the 20 most recently accessed into Redis. This runs
asynchronously after startup, so the most popular links are cache-hot from the beginning.
Cache Priming
New URLs are cached immediately at creation time. When a user shortens a URL, the response is written to Redis before the HTTP response is sent. The first visitor gets a cache hit.
Eviction Strategy
When a URL is deactivated, activated, updated, or deleted, the corresponding cache entry is evicted immediately. Deactivation and activation evict by key, delete evicts all entries (it's a rare operation). The scheduled URL deactivation task (runs every minute) does not evict cache — expired URLs fail validation on cache hit, so the database is only queried when necessary.
Graceful Degradation
Redis is not a hard dependency. A custom CacheErrorHandler catches every Redis
failure — get, put, evict, clear — logs a warning, and returns null. The application falls
through to the database without throwing an error. During a Redis outage, redirects are
slightly slower (two database queries instead of one cache lookup), but the site stays up.
The admin dashboard tracks redirect timing separately for cache hits and cache misses via Micrometer timers, so we can monitor cache effectiveness in real-time.