Caching

Lesson 14 of 14 · 40 min

x
100%

Final Project: Build a Cached URL Shortener API

Put everything together in a URL shortener API that demonstrates production-grade caching. The read path (resolve a short code to a URL) should be extremely fast — this is 100x more frequent than the write path (create a short URL). Implement cache-aside with Redis as L2 and an in-process LRU as L1. Set TTL on cached URLs (1 hour) plus event-based invalidation when a URL is updated or deleted. Add jittered TTLs to prevent thundering herd on popular links. Use HTTP Cache-Control headers on the redirect response for CDN and browser caching of stable URLs. Benchmark with k6 or a simple load test script: run 1000 concurrent requests against /:code with an empty cache (cold start), then with a warm cache. You should see 10–50x latency improvement and near-zero database queries at high hit ratios. Track hit ratio, p99 latency, and Redis memory usage throughout.

Before
Uncached — every redirect hits the database
app.get('/:code', async (req, res) => {
  const url = await db.query(
    'SELECT long_url FROM urls WHERE code = $1',
    [req.params.code]
  );
  if (!url) return res.status(404).end();
  res.redirect(302, url.long_url);
});
// 10k redirects/sec = 10k DB queries/sec
After
Full caching stack — L1 + L2 + HTTP headers
const l1 = new LRUCache({ max: 1000 });

app.get('/:code', async (req, res) => {
  const { code } = req.params;

  // L1 in-process
  const local = l1.get(code);
  if (local) {
    res.set('Cache-Control', 'public, max-age=300');
    return res.redirect(302, local);
  }

  // L2 Redis
  const cached = await redis.get(`url:${code}`);
  if (cached) {
    l1.set(code, cached);
    res.set('Cache-Control', 'public, max-age=300');
    return res.redirect(302, cached);
  }

  // Database (cache miss)
  const row = await db.query(
    'SELECT long_url FROM urls WHERE code = $1', [code]
  );
  if (!row) return res.status(404).end();

  const ttl = 2700 + Math.random() * 600; // jittered 45–55 min
  await redis.setex(`url:${code}`, ttl, row.long_url);
  l1.set(code, row.long_url);
  res.set('Cache-Control', 'public, max-age=300');
  res.redirect(302, row.long_url);
});

Key Takeaway

A cached URL shortener proves the full stack — L1 + L2, TTL with jitter, HTTP headers, and load testing to prove the improvement.