Skip to content
← Blog Blog

Road to glory

At a previous job I restructured 250,000 lines of code, and a task that had taken two days started taking three minutes. Nobody got a hundred times better at programming — the restructuring deleted everything that wasn't the work. Then frontend developers started picking up backend tickets. This is the plan to do that again here, and the part of it that gets worse before it gets better.

A trophy on a green at sunrise, binary faint in the grass

Ask a small company where its work actually waits and the answer is almost never capacity. It’s routing.

A change gets described in one sentence, everyone agrees it should happen, and then it sits — because the repository it lives in has exactly one person who can confidently open it. Not because they’re guarding it, but because they’re the only one who knows this project wants a JDK three majors behind the last one you used, and that what looks like the entry point stopped being the entry point in 2021.

We have over seventy repositories at GolfNext, built over thirteen years in half a dozen languages, and for a fair number of them that count of people is one. It’s the subject of this post; everything else is downstream of it.

“Road to glory” is an ambitious name for a plan whose first three steps are boring. The title isn’t ironic: the ambition is for this to become the platform the golf industry runs on, worldwide, and boring is what an engine for that is made of.

The number that actually hurts

Our tech lead has written here about the state of the estate, and I spent my first two weeks finding out which repositories could still start on a fresh machine. Neither account was a complaint. Over seventy repositories is what thirteen years of shipping working software to real golf clubs looks like, and the tidying is only possible because the product works. But there’s a failure mode in it they didn’t name directly, and it’s the one that decides how fast this company can grow.

When a codebase has one maintainer, three things are true at once, and none of them are that person’s fault.

The work queues behind them. Everything that touches that system waits on the one calendar it can wait on. Fine at three things a quarter. Ruinous at three a week.

The knowledge never leaves their head. Not because they’re unwilling to share it, but because nothing takes it out. Documentation doesn’t: people write about the parts they found interesting, which are by definition not the parts a newcomer gets stuck on.

Nobody can check their work. A review by somebody who can’t run the code is a review of the diff’s spelling.

Put those together and you get a system technically owned by the company and practically owned by a person. We have several. So does every company this age.

There’s a second-order cost I only understood by living through it: when a newcomer hits something broken, they cannot tell whether the repository is broken or they are. I lost days to a local database that turned out to be impossible to build from scratch — four migrations, in a chain of 251, that could not run against a fresh import — and for most of those days I assumed I was the problem. That’s the natural assumption in your first fortnight, and nobody could have corrected me, because everyone already set up had a database that predated the break. Eventually I stopped assuming it was me, and fixed it migration by migration.

That tax lands only on new people, which is exactly why it’s invisible to the people who could remove it. It’s also one of the cleaner arguments for this plan: when every repository behaves the same way, a newcomer has a control group. If this one differs from the last five, it’s the repository that’s wrong.

And it scales the wrong way. We’re in eighteen countries with more than four hundred venues and the plan involves a good deal more of both, so a bus factor of one doesn’t stay put as you grow — the same person simply becomes the bottleneck for more things.

What “different” costs, itemised

It’s easy to say a mixed stack is expensive and hard to say where the money goes. Here is where it goes, in the order I’ve actually felt it.

Switching. Move between three repositories in a morning and you pay a small tax at each hop: which build tool, which version, which command, which idioms. Individually trivial. It’s the frequency that gets you, and the frequency is a function of how many different shapes you own.

Integrations. This is the one that matters most to our business and gets discussed the least. Our position in the market is that a club keeps its own hardware and its own management system and we work with what they already run. That means every new partner is another integration, and the cost of an integration is roughly the work itself multiplied by how strange the codebase you’re putting it in happens to be. Write the fifth one into the same shape as the first four and it’s a fraction of the price. Write it into a codebase with its own conventions and you pay the archaeology again.

Onboarding. A new engineer has to learn the company once, and then learn each system separately. The second part is where the weeks go. If every system has the same shape, learning the second one is nearly free, and that’s the difference between a new person being useful in a fortnight and being useful in a quarter.

Review. Standardisation is what makes a real review possible for more than one person. If I can run your code, I can disagree with it.

Agents. Our tech lead has written that the repositories where he trusts a coding agent most are, without exception, the ones somebody has already tidied. That matches my experience exactly. An agent working in a clear, consistent structure produces something you can use. An agent in an unfamiliar one guesses fluently, and you spend your afternoon working out which parts were guesses. Consistency isn’t just nice for humans anymore. It’s the thing that decides whether half our tooling works.

Sameness of shape beats sameness of language

Here’s the part I’d argue with anyone about, because it’s the bit people get backwards.

“Lean tech stack” gets heard as “fewer languages”, and fewer languages is only about a third of the benefit. The three things that decide whether you can walk into an unfamiliar repository and be useful are, roughly in order:

  1. Is the file structure the same? Does a given kind of thing live at a predictable path, under a predictable name, in every repository?
  2. Are the verbs the same? Can you build, test and run it with the commands you used in the last repository?
  3. Is it the same language and framework?

Number three is the one that gets the headline and it’s the least important of the three. I’ve moved between two Java services with completely different package structures and it was harder than moving between a Java service and a TypeScript one that were laid out identically.

Numbers one and two are also enormously cheaper to fix. The verbs we’ve already largely solved: mise puts a repository’s toolchain and its commands in a file in the repository, so build, test and run mean the same thing everywhere they’ve been applied, whatever’s underneath. That was our tech lead’s work and it’s the highest return-on-effort thing anybody has done here.

File structure is the other one, and it’s the one I’d put real effort into, because it’s what you use before you’ve read a single line. You don’t find code by reading it. You find it by guessing a path. Where every repository agrees on where the handlers live, where the database code lives, where the tests live and what the files are called, that guess is right first time, and a search that worked in one repository keeps working in the next. Where they disagree, every repository is somebody else’s filing system and you open four wrong files before the right one — which is most of what the first hour in an unfamiliar codebase actually consists of.

It’s also the cheapest of the three to fix, because it requires no rewrite at all. It’s an agreement, plus the patience to apply it to the file you already have open. Our tech lead did exactly that when he took the React proof of concept into the real repository: a pass on how directories and files get named, before anything got built on top of it. And it’s the part an agent feels most, for the same reason you do — it locates code by predicting where the code should be.

Which is a useful thing to know, because it means most of the benefit of a lean stack is available before you’ve consolidated a single language. It also means language consolidation should be justified on its own merits rather than borrowing the credit for the other two.

Two and a half days → three minutes

So far this is an argument, and arguments are cheap. Here’s why I believe it.

At a previous job I spent a long stretch restructuring about 250,000 lines of code. Not a rewrite. Same language, largely the same logic, reorganised: where things live, what is allowed to depend on what, and what shape a change has to be to fit.

Before it, a simple task — the ordinary recurring kind that makes up most of a week — took two to three days. Afterwards I got curious enough to time myself on one, and it came out at three minutes.

I should be precise about that number, because it’s the kind of figure that gets repeated until it turns into a lie. Three minutes was one task, timed once. Others were five minutes, or ten. What changed wasn’t that every job became a three-minute job. It was that the unit changed: work counted in days was now counted in minutes, and none of it went back.

Even the cautious end of that is a ratio nobody should believe about themselves, so here’s what it actually means. Nobody got a hundred times better at programming. The work itself had always been small — a few minutes of typing, buried under everything that wasn’t the work: finding where the change belonged, reading enough of the surrounding code to be sure, working out what else touched it, and then the part that never appears on a timesheet, which is being frightened of breaking something you don’t understand and moving slowly on purpose because of it.

The restructuring didn’t make the work faster. It deleted everything that wasn’t the work.

That also tells you which tasks collapse and which don’t. Genuinely novel work, where the hard part is deciding what the thing should do, took about as long afterwards as before; structure doesn’t help you think. What collapses is the recurring shape — the fifth integration, another field on a form, the endpoint that looks like the eleven next to it. In an estate like ours, that’s most of the queue.

The result I’d actually point at

Then the part I hadn’t predicted. Frontend developers started picking up backend tickets.

Nobody asked them to. When every service is laid out the same way, the barrier turns out to have been structural rather than a matter of knowledge. That’s the most useful evidence in this post because it isn’t self-reported: a productivity figure is something you say about yourself, and crossing a boundary you’d avoided for years is something you do. It moved the number this post opened with, without hiring anyone.

One more thing, because this blog writes about coding agents constantly and it would be easy to read all of that as an AI story. There was no AI in it. None. Every minute of the gain came from where the code sat and what was allowed to depend on what — which means the payoff isn’t contingent on agents continuing to get better, or on us using them well. Structure paid before any of this existed. What agents do is multiply it, most of all in the repositories that already have it. If they stalled tomorrow the argument in this post would be unchanged.

I want to reproduce that here. Different company, different estate, same mechanism. That refactor is also where this post’s title comes from, and it’s why I can write a plan this confident about work that hasn’t happened yet: I’m not predicting an effect, I’m expecting one I’ve already stood in the middle of.

What the engine is for

A post about making developers more productive can be read the wrong way, so let me close that reading off myself. This is not about needing fewer developers. Nobody here is doing it so the same work can be done by fewer people, and if that were the plan I wouldn’t be writing a blog post about it. The developers here are good. They built a product that works in eighteen countries, under constraints I never had to live with, and losing any of them would set this back rather than forward.

The reason is growth, and I’d rather state the size of it than be coy. We are not trying to get somewhat bigger. The goal is for this to be the platform the golf industry runs on — every club, every range, every country that has both — and the reason that’s sayable out loud rather than embarrassing is that the demand side is already turning up. Clubs who see the product are, in my experience so far, genuinely amazed by it. That reaction is the asset. Nothing in this post creates it, and nothing in this post can substitute for it.

What we don’t have is the engine to serve it at that size. The product is good; the machinery that builds, integrates and supports it is thirteen years of accumulated decisions, and that’s the part that breaks first if demand arrives faster than the team can absorb it. We’re not building the product — it exists, and people who had nothing to do with building it are impressed by it. We’re building the thing underneath that decides how many clubs, how many countries and how many integrations a small team can carry at once. Amazed customers are what got us to more than four hundred venues in eighteen countries. What decides whether the number after that has another zero on it is whether the eleventh integration costs what the first one did.

Which is why the part of this I care most about getting right isn’t the language. It’s that the people who’ll have to live in the result want to.

React on the front, Go behind it

What you’re looking for when you finally do pick a language, then, isn’t the most powerful one. It’s the one that hands you the other two for free.

The target is two languages.

Every interface is React. Web is React and TypeScript, mobile is React Native, and that includes the interfaces sitting on hardware — the kiosk screen a player stands in front of at half five in the morning is a screen like any other. That was already half-true before anyone decided it, because our mobile applications are React Native, and it became true in practice when one of our frontends went from Vue to React and shipped. The rest follow.

Everything behind those interfaces is Go. Not “the new services” — everything. The web backends, the public API, the integrations, and the desktop point-of-sale application, which becomes a Go core with a React interface instead of the JavaFX application it is today. Whether a thing runs in a data centre or on a till in a pro shop stops being a fact about what language it’s written in.

I’m not going to argue the languages here. Go against the alternatives is a post of its own, and the honest summary is that the specific choice matters considerably less than the fact that there is one — which is the argument this entire piece has been making. Worth being plain about one thing before the next section, though: Go is not what we have most of. Java, PHP, Node and Kotlin all have more running code behind them today, and that’s a real cost rather than a detail.

What the decision buys is two things, and they’re worth separating because they get blurred together in every post like this one. The first is developer experience, which is most of what this post is about and which comes down to the number at the top: how many people can open a given repository and be useful in it.

The second is performance, and there I want to be precise rather than sloganeering, because “simple stack, therefore fast” is not an argument. What it does mean is two specific things. One is footprint: what a process costs to start and to keep running, which is a real number on kiosk hardware and a real line on a cloud bill. The other is that performance work is gated on comprehension — you cannot fix the slow thing in a system nobody can hold in their head, and most systems are slow in a place their maintainers stopped looking at years ago.

One sub-decision genuinely isn’t settled. A Go core with a React interface on the desktop needs a shell to host that interface, and we haven’t picked one. The shape is decided; the mechanism isn’t, and it isn’t urgent, because the desktop application is nowhere near the front of the queue.

The shot we’re not taking

On most courses there’s a hole where the ambitious line goes straight over the water. The big-bang rewrite is that shot: freeze the old thing, build the new one beside it, switch. Quickest if it comes off — and the graveyard is full of companies that spent two years shipping nothing while their competitors shipped features.

We’re laying up. Four rules.

New work is built in the target shape. The only free rule, and the one that does most of the work over a year: every new service is a repository that never needs converting.

Rewrite only what you were opening anyway. Conversion is cheapest when somebody is already paying to understand the system. Working software nobody needs to change isn’t a problem; it’s an asset with an ugly logo.

Seams before rewrites. Two admin portals, two backends, two logins. Not a rewrite into one: an auth bridge first, then a single frontend over both backends, then the backend consolidation. Each step separately shippable, and separately abandonable.

Structure and verbs first, everywhere; language last, selectively. The cheap two-thirds is available in every repository today, including the ones that will never be converted.

Then the thing I’d rather write down than be caught not having thought about: it gets more varied before it gets less. Go isn’t a language we have much of, so for a couple of years the estate holds Java, PHP, Node, Kotlin and Go. Convergence arrives last. If you want to argue with this plan, argue there — it’s the weakest point in it, and the only defence is that the four rules exist to make that interval survivable.

One caution rather than a rule: agents have made rewrites cheap to produce and no cheaper to review. A rewrite that looks finished is the most flattering thing they make, because its gaps are omissions, and an omission never shows up in a demo.

What this actually costs

The plan has a bill and I’d rather itemise it than have somebody else do it.

Monoculture is a real risk, not a rhetorical one. Two languages everywhere means two sets of blind spots, two dependency ecosystems whose bad weeks are now everybody’s bad week, and a team that gradually loses the reflex of asking whether the usual tool is the right one.

The strongest form of that objection is the right tool for the job, and in general I agree with it. But it’s an argument about problem spaces, and ours is one problem space. Take the labels off our systems and they are all doing recognisably the same work: accept a transaction, talk to a device or to a partner’s API, and put a record somewhere it can be trusted later. There’s no machine-learning workload here pulling towards Python and no signal processing pulling towards C. The variety in our estate corresponds to no variety at all in the problems it solves, which is the tell that it’s accident rather than design. Nobody picked half a dozen languages as a hedging strategy.

So the risk is real and worth naming, and the mitigation is that “standard” has to mean a strong default with a documented way to argue, not a rule. The day a genuinely different problem does turn up — and in a company doing more with AI every quarter, it might — that argument has to be winnable, or the default has quietly become a rule after all.

Consolidation can be a way of avoiding product work. Structural work is satisfying and legible and it produces no customer value on the day it lands. The discipline is that every step has to be justified by something it unblocks. The portal merge is justified because club staff log in twice. The React standardisation is justified because it’s the precondition for everything after it. If a step can’t name what it unblocks, it isn’t ready.

It replaces work people are proud of. This is the one management raised when we presented the plan, ahead of cost and ahead of customer risk, and they were right to. Nobody wants to hear that the thing they built well, under the constraints they actually had, is now the old way. We ran the brainstorm with the whole team and deliberately held our own proposal back until everyone else had spoken. Nobody disagreed. The parts about merging the portals people were genuinely excited about; the standardisation itself people were fine with, which is a reasonable thing to feel when it’s your choice being replaced, and I’d rather report that accurately than upgrade it.

And it will take longer than this post makes it sound. Golf clubs are busy in summer, so we’re busy in summer. The platform goes quiet over the winter and that’s when the structural work has room. This is a plan measured in seasons.

How we’ll know it worked

Not by a diagram. By four things you can actually check.

How many repositories have exactly one person who can confidently ship a change to them. Today that number is uncomfortable. It’s the headline metric for this entire plan and it’s the only one I’d defend as being about the company rather than about engineering taste.

How long from clone to running code, on a machine that’s been through it once. That was two to six hours when our tech lead measured it in August, depending which repository you picked. In the repositories mise and fnox have reached, it’s a command.

The ambition behind that number is blunter than the number itself, so I’ll put it the way we actually say it: getting a codebase running locally should be as close to idiot-proof as we can make it. Not because anyone here is an idiot — the opposite. If starting a repository takes cleverness, that cleverness is being spent on the wrong thing, and it gets spent again by every person who arrives after you. Clone it, run one command, get a working system with a database, test data and an account to log in as. Anything standing between those two points is a defect, whether or not it’s in a tracker.

What the second integration costs relative to the first. If we do the structural work and the fifth partner integration still costs what the first one did, the work didn’t do what we said it would.

And what a new engineer can do at the end of their second week. Not “has read the codebase”. Has shipped something to a club.

That last one is the whole plan compressed into a sentence, and it’s the reason for all of it. Everything in this post — React on the screens, Go behind them, the folder layouts, the auth bridge, the one command that means the same thing in every repository — exists so that the answer to “can you pick this up?” stops depending on which repository we’re talking about.

And the reason to care about a question that small is that its answer caps everything above it. A club somewhere decides we’re the best thing they’ve seen this year, and from that moment the only question that matters is whether we can build what they need while they’re still excited about it. Doing that four hundred times is a product. Doing it four thousand times is an engine.

So here’s the version I actually believe, written down where you can hold me to it.

Two years from now, a developer who joined last month picks up an integration with a management system in a country we don’t sell in yet. They have never opened that repository. They clone it, mise run serve fetches the toolchain and brings the thing up, the Go handlers are where Go handlers always are, the React screens are laid out like every other React screen we own, and the tests pass first time. By Thursday it’s live at a club that had never heard of us in the spring. Nobody in the building thinks this is remarkable.

That’s the whole ambition. Not a heroic quarter — a Thursday.

Multiply that Thursday by every developer we have and every country that has golf in it, and you get a company that ships as fast as it can think, in an industry that has never once had software moving at that speed. The product that amazes people already exists; we didn’t have to invent it and I can’t take credit for it. What we’re building now is the only thing standing between that product and every club on earth that would want it — and we’d rather build it before somebody else works out that this is the race.

So: glory is a big word, and it’s the right one. A course record isn’t one miraculous shot — it’s eighteen ordinary holes played by somebody who has stopped losing strokes to the rough. We’re not lining up the hero shot over the water. We’re removing every place the ball can get lost, and then we’re going to play the round of our lives.

Which in practice is as unglamorous as it sounds, and I’d rather finish there than on the metaphor. React on every screen and Go behind every one of them. The same folder structure in every repository, so you can guess a path before you open the tree. One command that builds any of them. Secrets in a store instead of a chat message. Over seventy repositories converging on a single shape — and a company that can then take on the hundredth without anybody having to be brave about it.