URL Shortener in Go
A build-in-public series that starts as a 173-line, single-binary URL shortener and grows it toward production one constraint at a time — persistence, horizontal scale, collisions, custom codes, analytics.
What this is
A URL shortener built in the open, deliberately starting from the smallest thing that works end to end and then breaking it on purpose to fix what breaks. Each phase ships, gets written up, and sets up the constraint the next phase has to solve. The accompanying blog series walks the decisions and the failure modes in detail.
Where it is now (Part 1)
The first cut is ~173 lines of Go across three files — main.go,
handlers.go, store.go — plus 47 lines of HTML templates and a stylesheet.
It compiles to a single ~11.7 MB binary with zero third-party dependencies and,
on loopback against an Apple M1 Pro, serves north of 19,500 requests/second on
every route under ab -n 20000 -c 50.
Three choices define this phase, and each is the simplest defensible option:
- Codes are an FNV-1a hash of the URL, base62-encoded. Deterministic and stateless — the same URL always maps to the same code, with nothing to coordinate. The cost is silent collisions and codes you can't revoke or customize.
- Storage is a
mapbehind async.RWMutex. Trivially simple, and gone the moment the process restarts or you run a second copy. - The frontend is HTMX, not a framework. The server returns rendered HTML fragments; the client ships near-zero JavaScript and no build step.
Roadmap
- Persistence + horizontal scale — the in-memory map loses everything on restart and can't be replicated; two instances mean two disjoint sets of links. This is the constraint Part 2 exists to fix.
- Collision handling — harmless-looking today, it becomes permanent data corruption the moment storage is durable and shared.
- Server-side validation — Part 1 trusts the browser's
type="url"; a rawcurlsails straight through. - Custom/vanity codes, link expiry, click analytics — none of which the hash-as-code design can support as written.