Legacy and migration

·5 min read·Updated

Branch by Abstraction: Replace the Implementation, Not the Product Roadmap

The document library must change while the product keeps shipping. How to introduce a boundary, compare implementations and finish the migration without duplicate effects.

An application produces equipment-inspection documents through a library that has become difficult to maintain. The customer portal, batch exports and a support screen all call it. Replacement work starts on a separate branch. Meanwhile, new fields and templates arrive in the product, and the future implementation drifts further from what customers currently use.

Branch by Abstraction takes a different route: separate callers from a concrete implementation, then replace it incrementally along the shared development line. Normal releases remain possible before the migration is complete. The trade-off is temporary coexistence and verification work, not the disappearance of migration complexity.

Put a boundary around the working implementation first

For inspection reports, the application might expose an operation to render a document from inspection data. Wrap the old library behind that interface, then move callers to it. This first step should preserve behaviour: the call path changes, not the document’s rules.

Martin Fowler describes this sequence before introducing another implementation behind the boundary. Callers can then switch in an appropriate order. Often the move is incremental; some application constraints require switching them together.

Finding every caller can be harder than writing the interface. An overnight export may instantiate the library directly, while a support extension relies on its internal format. Migrate or explicitly account for those paths, or the dependency will remain necessary long after the main screen has moved on.

Do not reproduce the library in the new contract

The interface need not repeat dozens of legacy methods. But a tidy operation named “make document” is insufficient without defining its result. Which inputs are required? How are failures reported? Who stores the file? Can the operation be retried? Where is permission to receive the report checked?

If the new implementation still accepts internal objects from the old library, the dependency has merely changed location. Express the application’s needs and keep technical details inside an adapter. Do not silently discard behaviour required by batch exports because it is inconvenient to implement.

AWS contrasts this approach with Strangler Fig: a deep internal dependency may not be interceptable at the application perimeter. Branch by Abstraction works at that code boundary. The replacement does not have to be a separately deployed service.

Compare documents without sending two emails

Separate rendering from storage, document numbering and notification delivery where possible. Both renderers can then process the same permitted inputs without executing the customer workflow twice. If the effects cannot yet be separated, use an isolated test rather than quietly running a second production operation.

Byte-for-byte equality may be a poor criterion for PDFs: libraries can encode metadata and file structure differently. Check contents, values, readability, pagination, fonts and compatibility with required readers. A visually similar result still does not prove that every required detail is present.

Include awkward examples: long equipment names, missing optional sections, many photographs and large batches. Agree on acceptable differences in advance. Correcting an old defect may intentionally change the result, but that decision should be distinguishable from an unexplained mismatch.

Running both implementations also consumes resources. If comparison delays the real inspection document, the experiment is affecting customers. Bound its volume and decide what happens when the candidate falls behind. Its output must not accidentally replace the authoritative document.

Disabled code still participates in the deployment

Both implementations are built and tested with the product. Keeping the shared line releasable is central to the Trunk-Based Development guidance. An unfinished replacement should not make ordinary customer fixes wait for the whole migration project.

A switch does not prevent dependency conflicts or startup failures. The new library may require a different runtime or initialise resources before it receives a request. Test installation and application startup, not only the disabled path’s behaviour.

As the product evolves, changes to the shared contract must be reflected in both implementations that remain supported. The migration needs an owner and allocated capacity. Otherwise the candidate becomes another permanently unfinished effort, now inside the main repository rather than on a remote branch.

Choose a switching unit and a rollback boundary

For a batch export, associating the implementation with the job can prevent a restarted batch from continuing under different rules. Independent documents may allow a smaller unit of selection. The choice follows the operation’s state, not where it is easiest to insert a conditional.

If the new component only renders a compatible file, reverting may be simple. If it writes a changed template format or updates shared state, verify that the previous implementation can handle those records. An interface cannot synchronise independent databases or undo completed writes.

Observe completed jobs, file quality, latency and support cases, not merely the number of calls reaching the new code. Stopping new jobs and recovering a batch already in progress are different decisions. The latter needs an explicit continuation or repair procedure.

Decide what survives the migration

Once all required callers have moved, remove the old implementation, its dependencies and temporary selection logic. Check administrative jobs and recovery procedures too: neither should unexpectedly require the retired library. Coexistence costs only disappear when its machinery is actually removed.

The interface may remain if it serves useful application or testing boundaries. If it existed only to stage the replacement, reassess its purpose. For the inspection product, completion means the regular team can change a template and ship without negotiating around an unfinished replacement. The existence of another abstraction is not that outcome.