Branch by Abstraction

Updated Sep 18, 2026

Branch by Abstraction is a technique for replacing a dependency or component behind a shared interface. Existing callers move to the abstraction first, then old and new implementations can coexist while traffic shifts between them. The system remains buildable and releasable throughout the change.

Branch by Abstraction replaces a component without keeping the work isolated on a long-lived version-control branch. The branch is a design boundary in the code. Callers depend on one abstraction, while the legacy and replacement implementations sit behind it until the migration is complete.

The sequence behind the technique

The first change introduces an abstraction that represents what callers need from the existing component. Callers then move to that abstraction without changing behaviour. Only after this step is stable does the replacement implementation begin to take over.

StepActive implementationMain changeVerification focus
IntroduceLegacy onlyAdd the abstractionExisting behaviour remains stable
Redirect callersLegacy onlyRoute calls through the abstractionNo direct callers remain in scope
Add replacementLegacy and newImplement the same contractOutputs and side effects can be compared
SwitchNew by defaultChange implementation selectionRuntime behaviour stays within the contract
RemoveNew onlyDelete legacy code and temporary controlsNo hidden dependency remains

Feature flags, configuration, or dependency injection can choose the active implementation. The selection mechanism is temporary migration infrastructure and needs its own retirement condition.

Why the abstraction matters in legacy code

Legacy callers often depend on details rather than a defined contract. They may construct a library directly, read its tables, or rely on its error messages and timing. A legacy codebase audit identifies these dependencies before an interface is declared stable.

Creating that interface is refactoring because it changes internal structure while preserving observable behaviour. The work can expose assumptions that were previously distributed across callers. Those assumptions must either become part of the contract or be removed deliberately.

Comparing old and new implementations

Coexistence creates an opportunity to run both implementations against the same controlled inputs. A characterization test can capture current behaviour before the replacement is trusted. For read-only operations, teams may also compare results produced by both paths. Operations with side effects require isolation because sending the same command twice can duplicate writes, messages, or external actions.

A safe comparison plan records:

  • Inputs. The requests and data each implementation receives.
  • Outputs. Returned values, errors, and externally visible responses.
  • Side effects. Database changes, messages, files, and network calls.
  • Accepted differences. Changes that are intentional and reviewed rather than accidental.

When the migration is complete

The switch is not complete merely because most traffic uses the new implementation. Direct legacy callers, fallback paths, operational scripts, and data ownership can keep the old component alive.

Completion means the replacement owns the agreed contract, monitoring covers the new path, and no required process depends on the old implementation. The abstraction can remain when it represents a useful domain boundary. If it exists only to support the transition, removing it may simplify the final design. This makes the technique useful for taking over unfamiliar code in stages while keeping the system releasable between changes.