The public door moves to Go

milestone architecture, go, api

By late August the workers were already in Go and Nest was down to one job: being the door. It was also still the fattest process in the house, and the Swagger describing it didn't match what Angular was unwrapping at the other end. Two runtimes that both believe they own HTTP will eventually disagree about a wage.

The temptation with something like this is the midnight rewrite. Read the controllers, write the Go version, flip the host, go to bed. I know how that ends.

So the first job wasn't Go at all. It was repairing the Nest OpenAPI until every mounted operation described the envelope it actually returns — { statusCode, message, data, timestamp } — with the live DTOs attached, and then committing that snapshot so drift becomes a failing check instead of folklore. Photograph the building before you copy it. Designing /v2 from drifted decorators would have blessed payloads nobody was sending, and the slow, boring repair was the load-bearing part of the whole thing.

Then go/cmd/api, sitting beside the worker and the scheduler. Health endpoint first, then the catalog, and a per-resource version map on the Angular side so one page could move without flipping the entire app over.

After that it was slices, in order. Catalog and search. Dashboard and /me. Matches and competitions. Team reads, then team mutations. Players, transfers, finances. Inbox, news and press. The leftover club bits. FA admin last. Each slice freezes as it lands — once a domain is Go, new behaviour for that domain is Go, no exceptions. That's a strength, because game, admin and workers can't fork a resource between them. It's also a rod for my own back, because a late slice doesn't get to quietly rename a URL the early slices already published.

On the twenty-seventh, the leftover /v1 routes started answering with a gone-away and the public API host started pointing at Go.

Nest stays on as the librarian — TypeORM migrations with synchronize off everywhere as it has always been, seeders, backup-restore. It just doesn't serve requests anymore.

A few things I'd had in Nest and missed on night one, when local smoke tests hadn't caught them: response caching, the process version, request observability. Those got rebuilt on the new door rather than smuggled back into the old one, which is how these projects never finish.

Memory was the easy argument. A dedicated API process sits in a sliver of what the Nest request process wanted, and this whole thing is staged on a Raspberry Pi, so a sliver matters. But the real argument is that HTTP, queues and schema want three different failure domains. A Saturday shouldn't be able to take the login screen with it.

Speaking of which, the same window fixed two things in that family. A youth player appearing the same day no longer fails an entire senior simulation job — the sheet refills, it doesn't invent an eleven nobody picked. And match simulation moved onto its own workers, so a round of fixtures stops starving every other queue in the farm.

Things I passed on. Folding the API into dugout-worker, which would have search and the match engine sharing one memory budget. Generating Go types from Nest's boot-time /docs-json, which fossilizes the exact Swagger I'd just admitted was wrong. Moving migrations to a Go tool in the same programme, when the one rule I don't bend is that synchronize stays off and I wasn't going anywhere near it mid-cutover. And leaving /v1 in place forever as a compatibility museum, which is precisely how two wage endpoints end up disagreeing.

What I get out of it is the boring kind of freedom. New manager screens can be Go from the first line, and this diary can talk about the club again instead of about which process owns /teams.

← All entries