TL;DR
- Architectural migrations rarely fail because the target was wrong. They fail on the transition — the build, the pipeline, the module resolution, the permissions — and those are invisible until something real runs.
- Deploy a trivial function through the entire new path first. Every link proven, in production, before any real service moves.
- Running two architectures at once is fine. The risk isn't that the new one stalls, it's that the boundary blurs. Enforce it in tool configuration, in more than one place, so nobody has to remember.
- After the bridgehead, build a user, not more tooling. If your new architecture has more supporting infrastructure than it has consumers, stop.
- The underrated benefit: it makes abandoning the migration cheap and honest. Most architectural migrations should probably be abandoned, and this is how you find out which ones before sunk cost makes the answer unsayable.
The most expensive way to change an architecture is to migrate a real service first and find out what doesn't work.
I've watched this happen more than once. A team decides the deployment model has to change — from one monolithic stack to independent ones, from a shared runtime to bundled functions, from hand-written configuration to something generated. They pick a service to move. Something moderately important, because a trivial one "wouldn't prove anything".
Three weeks in they discover that the build produces a package the new pipeline can't sign, that the shared library doesn't resolve the way it did before, and that the new configuration model has no way to express the one setting this service needs. Now they're holding a half-migrated service, a pipeline that works for nothing, and a decision about whether to push on or roll back.
The fix is unglamorous and it works: build the thinnest possible thing that exercises the entire new path, end to end, in production, before you migrate anything real.
Not a prototype. Not a spike on a branch. A deployed, running artefact on the new path, doing something trivial, that proves every link in the chain works.
I call it a bridgehead. It usually returns the string "pong".
What it's actually for
Any architectural migration is a chain of mechanisms, and every link is somewhere the plan can break:
- The template compiles and deploys under the new topology
- The configuration generates correctly for each environment
- CI authenticates and has the permissions it needs
- The build produces the right artefact — right format, right entry point, right dependencies included and excluded
- Shared code resolves at runtime the way it did before
- The function starts, gets its configuration, and responds
- Logs and traces arrive where you expect
- Rollback works
Eight links. A migration can die at any of them, and most of them are invisible until something actually runs in a real account with real IAM and real networking.
The bridgehead converts every one of those from an unknown into a known before anyone has committed real work. Finding out that your bundler inlines the shared layer costs a day when the only casualty is a function that returns "pong". It costs a quarter when three services are half-moved.
This is the same instinct as a walking skeleton, and the same instinct as deploying a hello-world through a new pipeline on day one of a project. It's not novel. It's just consistently skipped, because it produces nothing a stakeholder can see.
The failure mode it prevents is specific
Migrations don't usually fail because the target architecture was wrong. They fail because the transition was underestimated — and the transition is almost entirely made of these mechanism questions.
The tell that a team skipped the bridgehead is a migration that stalls at 30%. Two services moved, both with unresolved quirks, the third one blocked on something structural that nobody wants to own, and a growing reluctance to talk about it in standup. At that point you have two architectures, both in production, neither complete, and the cost of finishing has gone up because the people who understood the plan have moved on.
Then the harder problem: containing the old world
Here's what I think most people get wrong about running two architectures at once.
The risk isn't that the new one stalls. It's that the boundary blurs. New code drifts into old directories because that's where the similar thing lives. Old patterns get copied into new services because that's what the nearest example does. A helper gets shared across the line "just for now". Eighteen months later you have one architecture again — the old one, wearing the new one's syntax.
The counter is to make the boundary mechanical rather than cultural. Not a wiki page. Configuration that tools enforce:
- Compiler configuration that includes the new tree and excludes the old
- Formatter and linter rules scoped to the new tree only
- Separate CI workflows, so the new path can't accidentally deploy through the old one
- Directory structure that makes the line obvious from a file path
The point is that "is this file part of the new model?" should have a mechanical answer, not a cultural one. Nobody should have to remember. New code type-checks and is formatted; old code is left alone. When someone tries to import across the line, something complains.
I'd go further: draw the same line in at least two independent places. One config file is a setting. Two config files drawing the same boundary is a decision somebody made on purpose, and it survives the person who made it.
Be honest in the artefacts, not just the standup
Two habits that cost nothing and prevent a lot of confusion:
Mark the new path's maturity in the pipeline itself. If production isn't supported yet, the workflow shouldn't offer production as an option. Not a warning in the description — an input that doesn't include it. Constraints you can select past aren't constraints.
Write the promotion path where developers see it. A pull request template with the checks required for each target branch does more than any amount of documentation, because it's in front of someone at the moment they need it.
Neither of these is sophisticated. Both are the difference between a coexistence that stays managed and one that quietly becomes permanent.
The part people get wrong about bridgeheads
A bridgehead has a shelf life, and the failure mode is specific enough to name.
An unpopulated bridgehead decays like this: the new path is never exercised by real work, so it never meets real problems, so it never gets fixed. When someone finally migrates a genuine service, they hit six issues a "pong" endpoint could never have surfaced — a function that needs a queue, one that needs the legacy shared code, one whose configuration doesn't fit the new model, one that needs a VPC attachment, one with a scheduled trigger. And by then the people who built the bridgehead have moved to other work and the context is gone.
So the counter-move is straightforward: the next thing you build after the bridgehead should be a user, not more infrastructure.
Pick a real service. Small, low-traffic, low-stakes — but real. It has actual consumers, an actual failure mode, an actual on-call implication. Not because that service matters, but because it's the only way to find out what the bridgehead doesn't handle.
There's a decent heuristic in there: if your new architecture has more supporting tooling than it has users, stop building tooling.
I'd add one more discipline, because it's what separates a migration from a hobby: give the bridgehead an owner and a next milestone with a date. Not a completion date for the whole migration — that's fiction at this stage. A date for the first real service. If nobody will commit to that, the honest read is that the migration isn't funded, and it's better to know that in month one than in month fourteen.
What I'd actually do, in order
- Write down what changes and what doesn't. Deployment topology, configuration, build, runtime, code layout. Be specific. Most of the surprises live in the ones people don't list.
- Build the trivial thing, all the way to production. Deployed, invocable, observable. Rollback tested.
- Draw the boundary in tool configuration, in more than one place.
- Migrate one real, small service. Fix what breaks. Expect it to take longer than the bridgehead did — that's the point, and it's why doing it in this order was worth it.
- Only then, decide whether to keep going. With two real data points instead of a plan.
Step five is the one worth protecting. The value of the bridgehead isn't only that it de-risks the migration — it's that it makes abandoning the migration cheap and honest. You've spent a week and learned that the new model doesn't fit. That's a good outcome, and it's only available if you haven't already moved three services.
Most architectural migrations should probably be abandoned. Building the bridgehead first is how you find out which ones, before the sunk cost makes the answer unsayable.