URL Shortener
One-read 301 hot path, DynamoDB TTL expiry, async click analytics via SQS
Architecture
- Browser — Visitor clicking a short link, or the dashboard creating one
- Links API — HTTP API: POST /links (create), GET /{slug} (redirect hot path)
- Redirect Fn — The hot path: one DynamoDB read → 301. Emits link.clicked to SQS fire-and-forget so analytics never slows a redirect
- Create Link Fn — Validates the target URL, generates a collision-checked slug, writes the link with optional TTL expiry
- Links Table — slug → target URL. DynamoDB TTL deletes expired links automatically — no cleanup job
- Click Events Queue — Buffers link.clicked events; absorbs redirect traffic spikes so analytics scales independently
- Analytics Fn — Batch-consumes click events and increments per-day rollup counters (ADD — idempotent-ish, cheap)
- Click Rollups — Per-link, per-day click counts powering the stats view
Event flow
- Redirect Fn — Emits link.clicked after every successful 301 — fire-and-forget, never blocks the redirect
- Click Events Queue — SQS buffer — absorbs spikes, retries consumer failures, DLQ after 3 attempts
- Analytics Fn — Batch-consumes (25/poll) and ADDs per-day rollup counters
- link.clicked — One click on a short link; consumed for per-day rollups
Design Notes
- # URL Shortener — design notes A deliberately small serverless system that optimises ONE thing: the redirect hot path. ## The hot path is sacred `GET /{slug}` does exactly one DynamoDB read and answers **301**. Everything else — click tracking, stats, rollups — happens **after** the redirect, asynchronously. The visitor never waits for analytics. - **One read:** `GetItem` on the `links` table by slug (partition key). Single-digit ms. - **301, not 302:** browsers and crawlers cache a permanent redirect, so repeat clicks may not even hit us. (Trade-off: cached redirects skip click tracking — acceptable; rollups are a trend signal, not billing.) - **Fire-and-forget click events:** the redirect Lambda `SendMessage`s `link.clicked` to SQS and does NOT await delivery guarantees beyond the SDK call. A dropped click is a rounding error; a slow redirect is a product failure. ## TTL expiry without a cleanup job Links can carry an `expiresAt`, stored as the DynamoDB **TTL attribute** (epoch seconds). DynamoDB deletes expired items itself — no scheduled sweeper Lambda, no scan costs. Between logical expiry and physical delete the redirect checks the TTL and answers **410**. ## Async analytics via SQS - The queue absorbs redirect spikes so the analytics consumer scales independently. - The consumer processes batches of 25 and increments per-link-per-day counters with an `ADD` update — no read-modify-write, no contention. - Failures retry via the queue; poison messages land in a DLQ after 3 attempts. ## Slug generation Vanity slugs are accepted if free; generated slugs are short base62, written with a `ConditionExpression: attribute_not_exists(slug)` and regenerated on the (rare) collision — the same conditional-put pattern used for any uniqueness guarantee in DynamoDB. ## What this design deliberately skips Custom domains per user, auth on link creation (add Cognito in front of POST /links), real-time dashboards (rollups are eventually consistent by design), and bot filtering (user-agent heuristics can be added in the analytics consumer without touching the hot path).