Architectural Evolution Patterns: A Guided Track
Learn how to evolve a live system safely with strangler fig, branch by abstraction, parallel run and fitness functions, in a four-week guided track.
No architecture stays right forever. Load grows, teams change and requirements shift. The skill that separates durable systems from rewrites is evolving in small, reversible steps while the system keeps serving users. This track teaches the core patterns for doing that and gives you exercises to practise on a codebase of your own.
Learning goals
By the end you should be able to:
- Explain why big-bang rewrites fail and what to do instead.
- Apply the strangler fig, branch by abstraction and parallel run patterns.
- Define fitness functions that protect architectural properties automatically.
- Sequence a migration so every step is shippable and reversible.
Week 1: Understand why systems resist change
Start by diagnosing. Systems resist change because of tight coupling, hidden data dependencies, and missing tests. Before choosing a pattern, map the thing you want to change.
Steps:
- Draw the current dependency graph for one subsystem, including shared databases and scheduled jobs.
- Mark seams: places where behaviour can be intercepted without editing the callers, such as an HTTP boundary, an interface, or a queue.
- Identify the riskiest unknown, usually data coupling or undocumented consumers.
Exercise: produce a one-page map of a subsystem with at least three seams and the unknowns you cannot yet answer.
Week 2: The strangler fig and branch by abstraction
The strangler fig pattern, named by Martin Fowler after vines that gradually envelop a tree, routes traffic through a facade and moves functionality piece by piece to the new implementation.
Clients -> Facade/Router --+--> New service (migrated routes)
+--> Legacy system (everything else)
Branch by abstraction applies the same idea inside a codebase, when there is no network boundary to intercept:
- Introduce an interface in front of the component to replace.
- Route all callers through it.
- Build the new implementation behind the interface, selected by a flag.
- Migrate callers gradually, then delete the old implementation and, if useful, the flag.
Trade-offs: both patterns keep the system releasable at all times, but you run two implementations for a while, which costs effort and demands discipline to finish. Many migrations stall at 80 percent, leaving permanent duplication.
Exercise: choose one endpoint or module in a project, write the facade or interface, and move a single route or caller behind a flag.
Week 3: Verify with parallel runs and manage data
Behaviour parity is the main risk. A parallel run sends the same input to old and new implementations and compares results, serving only the old one.
- Log mismatches with enough context to diagnose.
- Treat expected differences, like timestamps, as noise to normalise.
- Cut over when mismatch rate stays at zero for an agreed period.
Data is harder than code. Common approaches:
| Approach | Description | Risk | |---|---|---| | Shared database temporarily | Both implementations use the same schema | Hidden coupling persists | | Expand and contract | Add new columns, dual write, backfill, switch reads, drop old | Needs several releases | | Change data capture | Stream changes from the old store to the new | Lag and ordering must be handled |
Expand and contract is the safest default for schema change: never make a change that breaks the previous version of the application.
Exercise: design an expand-and-contract plan for renaming a column used by two services, listing each release and its rollback.
Week 4: Guard the architecture with fitness functions
The idea of evolutionary architecture, popularised by Ford, Parsons and Kua, is to encode architectural intent as automated checks called fitness functions.
Examples:
- A test that fails when a domain package imports from the infrastructure package.
- A CI check that fails when p95 latency of a key flow regresses more than 10 percent in a load test.
- A script that fails when cyclic dependencies appear between modules.
rule: package "billing" must not depend on package "shipping"
check: dependency analysis in CI, fail build on violation
Fitness functions turn drift into a failing build rather than a surprise during a review.
Exercise: add one dependency-direction check and one performance budget to a real pipeline.
Pitfalls to avoid
- Starting migration without observability, so you cannot tell whether the new path is correct.
- Leaving both paths indefinitely. Put a removal date and owner on every flag.
- Migrating the easy parts first and never touching the hard core.
- Changing behaviour and structure in the same step, which makes mismatches impossible to attribute.
Key takeaways
- Evolve in small, reversible, shippable steps instead of rewriting.
- Use seams, the strangler fig and branch by abstraction to move behaviour gradually.
- Verify with parallel runs and handle data using expand and contract.
- Encode architectural intent as fitness functions so it survives team turnover.
- Finish what you start: remove the old path and the flags.