The strangler fig pattern gets its name from the actual plant: a fig that grows around a host tree, gradually taking over its structure until the original tree can be removed and the fig stands on its own. In software it means routing traffic incrementally from an old system to a new one, feature by feature, until the old system can be retired with nobody having flipped a single big-bang switch. I've used this pattern twice on Acumatica engagements to replace legacy customization code without a risky cutover weekend, and both times the discipline of "small, reversible slices" is what made it survivable.
When you actually need this, versus when you're overthinking a refactor
Strangler fig is for replacing something a live business depends on, where a hard cutover carries real risk - a legacy pricing engine bolted onto Sales Order Entry a decade ago through increasingly tangled RowSelected handlers, say, that nobody fully trusts anymore but that still runs every order for a business that can't tolerate a bad pricing bug on day one of a rewrite. It is overkill for a self-contained customization nobody else depends on; just rewrite that one directly. The pattern earns its complexity specifically when the blast radius of getting it wrong is large and the thing being replaced is deeply entangled with live transaction flow.
Finding (or building) the seam to route through
The hard part of strangler fig anywhere is finding a seam you can route traffic through incrementally, and on Acumatica the natural seams are graph actions and event handlers, because both already sit at defined entry points in the transaction flow. On the legacy pricing engine project, the seam was the price-calculation call inside SOLine's FieldUpdated handler for quantity changes - one method, called from a handful of places, that I could redirect without touching the rest of the tangled legacy logic yet.
public class SOOrderEntry_PricingBridge : PXGraphExtension<SOOrderEntry>
{
public static bool IsActive() => true;
protected virtual decimal CalculatePrice(SOLine line)
{
// The strangler seam: a per-customer-class flag lets us migrate
// one segment of the business at a time, and roll back instantly
// if the new engine misbehaves for that segment.
if (NewPricingEngineEnabledFor(line))
return NewPricingEngine.Calculate(line);
return LegacyPricingCalculator.Calculate(Base, line); // untouched legacy path
}
private bool NewPricingEngineEnabledFor(SOLine line)
{
var setup = PXSelect<UsrPricingMigrationSetup>.Select(Base);
return setup?.NewEngineCustomerClasses?.Contains(line.CustomerClassID) == true;
}
}
That single conditional is the whole pattern. It looks almost too simple to be an architecture pattern, and that's the point - the value isn't in clever code, it's in the discipline of expanding the set of customer classes routed to the new engine gradually, watching each cohort in production, and being able to flip any cohort back to the legacy path in seconds if something's wrong, rather than discovering a pricing bug across the entire customer base on day one.
What goes wrong when teams skip the discipline
I've watched a strangler fig migration fail in slow motion on a different project, not because the pattern was wrong but because the team treated the routing flag as a one-time toggle instead of a genuine phased rollout - they flipped every customer class to the new engine in one release, "since we built the flag anyway," and when a rounding edge case in the new engine surfaced against one particular customer's contract pricing, the flag had already been removed from the codebase as "cleanup" the same release. There was no seam left to fall back through. The pattern only earns its keep if you actually use the gradual part; a strangler fig with a flag you delete on day one is just a rewrite with extra steps.
My rule now: the routing flag and the legacy code path both stay in the codebase for at least one full business cycle after the last cohort migrates - a full month-end and quarter-end close, at minimum, for anything touching financial calculations - before I delete the old path. The cost of a slightly larger codebase for a few extra weeks is nothing compared to the cost of discovering a migration bug with no fallback during a close.
Actually retiring the host tree
The pattern's namesake step - removing the original tree once the fig has fully taken over - is the step most likely to get skipped indefinitely, because once every cohort routes to the new path, there's no urgent pressure to delete the old one. I've inherited instances with three-year-old "temporary" legacy branches still sitting in the codebase behind a flag nobody's flipped false in two years. Put a concrete date on the removal step when you plan the migration, not "eventually," and treat leaving dead legacy code in a live financial system indefinitely as its own quiet risk - it's one more thing a future developer might accidentally reactivate, and one more thing that has to be understood by anyone auditing the customization.
Wrapping up
Strangler fig on Acumatica means finding a real seam - usually a graph action or a specific event handler - routing a shrinking cohort through the legacy path and a growing cohort through the new one behind a deliberate flag, and watching each cohort in production before expanding it. The pattern only works if the migration stays gradual and reversible in practice, not just in the initial design, and it isn't finished until the legacy path is actually deleted on a scheduled date, not left behind a flag indefinitely because nothing's forcing the cleanup.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.