What is branch-by-abstraction, and how does it let a team extract a module into a separate service without a long-lived feature branch or a risky flag-day cutover?
answer
- abstraction interface before implementation
- trunk-based, no long branch
- toggle/flag picks implementation
- delete old code after rollout
- in-process cousin of strangler fig routing
basics
~20 sYou put a stable interface in front of the old code, build the new implementation behind that same interface, switch callers over gradually, then delete the old code — all on the main branch, no long branches.
solid answer
~50 sBranch-by-abstraction is a technique for making large-scale changes (like extracting a module to a new service) incrementally on trunk instead of on a long-lived feature branch. You introduce an abstraction layer (interface) in front of the code you're replacing, make all existing callers go through it, then build the new implementation behind the same interface, initially routing to the old code. You flip an internal switch (feature flag, config, or a facade's implementation choice) to route calls to the new implementation instead, validate, and once confident, delete the old implementation and the flag. Because every step lands on trunk, the team avoids merge hell from a long branch, gets continuous integration testing throughout, and can roll back instantly by flipping the switch rather than reverting a giant merge. It's the in-code counterpart to the strangler fig's traffic routing, but operates at the call-site level inside an application rather than at the network/HTTP level between systems.
go deeper
Should grasp that you build an interface, then swap what's behind it, then clean up — without needing to name the pattern.
Should describe the toggle-based gradual rollout and why it avoids a long feature branch, and be able to sketch the interface/two-implementations structure.
Should identify the leaky-abstraction and flag-debt failure modes, and know to define the interface contract around the eventual (harder) implementation's failure modes up front.
Should connect it to trunk-based development and CD strategy at an org level — mandating branch-by-abstraction as policy for large migrations, setting flag-lifetime SLAs, and using it as the standard in-process complement to network-level strangler-fig routing.
## What it is Branch-by-abstraction, a term credited to **Paul Hammant** from his time at ThoughtWorks, is a technique for making large, invasive code changes — such as swapping out an entire module's implementation, which is exactly what happens when extracting part of a monolith into a new service — without resorting to a long-lived version-control branch. It is the in-process, call-site-level counterpart to the strangler fig pattern's network-level traffic routing: where strangler fig routes HTTP requests between the old monolith and a new service, branch-by-abstraction routes in-process method calls between an old implementation and a new one, all while every change lands on the trunk/main branch. ## The five steps The mechanism has five concrete steps. 1. **First**, the team introduces an abstraction — typically an interface or a facade — that exposes the same operations as the code being replaced. 2. **Second**, every existing call site in the codebase is refactored to depend on this abstraction rather than the concrete class directly; because the abstraction's only backing implementation at this point is the original code, behavior is unchanged, so this refactor can be reviewed, tested, and merged to trunk safely on its own, with no functional risk. 3. **Third**, the team builds the new implementation behind the same interface — this might start as another in-process class and later become a thin client that calls out to a genuinely separate service over HTTP or messaging, a distinction the interface hides from callers. 4. **Fourth**, a toggle — a feature flag, a percentage rollout, or a per-tenant switch — decides at runtime which implementation the abstraction delegates to, and the team ramps this from 0% toward 100% while watching correctness and operational metrics. 5. **Fifth**, once the new implementation has run at 100% and proven stable for a sustained period, the team deletes the old implementation and the toggle, leaving a single clean implementation behind the abstraction. ## Why it exists This exists to solve a specific tension in **trunk-based development**: large changes are risky to do all at once, but the usual alternative — a long-lived feature branch that carries the change until it's "done" — causes its own problems: - diverging further from trunk every day it stays open; - accumulating merge conflicts; - losing the safety net of continuous integration testing against everyone else's latest work. Branch-by-abstraction sidesteps this by making the change progressive but entirely on trunk: nothing is hidden in a branch, every intermediate state is shippable, and rollback is an instant toggle flip rather than a branch revert or a redeploy. ## The costs The costs are real, though. - **More code during the transition.** The codebase carries an abstraction layer plus two live implementations, which is genuinely more code to understand, test, and maintain than either the old or new implementation alone — and if the interface is drawn at the wrong level (too coarse, hiding meaningful differences; or too granular, adding indirection without isolating anything useful), that overhead outlives its usefulness. - **A leaking abstraction.** There is also a real risk of the abstraction leaking: if the new implementation has meaningfully different failure semantics than the old one — for example, a formerly in-process, effectively-never-fails call becomes a network call that can time out or return a 503 — and the interface's contract wasn't designed with that possibility in mind from the start, callers written against the "old" assumptions will mishandle the new implementation's failures once traffic starts flowing to it. The interface should therefore be designed around the hardest eventual case (the network-backed implementation) even while only the easy case (the in-process implementation) exists. ## Failure modes Two failure modes recur in production. 1. **The first is incomplete migration of call sites.** If even one caller bypasses the abstraction and calls the old concrete class directly, the team can neither safely delete the old implementation (that caller would break) nor be confident the toggle actually governs all traffic — a subtle, easy-to-miss gap that static analysis or a visibility restriction on the old class can catch. 2. **The second is flag debt.** Teams routinely reach 100% rollout, declare success, and move on to the next priority without doing the cleanup step, leaving the old implementation and its toggle in the codebase indefinitely — dead code that still compiles, still gets tested, and still carries the risk of being accidentally re-enabled. ## A worked example A concrete worked example: extracting an `InventoryService` used by 40 call sites into a future microservice. The team: - introduces an `InventoryPort` interface, migrates all 40 sites to depend on it, and backs it initially with the existing `InventoryService` class; - then builds an `InventoryServiceClient` implementing the same interface that calls a new inventory microservice over HTTP; - ramps traffic to it region by region via a flag, and deletes the old class and flag once the new client has proven stable at 100% for a couple of weeks.
- How is branch-by-abstraction different from a simple feature flag around new code?A plain feature flag typically wraps a block of new code inline; branch-by-abstraction specifically inserts a stable interface that all callers depend on, so the two implementations are fully interchangeable and callers never know which is active. This makes it suited to large, cross-cutting replacements rather than a single feature toggle, and keeps the seam clean enough to delete the old implementation without touching call sites again.
- What's the risk of leaving the toggle and old implementation in the codebase long after 100% rollout?It accumulates as flag debt and dead code that still gets compiled, tested, and reasoned about, increasing cognitive load and the chance someone re-enables the old path by accident. It also means the abstraction never gets simplified back down, so the team pays the indirection cost of the migration forever instead of just during the transition.
- If the new implementation behind the abstraction has different failure semantics (e.g., it can time out over the network, where the old in-process call couldn't), how should the interface account for that?The abstraction's contract should be defined for the hardest case up front — e.g., method signatures should return a Result/Either type or declare exceptions for timeouts/unavailability even while only the in-process implementation exists — so callers are already handling failure modes before the network-backed implementation lands. Retrofitting error handling into dozens of call sites after the flag flips is exactly the kind of stabilization work strangler-style migrations try to avoid.
Like renovating a house while living in it: you first build a doorway frame (the abstraction) that works with the old room, construct the new room behind a temporary wall, then simply open a connecting door (the toggle) to start using the new room, and only once everyone's moved in do you demolish the old room — you never have to move out (branch) and move back in (merge) all at once.
saying these in an interview costs you the question
- Confuses it with just wrapping new code in a boolean if-flag with no shared interface
- Doesn't mention deleting the old implementation/toggle afterward
- Thinks it requires a long-lived git branch (the opposite of the point)
- Ignores that the abstraction's contract must anticipate the new implementation's failure modes (e.g., network calls)
- Assumes all callers were migrated to the interface without checking for direct calls that bypass it