Branch by Abstraction
5 minute read
Phase 1 - Foundations | Scope: Team
Branch by abstraction lets you replace an internal implementation, algorithm, or library on trunk, without a long-lived branch and without disrupting the code that already depends on it.
What Is Branch by Abstraction?
Branch by abstraction introduces an interface over an existing implementation, redirects callers to that interface, then builds and switches in a new implementation behind it, all as small commits on trunk. The “branching” happens in the abstraction layer, not in version control.
The technique gets its name because it replaces a source-control branch with a branch in the code itself: an interface with two implementations, one of which is live.
What Branch by Abstraction Is Not
- It is not a long-lived feature branch with an interface added to justify it. If the work still takes weeks on a branch, the abstraction hasn’t replaced anything.
- It is not the strangler fig pattern. Branch by abstraction swaps an implementation behind an in-process interface. Strangler fig replaces a whole subsystem or service by routing traffic to it at a system boundary. Use branch by abstraction inside a codebase you own; use strangler fig when the thing being replaced is bigger than one component.
- It is not a permanent abstraction layer. Once the swap is complete, remove the old implementation, and remove the interface too if nothing else needs it.
What Branch by Abstraction Improves
| Problem | How Branch by Abstraction Helps |
|---|---|
| Large refactors force a long-lived branch | The refactor happens in small commits on trunk, behind an interface |
| Fear of breaking existing callers during a rewrite | Callers depend on the interface, not the implementation, so the swap is invisible to them |
| “Big bang” cutover risk | The switch is a single dependency-injection change, easy to revert |
| Dead code left behind after a migration | The interface makes the old implementation easy to find and delete |
Making the Swap
Step 1: Abstract
Introduce an interface over the existing code and redirect every caller to it. This is a zero-behavior-change commit: the interface wraps the current implementation and nothing else changes.
Step 2: Implement
Build the new implementation alongside the old one, as its own class. Commit and deploy the new class in small pieces; it isn’t wired to any caller yet, so it carries the same zero risk as dark code.
Step 3: Switch
Change the dependency injection or factory binding to instantiate the new implementation instead of the old one. This is the entire cutover: one line, one commit, easy to revert.
If you need to de-risk the switch further, or need confidence the two implementations produce identical results first, run them side by side with a parallel run before flipping the binding.
Step 4: Prune
Delete the legacy implementation. Delete the interface too if only one implementation remains and nothing else depends on the abstraction.
Cleanup here is a straightforward deletion of a class. There is no scattered if/else logic to search for, because the old and new implementations were never in the same function.
Key Pitfalls
1. “We built the new implementation and the interface in the same commit”
This makes the abstraction hard to review on its own merits, and it removes the option to ship the interface as a safe, standalone step. Extract the interface first, verify it changes nothing, then start on the new implementation.
2. “We left the old implementation in place after the switch”
The switch commit is not the finish line. If the old class is still in the codebase a month later, delete it. An unused implementation behind a working interface is exactly the kind of dead weight branch by abstraction is supposed to avoid.
3. “We used branch by abstraction to replace a whole service”
If the replacement spans multiple components, teams, or a system boundary, that’s a strangler fig problem, not an in-process interface swap.
Measuring Success
| Metric | Target | Why It Matters |
|---|---|---|
| Time from abstraction to switch | Days to a few weeks | Confirms the technique is replacing a branch, not becoming one |
| Legacy implementations still in the codebase after switch | Zero after the agreed cleanup window | Confirms pruning actually happens |
| Commits per swap | Many small commits, no single large diff | Confirms the refactor stayed on trunk in small pieces |
Next Step
If you need to prove the new implementation matches production behavior before switching the binding, use Parallel Run.
Related Content
- Evolutionary Coding Techniques - the full decision hierarchy
- Dark Code - the simpler technique for new, unreferenced logic
- Architecture Decoupling - the strangler fig pattern for replacing whole subsystems
- Long-Lived Feature Branches - the anti-pattern branch by abstraction replaces
- Branch By Abstraction - external reference site for the pattern