Async Everything: Keeping Redirects Fast While Counting Millions of Visits
How we use Spring's TaskExecutor to fire-and-forget visit tracking, email sending, and cache warmup — keeping the redirect path under 5ms.
Every click on a short URL does two things: redirect the user to the destination, and record the visit. The redirect must be fast. The visit recording can wait. Here's how we separate them.
The Redirect Path
When a user visits hrva.cc/abc123, the backend checks Redis for cached URL data.
If found and valid (not expired, not deactivated, under visit limit), the redirect response is
returned immediately while the visit counter is incremented asynchronously in a background thread.
Async Visit Tracking
Spring's TaskExecutor runs the visit tracking in a separate thread. The method
fireVisitTracking loads the URL entity from the database (it's already cached for
the redirect, but we re-fetch the entity for the write), checks for duplicate IPs, increments
the counter via url.onVisit(), and saves. If the visit exceeds the limit, the
URL is automatically deactivated.
This means the redirect response time is independent of the database write latency. A slow database write doesn't slow down the redirect. The user gets their 302 response in milliseconds while the visit is recorded behind the scenes.
Async Email Sending
Email is another fire-and-forget operation. Welcome emails, password resets, expiry notifications,
and malware alerts all go through @Async methods. The tryToSendEmail
method attempts to send up to 3 times with exponential backoff, but the caller doesn't wait —
the email is queued and delivered in the background.
Async Cache Warmup
On application startup, the CacheWarmup component loads the 20 most visited,
20 most recent, and 20 most recently accessed URLs into Redis. This runs asynchronously
after the application context is ready, so the application starts serving requests immediately
while the cache fills in the background.
What We Don't Make Async
Not everything should be async. URL creation, deletion, and schema migrations are synchronous — the caller waits for confirmation. Authentication is synchronous (you need to know if login succeeded before proceeding). The rule: if the user needs to see the result, it's synchronous. If it's bookkeeping that happens after the response, it's async.