I have never been good at staying in one developer camp for too long. I am curious and my questions tend to end in code, and on a CV the convenient name for that is “engineer.”
Recently I’ve been playing Gothic Remake. As you move through the story you need to make a couple of decisions: pick a camp and an allegiance. But the camp you pick at the start of the game is a place to live and sleep, not a lifelong contract. Sign up with the Old Camp and you can still do some jobs for the mercenaries in the New Camp or trade with the Swamp Camp. You’re also free to switch if Gomez pisses you off too much.
At various points of my life I have worked on backend systems and databases, written frontend code, and managed a team.
One of my less typical assignments was moving a large diverse monorepo from Bazel 7 to Bazel 9: from the old WORKSPACE approach to MODULE.bazel and bzlmod.
WORKSPACE, for those who have not touched Bazel (lucky you), was the old way of declaring external repositories: one root file, plus the macros it loaded, describing every external repository, all by hand.
bzlmod replaces it with versioned modules that declare their own dependencies and let Bazel resolve the graph, much more like what contemporary dependency managers do.
In a large monorepo such a migration reaches further than any single configuration file.
Targets, toolchains, and CI paths all lean on the old arrangement, and the release train of the project has to keep running while you change the machinery underneath it.
When I took on the task, it looked almost immovable. “Stop shipping while I migrate the world” was not a plan anyone would sign off on.
That was when I remembered an online shop I had moved from someone’s custom home-grown engine to Symfony more than a decade earlier. The old engine was spectacularly crooked: magic numbers everywhere, functions with cyclomatic complexity three zeros deep, a core that took five to eight seconds to render a page. I wrote about the migration at the time, never suspecting the same idea would come back to me through a build graph.
Writing the new Symfony code was the easy half of that job. The old application owned the whole page — routing, templates, constants assigned at runtime, pieces of state whose behaviour was hard to trace — and replacing everything at once would have meant rewriting the world while preserving all of its old behaviour in one go.
The strangler approach changed the unit of work. The question “how do I replace the whole system” turned into “where can I draw one small, honest boundary”. In the shop, that boundary was an NGINX SSI include: the old engine kept rendering the page, while one block on it already came from the new application. The old code kept running, the new code took over responsibility a piece at a time, and each boundary could be checked on its own.
More than a decade later, the Bazel migration asked the same question again. There was no PHP this time, just hundreds of external dependencies, toolchains, build targets, and CI jobs. Java, TypeScript, Go, Python, Bash… The requirement underneath was the same: the old and the new system had to coexist long enough for responsibility to move in small pieces.
Bazel 7 even shipped the coexistence mechanism.
You introduce an empty MODULE.bazel, enable bzlmod, and move dependencies over one at a time; the repository rules that have not moved yet go into WORKSPACE.bzlmod, which Bazel reads instead of WORKSPACE while bzlmod is on.
The official migration guide walks through the whole mechanism.
What the guide cannot walk you through is the months.
My loop was plain: move a small piece, build the affected targets, then diff what actually changed — the lockfile, the resolved versions, the shape of the dependency graph.
bzlmod is allowed to pick different versions than WORKSPACE did, so “nothing changed at all” is not a realistic success criterion.
The question I could actually answer was narrower: is every change explained by the piece I just moved?
When it was not, I rolled back and went digging for whatever the old setup had been doing for me behind my back.
Within a few weeks the loop itself turned boring.
Deciding where to cut the next boundary did not, because the guide describes a tidy migration and real repositories are not tidy.
In our case rules_docker had no BCR module and no active maintainers left; we did not want to wire a dead ruleset in through an extension, so we ported the image builds to rules_oci: a separate migration nested inside the bigger one.
The Java dependencies still lived on the old pinning setup, and moving them meant repinning the lockfile and checking every version that shifted.
Some steps refused to stay small; what mattered was that each one could still be inspected on its own.
The whole thing took many months, in the gaps between frontend work and managing a team. Nobody reserved me a quiet month alone with the dependency graph, so the migration had to fit around normal work, and sometimes it just waited.
That is why I plan long migrations to survive interruption. Sooner or later something more urgent turns up, and the plan has to assume it will. Small steps get praised for cheap rollback, but over those months I rolled back a handful of times and resumed constantly. The verification loop is what made resuming cheap: a piece that built green, explained all of its own diff, and got merged was a piece I could safely put down. A short list in the migration ticket recorded which dependencies had moved and which were still waiting. Coming back after two weeks of other work meant reading that list, not reconstructing a graph of hundreds of dependencies in my head.
There was also a real deadline.
Bazel 8 disabled the legacy WORKSPACE system by default, and Bazel 9 removed it altogether.
A repository stuck at “almost migrated” would have stayed on Bazel 7 for good.
I never did settle in one camp in Gothic, and so far this migration is my best excuse for the habit. The route between camps is dustier, and you spend time explaining why a frontend engineer cares about Bazel and why a DevOps engineer still remembers old Symfony. But the trick I learned rescuing a crooked PHP shop sat untouched for a decade and then paid for the whole trip. You forget these things for years, until the same monster climbs out of somebody’s build graph and you recognise it. Last time it was written in PHP.






