url-redirection-deep-dive.md

URL Redirection & Redis Caching: Deep Dive (Snips Project)

1. The Frontend & Browser Flow (When a user clicks a link)

Before the backend processes a redirect, it's important to understand what happens in the browser and how the frontend is involved (or intentionally bypassed).

  1. The Click: A user clicks a shortened link (e.g., https://snips.ly/xyz123) on Twitter, in an email, or types it into their browser address bar.
  2. The Browser Request: The browser initiates a standard HTTP GET /xyz123 request to the server.
  3. Bypassing the React SPA (Normal Flow):
    • Even though Snips has a React frontend, a standard short link click never loads the React application.
    • The Express backend is configured to intercept the GET /:slug route before any static frontend files (like index.html) are served.
    • The backend processes the request, updates analytics, and sends back an HTTP 302 Found response with a Location: <original-url> header.
    • The browser receives this 302 response and instantly navigates to the destination. The user never sees the Snips UI.
  4. The Exception (Password-Protected Links):
    • If the backend discovers the link requires a password, it cannot redirect the user to the destination.
    • Instead, the backend issues a 302 redirect pointing to a specific frontend route: /unlock/xyz123.
    • The browser follows this redirect, but this time, the Express server serves the React SPA.
    • React Router takes over on the client side, renders the UnlockLink.jsx page, and prompts the user for a password.
    • Once the user submits the correct password, the React frontend manually forces the final redirect using standard browser APIs (window.location.href = destinationUrl).

2. First-Principles Explanation

At a high level, URL Redirection is the process where a web server tells a browser, "The page you are looking for is actually over here." This is done using HTTP Status Codes. A 301 Redirect means "Moved Permanently" (the browser remembers this forever), while a 302 Redirect means "Found / Moved Temporarily" (the browser will ask the server again next time).

Caching is the technique of storing a copy of frequently accessed data in a fast, temporary storage layer (usually RAM) so you don't have to fetch it from the slower primary database (which reads from disk). We use a Read-Aside Cache pattern with Redis (an in-memory data store). When a request comes in, the application first asks Redis, "Do you have this data?" If yes (Cache Hit), it returns it instantly. If no (Cache Miss), it queries the database, saves a copy in Redis, and then returns it.

3. Why It's Used Here (Grounded in the Code)

In snips, the most frequent operation is redirecting a user from a short slug (e.g., /xyz123) to a long originalUrl.

  1. Why Redis? If a shortened link goes viral on social media, it might receive thousands of clicks per second. If we queried PostgreSQL for every single click, we would exhaust the database connection pool, and the server would crash. Redis stores the link data in memory and can handle hundreds of thousands of reads per second, protecting the database.
  2. Why 302 Redirect? If we used a 301 Permanent Redirect, the user's browser would cache the destination. The second time they click the short link, their browser would bypass our server entirely. Because snips features heavy analytics tracking (devices, location, timeseries), we must intercept every single click. A 302 ensures the browser always asks our server first.

4. Line-by-Line Walkthrough of the Implementation

The Cache Fetching Logic (backend/controller/redirectController.js)

const getLinkData = async (slug) => { // 1. Attempt to fetch the link metadata from Redis memory const cached = await redisClient.get(`slug:${slug}`); // 2. Cache Hit: Data is stored as a JSON string in Redis, so we parse it back to a JS object and return early. if (cached) return JSON.parse(cached); // 3. Cache Miss: Query the PostgreSQL database via Prisma const link = await prisma.link.findUnique({ where: { slug } }); if (!link) return null; // 4. Construct the specific data we want to cache (we don't need the whole DB row) const cacheData = { linkId: link.id, originalUrl: link.originalUrl, isActive: link.isActive, expiresAt: link.expiresAt, password: link.password, }; // 5. Save it to Redis. We must stringify it. // 'EX: 60 * 60 * 24' sets a Time-To-Live (TTL) of 24 hours. redisClient.set( `slug:${slug}`, JSON.stringify(cacheData), { EX: 60 * 60 * 24 } ); return cacheData; };

The Redirection Logic (backend/controller/redirectController.js)

const handleRedirect = catchAsync(async (req, res) => { // ... (code handling 404s, paused links, and password protection) // 1. Send the 302 HTTP status and the destination URL to the browser. // The user's redirect happens immediately at this line. res.redirect(302, linkData.originalUrl); // 2. Non-blocking Analytics Tracking. // Notice there is no 'await' here. The server processes this in the background // so the user doesn't have to wait for the database writes. trackClick(linkData.linkId, req).catch(console.error); });

The Password-Protection Escape Hatch (Frontend Integration)

Note: Normally a 302 redirect bypasses the React frontend entirely. But if a link is password-protected, the backend intercepts it and redirects the user to our frontend React app instead.

// In backend/controller/redirectController.js if (linkData.password) { const frontendUrl = process.env.FRONTEND_URL || "http://localhost:5173"; // Preserve the original referrer so analytics aren't lost const originalReferrer = req.headers["referer"] || "direct"; const encodedRef = encodeURIComponent(originalReferrer); // Redirect browser to the React frontend unlock page return res.redirect(302, `${frontendUrl}/unlock/${slug}?ref=${encodedRef}`); }

Once the user lands on the frontend (frontend/src/pages/UnlockLink.jsx), they submit the password. The React app catches the response and programmatically forces the final redirect using standard DOM APIs, bypassing React Router:

// In frontend/src/pages/UnlockLink.jsx const handleSubmit = async (e) => { e.preventDefault(); try { // Send password to the backend unlock API const { data } = await axios.post(`${server}/${slug}/unlock`, { password, referrer: originalReferrer }); // Force the browser to navigate to the real destination URL window.location.href = data.url; } catch (err) { toast.error(err.response?.data?.message || "Incorrect password"); } };

Cache Invalidation (backend/controller/linkController.js)

Note: Caching introduces a new problem: Stale Data. If a user pauses a link, the cache still thinks it's active.

const toggleLink = catchAsync(async (req, res) => { // ... (code updating the database to toggle isActive) // 1. Explicitly delete the cached entry from Redis. // The next time someone clicks this link, it will force a Cache Miss // and load the fresh 'isActive' state from PostgreSQL. await redisClient.del(`slug:${link.slug}`); // ... });

5. Alternatives Comparison Table

ApproachProsConsWhy I didn't use it
Node.js In-Memory Cache (e.g., Map or node-cache)No external dependencies, extremely fast.Cache is lost on server restart. Does not share data across multiple server instances horizontally.Snips is designed to scale. If deployed across 3 Node.js instances behind a load balancer, they need a centralized cache (Redis) to share link states.
CDN Edge Caching (e.g., Cloudflare)Redirect happens at the network edge, zero load on the origin server.Impossible to track accurate, real-time backend analytics.Our core feature is analytics. We have to intercept the request to track the IP, device, and referrer.
301 Permanent RedirectBrowsers cache it, reducing server load naturally. Better for traditional SEO.Bypasses the server on subsequent clicks, breaking analytics completely.Analytics tracking is a non-negotiable product requirement.

6. Edge Cases & Failure Modes (Honest Gaps)

  1. The Cache Stampede (Thundering Herd)
    • The Problem: Imagine a link with 5,000 clicks per second. If the 24-hour Redis TTL expires, the key disappears. Suddenly, 5,000 concurrent requests all check Redis, see a Cache Miss, and all query PostgreSQL for the exact same slug at the exact same millisecond. Postgres will choke.
    • Handled?: No. The current code has no locking mechanism. To fix this, I would implement a Redis lock (mutex) during a cache miss, forcing subsequent requests to wait a few milliseconds until the first request repopulates the cache.
  2. Redis Unavailability
    • The Problem: If the Redis server goes down or restarts, await redisClient.get() will throw an error or time out.
    • Handled?: Poorly. Because getLinkData doesn't wrap the Redis calls in a try/catch block that falls back to Postgres, a Redis outage will bubble up an error and crash the request, meaning no links will redirect. A resilient system should treat the cache as optional and fallback gracefully to the DB if the cache is down.
  3. Analytics Tracking Failure
    • The Problem: trackClick runs asynchronously in the background. If the Node.js process crashes or restarts exactly after res.redirect() but before the Prisma transaction finishes, that click data is permanently lost.
    • Handled?: Accepted Tradeoff. For a URL shortener, losing 0.01% of click data during a deployment is usually acceptable compared to the UX penalty of forcing the user to wait for the database write before redirecting.

7. Spoken Summaries (For Rehearsal)

60-Second Version (The Elevator Pitch)

"For the core redirection engine, performance and analytics were my top priorities. I implemented a read-aside caching layer using Redis. When a user clicks a short link, the server checks Redis first. If it's there, we bypass PostgreSQL entirely, allowing the app to handle massive spikes in traffic. I specifically chose to use a 302 Temporary Redirect instead of a 301. While a 301 is better for SEO, it causes the browser to cache the redirect locally, which would bypass our server on repeat clicks and break our analytics engine. The only time the React frontend gets involved during a redirect is if the link is password-protected—in that case, the backend issues a 302 to our frontend's unlock page, and the frontend manually redirects the user via window.location.href after successful authentication."

5-Minute Version (The Deep Dive)

"Let me walk you through the lifecycle of a link click in my application. When a request hits the /slug endpoint, we enter the handleRedirect controller.

The first thing we do is call getLinkData, which implements a Read-Aside caching pattern with Redis. We check if slug:the-slug exists in memory. If it does, we parse the JSON and return it instantly. If it doesn't, we query PostgreSQL via Prisma, structure just the data we need—like the destination URL, expiration date, and active status—and save it back to Redis with a 24-hour TTL. This protects the database from being overwhelmed by high-traffic links.

Crucially, any time a user modifies a link—say they pause it or change the destination—I ensure we actively invalidate the cache by calling redisClient.del().

Once we have the link data, we issue a 302 Found redirect to the destination. I made a deliberate architectural choice to use a 302 instead of a 301 Permanent Redirect so the browser hits our server every single time, which is mandatory for our analytics engine. The one major exception is if the link is password-protected. In that scenario, a direct backend redirect won't work. Instead, the backend issues a 302 redirect pointing the user to our React frontend (UnlockLink.jsx). The frontend renders a password prompt, securely sends it to our unlock API, and upon success, forces the browser to the final destination using window.location.href.

Finally, after calling res.redirect(), I fire off the trackClick function. Notice that I purposely do not await this function. Gathering geolocation and writing to the database takes time. By executing it in the background, the user gets a lightning-fast redirect, and the analytics are processed asynchronously.

While this architecture is fast, if I were to scale it further, I'd need to address a potential cache stampede by implementing a mutex lock on cache misses, and add fallback logic in case the Redis instance goes offline."


8. Mock Interview Questions

Q1 (Easy): Why do you use Redis for caching instead of just storing the link data in a JavaScript object or Map? Answer: "While a standard JS Map is fast, it's scoped to the specific Node.js process. If I scaled this app horizontally across three servers behind a load balancer, each server would have a different, out-of-sync cache. Redis acts as a centralized caching layer that all instances can share, and it persists across server restarts."

Q2 (Medium): You are using a 302 Redirect. What is the difference between a 301 and a 302, and why does it matter here? Answer: "A 301 is a permanent redirect; browsers will cache it aggressively and stop asking the server for the destination. A 302 is temporary, forcing the browser to hit our server every time. Because Snips tracks detailed click analytics, a 301 would break our core feature since repeat clicks wouldn't reach our backend."

Q3 (Hard): I see getLinkData sets a 24-hour TTL. What happens if a wildly popular link expires in Redis at exactly 12:00 PM? Answer: "This creates a 'Cache Stampede' or 'Thundering Herd' problem. If 1,000 users click the link at 12:00:01 PM, all 1,000 requests will check Redis, see a cache miss, and simultaneously execute a findUnique query against PostgreSQL for the exact same data. This could overwhelm the DB connection pool. To fix it, I would implement a Redis-based distributed lock. The first request acquires the lock and queries the DB, while the other 999 requests wait a few milliseconds for the cache to be repopulated."

Q4 (Hard): Why did you choose not to await the trackClick function? What is the tradeoff? Answer: "trackClick makes an external HTTP call to a geolocation API and executes a database transaction. If I awaited it, the user would be staring at a blank screen waiting for those network calls to finish before being redirected. By running it asynchronously, the user gets an instant response. The tradeoff is that if the Node process crashes milliseconds after the redirect but before the DB write finishes, we lose that analytics event."

Q5 (Medium): A standard short link bypasses your React frontend completely. How do you handle password-protected links where the user actually needs a UI to type the password? Answer: "If the backend detects the link has a password in the Redis cache, it changes the redirect destination. Instead of sending the user to the originalUrl, it issues a 302 redirect to our React app's /unlock/:slug route, passing the original referrer as a URL query parameter so we don't lose that analytics data. The React app then takes over, prompts for the password, hits a separate unlockLink API endpoint, and if successful, uses window.location.href to send the user to their final destination."