This is the pattern catalogue for moving code out of a running system. Every pattern in the chapter serves the same goal from part 2: move functionality in small steps that can be shipped alongside feature work and reversed quickly. Newman is careful to give each pattern a "where to use it" section, and those sections are where most of the judgement lives.
As a frontend developer, I found the UI composition section more relevant than I expected. Micro frontends get only a short treatment, but the broader point, that a migration which ignores the UI leaves a lot of value on the table, holds up very well.
Chapter 3 in the book.
Key ideas
Before the patterns: copy, do not cut
Newman starts with a few framing decisions. Can you change the monolith at all? If it is vendor software, or the code is effectively unavailable, only patterns that work at the edges apply. If you can change it, copy code into the new service rather than cutting it out, so the old implementation stays available as a rollback and as a comparison during a parallel run. Remove it from the monolith only once the migration has proven itself.
He also recommends refactoring the monolith along domain lines first, using seams in the sense of Michael Feathers' Working Effectively with Legacy Code. Turning those seams into modules gives you a modular monolith, and he notes that for some teams that step alone solved most of their problems. If you rewrite instead of moving code, keep each rewrite small: days or weeks, not months.
Strangler fig: intercept at the edge
The strangler fig application, named by Martin Fowler, is Newman's default first pattern. The new service grows around the old system, takes over one piece of functionality at a time, and the old system shrinks. The mechanics with an HTTP monolith are three steps:
- Put a reverse proxy in front of the monolith that passes everything through, and measure the extra hop.
- Build the new service and deploy it to production before it receives traffic, even if it only returns 501 at first.
- Switch the route for that functionality to the new service, keeping the switch easy to reverse.
The second step separates deployment from release, an idea that recurs across the chapter. The new service can live in production, monitored and tested in place, while users still hit the monolith. A proxy configuration for one migrated route might look like this:
upstream legacy_app {
server legacy.internal:8080;
}
upstream certificates_service {
server certificates.internal:3000;
}
server {
listen 80;
location /api/certificates/ {
proxy_pass http://certificates_service;
}
location / {
proxy_pass http://legacy_app;
}
}
Rolling back is reverting one location block. The pattern breaks down when the functionality you want is not reachable from an inbound call, for example notifications triggered from deep inside many flows, and when the protocol is hard to route. Newman also warns against building protocol translation into the shared proxy: smart shared pipes become a new source of contention. If you need to move from one protocol to another, let the service expose both.
The chapter shows the pattern beyond HTTP too: Homegate intercepted FTP uploads and fed them to a new REST service, and message-driven monoliths can be strangled with a content-based router or by having the monolith and the new service selectively consume from the same queue. Selective consumption avoids an extra queue but needs coordinated changes on both sides, so it suits only a few consumers with simple rules.
One rule I would print and pin up: avoid changing the behaviour of functionality while you are migrating it. Bug fixes or new features in the new service make rollback to the old one remove those changes from users, so the smaller each migration step, the less pressure there is to slip in changes.
UI composition: migrate what users see
The user interface is another place to splice old and new. Newman describes three forms, drawing on The Guardian, REA Group, Orbitz and Spotify:
| Form | How it works | Fits when |
|---|---|---|
| Page composition | Route whole pages or site sections to the new system | A team owns a whole vertical, as with The Guardian's travel section or REA's channels |
| Widget composition | Splice a fragment from the new system into old pages | You want a low-risk first release; one widget failing degrades only part of the page |
| Micro frontends | Independently built parts of a single-page app composed in the browser | A large SPA owned by several teams |
For native mobile apps, he notes that the app itself is a monolith shipped through store review, which is why companies like Spotify drive component layout from the server.
Branch by abstraction: when the code is deep inside
When the functionality cannot be intercepted at the edge, you need to change the monolith without long-lived branches. Branch by abstraction does this in five steps: create an abstraction over the functionality, move callers onto it, build a new implementation that calls the new service, switch over (ideally with a feature toggle), and clean up the old implementation and the toggle. Both implementations live on the main branch, and only one is active at a time.
export type CertificateRequest = {
enrollmentId: string;
learnerName: string;
courseTitle: string;
};
export interface CertificateIssuer {
issue(request: CertificateRequest): Promise<{ certificateId: string }>;
}
export class RemoteCertificateIssuer implements CertificateIssuer {
constructor(private readonly baseUrl: string) {}
async issue(request: CertificateRequest): Promise<{ certificateId: string }> {
const response = await fetch(`${this.baseUrl}/certificates`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`Certificate service responded ${response.status}`);
}
return (await response.json()) as { certificateId: string };
}
}
export function chooseIssuer(
useRemoteIssuer: boolean,
legacy: CertificateIssuer,
remote: CertificateIssuer,
): CertificateIssuer {
return useRemoteIssuer ? remote : legacy;
}
Newman mentions a variation, verify branch by abstraction, that falls back to the old implementation when the new one fails. It adds complexity and requires both implementations to see consistent data. He also reminds you to delete toggles once the switch is permanent; stale flags are a real maintenance cost.
Parallel run: call both, trust one
Strangler fig and branch by abstraction let old and new coexist, but you still choose one per request. A parallel run calls both and compares the results, while only one remains the source of truth. Newman's credit derivative pricing story is the vivid example: both systems priced trades for a month, a reconciliation report surfaced differences, and several turned out to be bugs in the old system. The comparison should cover non-functional behaviour too: latency, error rates and time-outs. For side effects like sending email, a spy records what would have happened instead of doing it twice.
type Outcome<T> = { ok: true; value: T } | { ok: false; error: unknown };
async function settle<T>(run: () => Promise<T>): Promise<Outcome<T>> {
try {
return { ok: true, value: await run() };
} catch (error) {
return { ok: false, error };
}
}
export async function parallelRun<T>(
name: string,
trusted: () => Promise<T>,
candidate: () => Promise<T>,
equivalent: (a: T, b: T) => boolean,
report: (name: string, details: Record<string, unknown>) => void,
): Promise<T> {
const [trustedOutcome, candidateOutcome] = await Promise.all([
settle(trusted),
settle(candidate),
]);
if (!candidateOutcome.ok) {
report(name, { candidateError: String(candidateOutcome.error) });
} else if (trustedOutcome.ok && !equivalent(trustedOutcome.value, candidateOutcome.value)) {
report(name, { trusted: trustedOutcome.value, candidate: candidateOutcome.value });
}
if (!trustedOutcome.ok) {
throw trustedOutcome.error;
}
return trustedOutcome.value;
}
He distinguishes this from canary releasing (a subset of users gets the new version) and dark launching (the new version runs but users cannot see it), grouping all three under progressive delivery. Parallel runs are expensive to build, so he reserves them for high-risk functionality.
Decorating collaborator and change data capture
Two patterns trigger new behaviour without changing the monolith. A decorating collaborator lets the call reach the monolith as usual, then uses the request or response to call a new service, such as awarding loyalty points after a successful order. It works when the request and response already carry the data you need; if the proxy has to call back into the monolith for more, the design gets tangled.
Change data capture reacts to changes in the monolith's datastore instead. Newman compares three implementations: database triggers (easy to start with, a slippery slope if overused), transaction log pollers (the neatest option, running outside the database and seeing only committed changes) and batch delta copiers (simple, but you have to work out what changed). The unavoidable cost is coupling to the monolith's schema, so he keeps the pattern for cases where the edge patterns do not work.
Reading it in 2026
- Strangler fig at the edge. The proxy is now often an API gateway, a CDN rule or the framework itself. For React teams, Next.js multi-zones split one domain across several apps using rewrites, which is a strangler for the frontend. Rewrites are the recommended routing mechanism, with middleware for cases that need a dynamic decision, such as a flag during migration.
- Feature toggles. Branch by abstraction pairs naturally with a flag service. OpenFeature, a CNCF project, standardises the flag evaluation API so the toggle code does not lock you into one vendor.
- Service meshes have settled. Newman advised waiting for the space to stabilise. Istio's ambient mode, which removes per-pod sidecars, reached general availability in Istio 1.24 in November 2024, and sidecar mode remains supported.
- Change data capture. Debezium is the usual log-based CDC tool, and its outbox event router builds on the same idea (covered in the next part). GitHub Scientist and its ports remain the reference implementation of the parallel run idea at code level.
- Micro frontends. Module Federation, multi-zones and plain server-side composition all exist, and the book's caution still applies: compose by vertical slices that one team owns, or you rebuild a distributed monolith in the browser.
Putting it to work
- Put a pass-through proxy in front of the monolith now, even before the first extraction, and measure the latency it adds.
- Deploy each new service to production before it takes traffic, and release by switching a route or a flag.
- Freeze behaviour changes for functionality while it is being migrated; queue bug fixes and features until the switch is done.
- Use branch by abstraction instead of long-lived branches for anything that is not reachable from the edge, and delete the toggle afterwards.
- For high-risk calculations, run old and new in parallel and review a mismatch report before switching the source of truth.
- On the frontend, migrate by whole routes or sections owned by one team before considering finer-grained composition.
Check yourself
What are the three steps of an HTTP strangler fig migration? Insert a pass-through proxy, build and deploy the new service without traffic, then redirect the relevant calls to it.
Why should behaviour changes wait until a migration is complete? Because rolling back to the old implementation would silently remove those fixes or features from users.
When is branch by abstraction preferable to a strangler fig? When the functionality is called from deep inside the monolith and cannot be intercepted at its perimeter.
How does a parallel run differ from a canary release? A parallel run sends every request to both implementations and compares them; a canary sends only some users to the new one.
What is the main cost of change data capture? Coupling to the monolith's datastore and schema, plus the complexity of triggers or log-based tooling.
Further reading
- Martin Fowler: Strangler Fig Application: the original write-up of the pattern.
- Next.js: multi-zones: splitting one domain across several Next.js apps.
- OpenFeature: a vendor-neutral API for feature flags.
- Istio: ambient mode reaches general availability: the sidecar-less mesh mode.