Zstem

URL Shortener

by Zstem · published 2026-07-16 · 0 forks

APIDatabaseEventsArchitectureDocsSequenceserverlessevent-drivenfull-stack

One-read 301 hot path, DynamoDB TTL expiry, async click analytics via SQS

Open & fork this on Zstem →
BrowserLinks APIRedirect FnCreate Link FnLinks TableClick Events QueueAnalytics FnClick RollupsURL Shortenerzstem.design

Architecture

Event flow

Sequence

Participants: Visitor, Links API, Redirect Fn, Links Table, Click Queue

API contract

Database

links
slugShort-link slug — partition key. Vanity or generated (base62, collision-checked with a conditional put).
target_urlWhere the 301 points.
created_at
created_byOptional owner for a future dashboard.
ttlEpoch seconds. DynamoDB TTL deletes expired links automatically — the redirect answers 410 between logical expiry and physical delete.
click_rollups
slugPartition key — FK to links.slug.
daySort key: one item per link per day.
clicksIncremented with ADD by the analytics consumer — no read-modify-write.

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 SendMessages 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: attributenotexists(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).

Open & fork this on Zstem →