System design

When a URL Shortener Becomes a Real-Time Analytics System

A worked system-design exercise: burst sizing, fast redirects, durable click events, and live campaign analytics without coupling their failure modes.

On this page

This is a hypothetical design exercise, revised from a practice session. The numbers below are assumptions for reasoning about the design, not results from a deployed customer system.

A short link has a simple job: resolve a slug and redirect the browser. But if the customer is watching a campaign dashboard during an SMS send, the product is also an analytics system. That second requirement changes what it is safe to cache, what must survive a crash, and how we measure success.

Start with the burst, not the monthly total

Assume 4,000 business customers, two million active links, and 200 million redirects a month. The redirect target is p99 below 100 ms with 99.95% availability. The dashboard should show campaign activity within 60 seconds and retain two years of history.

Two hundred million requests over a 30-day month is about 77 requests per second. That is a useful cost input, but a poor peak-capacity target.

For a campaign sending five million messages, a 10–15% click-through rate produces 500,000–750,000 clicks. If 60% arrive in the first half-hour, the window averages about 167–250 redirects per second. Several campaigns can overlap, and a thirty-minute average conceals shorter spikes. A 2,000-request-per-second test might be a reasonable stress scenario, but it would be a chosen margin, not a number derived from these assumptions.

The next questions are concrete: how many large campaigns may overlap, how narrow is the busiest minute, and does the latency target include the user’s network? Those answers determine the load-test shape.

Cache the mapping without hiding the click

A compact slug-to-destination mapping of 200 bytes per link is roughly 400 MB of logical data. Redis needs additional space for keys, allocator overhead, replicas, and operational headroom. Measure actual memory consumption with representative entries before selecting an instance size.

Use a durable mapping store with Redis in front. Keep a short, bounded fallback path for cache misses, protect the store against a cold-cache surge, and invalidate mappings when customers change destinations. A memory lookup helps latency; it does not prove the end-to-end p99 target.

For this analytics-oriented product, the proposed redirect response is 302 with Cache-Control: no-store. A cached redirect can bypass the service on subsequent visits, so counting origin requests would undercount activity. The status code alone is not a caching policy: HTTP caching rules and the 302 response semantics both matter. This records requests that reach the service, not proof that a human viewed the destination.

Separate redirect availability from analytics completeness

The proposed flow has two paths:

Click → redirect service → Redis mapping → 302 response
                 │              ↑
                 │       durable mapping store
        bounded event enqueue
             durable queue
       enrichment and aggregation
           ↙              ↘
   live counters       historical store
           ↓              ↓
       campaign dashboard / exports

An unacknowledged, in-memory “fire and forget” task is not durable. The process can exit after serving the redirect and before saving the event.

For this exercise, choose availability over complete click capture: attempt a durable queue write within a small latency budget, return the redirect if the queue times out, and count enqueue failures. The customer-facing consequence is explicit: live and historical totals can undercount during that failure window. Agree the acceptable loss budget before implementing this policy.

If clicks become billing records, revisit that choice. An acknowledged durable write must happen before reporting success, or another durable capture mechanism must own the event. That changes latency and availability. A queue acknowledgment also cannot make the browser’s receipt of the redirect atomic with the recorded event.

Consumers must handle redelivery. Standard SQS queues can deliver a message more than once. Attach an event ID at capture and make each sink’s update idempotent. For live counters, atomically record that ID and increment the relevant counters, with a deduplication retention period covering the replay window. Otherwise, retries inflate the dashboard.

Design the dashboard query before choosing its database

For the last thirty minutes, store minute buckets keyed by tenant, campaign, link, and the dimensions the product actually exposes. Keep the live window bounded. Counting every possible combination of city, device, channel, and link creates a different cardinality problem from counting each breakdown independently.

The warehouse receives raw or appropriately minimized events in batches for longer-range reports. Show a freshness timestamp on the live dashboard and distinguish a quiet campaign from delayed ingestion. Alert on the age of the oldest queued event as well as queue depth: a short queue can still contain stuck work.

Unique recipients need a stated definition. A per-recipient SMS token can count distinct recipient tokens within a campaign; forwarded links mean that is not the same as distinct people. QR links do not provide equivalent identity. A device or network-derived identifier is only an approximation and should not silently become an exact “people” metric.

An exact set may be suitable for a bounded campaign window. At larger scale, HyperLogLog offers approximate cardinality with bounded storage per structure. Decide whether approximate results are acceptable, and show that distinction in exports as well as the UI.

Test the tradeoffs you chose

Before calling this design ready, test:

  • A large send with several overlapping campaigns and a cold cache.
  • A mapping update during a send, including delayed invalidation.
  • A queue outage while redirects continue; verify that loss is observable.
  • Consumer crashes before and after each sink write, followed by replay.
  • A backlog larger than sixty seconds; verify dashboard freshness reporting.
  • Expiry of live buckets and deduplication records without unbounded memory growth.

The design succeeds when its promises match its failure behavior. A fast redirect with unexplained analytics gaps is unfinished. So is a perfect counter that makes customers’ links unavailable whenever the warehouse has a bad day.