Index

Changing Existing Software

The Strangler Pattern Still Needs an Exit Plan

A replacement proof of concept imported one provider’s postal data into PostgreSQL and served one existing JSON-RPC search method from a Go binary. A checked-in request and response fixture preserved field names, nulls, empty strings, ordering, and identifier shape. Import failure left the last committed dataset intact.

The slice deliberately excluded every other provider, the other RPC methods, queues, schedulers, authentication, caching, operational interfaces, and traffic cutover.

That is a good proof of one vertical path. It is not a plan for retiring the system around it.

A strangler migration needs two ledgers: what the new path can already do, and what still keeps the old system alive. Without the second, incremental replacement can become permanent duplication.

A slice proves a route, not a destination

The postal slice crossed meaningful boundaries:

provider bytes
    -> parsing
    -> atomic persistence
    -> search query
    -> JSON-RPC response

That was stronger than a library benchmark or a rewritten controller. It proved one complete request path and one import path could exist in the target runtime.

Its evidence remained deliberately narrow:

  • one provider format;
  • one country and limited locales;
  • one RPC method;
  • one sanitized fixture;
  • uncached search;
  • a local PostgreSQL stack; and
  • no production traffic.

The retained benchmark document even prohibited comparing its local Go numbers with the old application until both implementations used the same rows, requests, response semantics, warmup, topology, concurrency, duration, and resource limits.

That boundary is the first part of an exit plan: state exactly what the slice proves so success cannot expand by storytelling.

The retirement ledger starts with consumers

Later inventory found a client using at least six RPC methods. Its behavior depended on more than response status:

  • one result interpreted not-found as null;
  • one result fed a cached validation expression;
  • one caller used the first suggestion;
  • ordering and locale fallback were observable;
  • two deprecated methods still had callers; and
  • complete error envelopes were consumed in some paths.

Source search identified one known client. It did not establish that the consumer list was complete because access telemetry, deployment configuration, operator interviews, and external callers had not yet been verified.

The retirement ledger should therefore separate:

EvidenceMeaning
source referencea known caller exists
configurationa deployment can address the service
access telemetrya caller actually used the endpoint
contract fixturea caller-visible behavior is captured
target validationthe replacement satisfies that fixture
owner sign-offthe consumer accepts the cutover boundary

“We migrated the API” is not one checkbox. Each consumer and method needs a route, compatibility decision, and owner.

State ownership must move once

Routing reads to a new service while both systems keep writing the same logical data creates a harder system than either original.

For one slice, declare:

source of truth
    Which system owns new facts?

replication direction
    How does non-authoritative state catch up?

activation
    When does a new dataset become visible?

rollback
    Which retained state can become authoritative again?

retirement
    What old write path is removed?

The proof of concept used an atomic refresh: malformed or failed import left the previous committed data active. The later replacement inventory went further. It proposed immutable dataset revisions, an atomic active pointer, retention of a previous revision, and explicit source identity.

Those are exit-plan concerns because they make authority and rollback observable. Copying rows into a second database without deciding which import may write next does not strangle the old owner. It adds another one.

The target schema does not need to match the old tables. The business facts do. The retained plan required provider input, normalized output, target persistence readback, rebuilt projections, and affected RPC results to agree. Table-shape equality was explicitly rejected as proof.

Operations are part of the product surface

The narrow slice excluded queues and schedulers. The full inventory later identified multiple import jobs, recurring provider schedules, manual import and diagnostic commands, failed-work recovery, health checks, and an administration interface.

Some framework mechanisms could be retired. Their outcomes could not:

framework dashboard
    may be replaced

inspect failed work
cancel or retry an import
see schedule success and failure
check migration state
    must still exist somewhere

This distinction prevents accidental parity work. Rebuilding the old admin UI screen by screen may be wasteful. Removing the only way to recover a failed import is not simplification.

For every operational capability, classify:

  • preserve the outcome with a new interface;
  • replace it with platform observability or control;
  • retire it with evidence that nobody needs it; or
  • keep it unresolved and block retirement.

The retained inventory still had unresolved operator ownership, authentication, alert routing, schedule misfire policy, retry budgets, and dead-letter retention. A passing RPC slice cannot answer those questions.

Traffic movement needs its own state machine

A strangler diagram often shows a router gradually sending traffic from old to new. The useful part is not the arrow. It is the selection and reversal contract.

A cutover state machine might be:

observe
    target receives copied requests but owns no effect

compare
    target results are checked against the source

limited
    an identified caller or request class uses the target

primary
    target owns the path; source remains a bounded fallback

retired
    fallback and source capability are removed

Each transition needs entry evidence, monitoring, a rollback action, and a maximum time in the mixed state.

Shadow traffic is useful for read-only or safely captured work. It does not prove write equivalence unless duplicate effects are prevented and state comparison is meaningful. Percentage routing can reduce blast radius, but it can also split one customer’s related operations across incompatible state owners.

Choose the unit of routing from the contract: consumer, tenant, account, dataset revision, operation identity, or another stable boundary. Random request percentage is not automatically gradual safety.

Every slice needs a removal condition

The proof-of-concept repository later classified its initial implementation as historical evidence rather than the production replacement. Some files were marked for recreation from approved contracts; the narrow POC source was marked for deletion.

That is not failure. A POC can answer “is this route viable?” without becoming the foundation which must carry the full migration.

Each slice should end with two possible decisions:

continue
    the slice meets its proof contract and is suitable for the next boundary

discard or rebuild
    the evidence remains useful, but the implementation is not the target

If the slice continues, name the old capability it will remove. “Add the next provider” grows the target. “Move provider import authority and disable the old schedule” advances retirement.

A useful retirement row contains:

FieldQuestion
capabilityWhat outcome is moving?
consumersWho observes or operates it?
state ownerWho may write before and after cutover?
compatibilityWhich behavior must remain?
routeHow is traffic selected?
rollbackWhat state and code restore service?
evidenceWhat permits the next transition?
removalWhich old producer, reader, or operation disappears?

The synthetic reproduction evaluates two slices. Both pass their local behavior fixture. Only one can advance retirement because it also has complete consumer evidence, single state ownership, operational replacement, rollback proof, and a named old capability to remove.

The model is intentionally strict and synthetic. It does not reproduce the private migration or claim that every real transition is boolean.

Progress is old responsibility removed

Lines of new code, endpoints implemented, and traffic percentages are useful delivery measures. They do not show whether the legacy system can be turned off.

Track retirement:

  • old producers disabled;
  • old reads no longer served;
  • old schedules removed;
  • old data authority transferred;
  • old operational tasks replaced;
  • old dependencies no longer required; and
  • fallback windows expired.

A strangler slice earns its name when it reduces the old system’s responsibility. Until then, it may be a valuable parallel implementation, a benchmark, or a learning exercise.

Build narrow slices. Just give each one a destination: a specific contract proven, a specific authority moved, and a specific old responsibility which can eventually disappear.