<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Strangler Fig Pattern | Alan P. Barber</title><link>https://alanbarber.com/tags/strangler-fig-pattern/</link><atom:link href="https://alanbarber.com/tags/strangler-fig-pattern/index.xml" rel="self" type="application/rss+xml"/><description>Strangler Fig Pattern</description><generator>Source Themes Academic (https://sourcethemes.com/academic/)</generator><language>en-us</language><copyright>© 2026 Alan P. Barber &amp;middot; Hosting by [CldSvr.com](http://cldsvr.com)</copyright><lastBuildDate>Sat, 05 Sep 2026 00:00:00 -0400</lastBuildDate><image><url>https://alanbarber.com/img/portrait.jpg</url><title>Strangler Fig Pattern</title><link>https://alanbarber.com/tags/strangler-fig-pattern/</link></image><item><title>The Strangler Fig Pattern for C# Developers</title><link>https://alanbarber.com/post/the-strangler-fig-pattern-for-csharp-developers/</link><pubDate>Sat, 05 Sep 2026 00:00:00 -0400</pubDate><guid>https://alanbarber.com/post/the-strangler-fig-pattern-for-csharp-developers/</guid><description>&lt;p>Rewriting a legacy system in one big push almost always goes badly. The scope creeps, the business can&amp;rsquo;t freeze feature work for as long as the rewrite takes, and by the time you ship, the requirements have changed again. The Strangler Fig pattern offers a different path: build the replacement incrementally around the edges of the old system, redirecting a little more traffic to it with every release, until the legacy system has nothing left to do and you can turn it off.&lt;/p>
&lt;h2 id="lets-define-the-pattern">Let&amp;rsquo;s Define the Pattern&lt;/h2>
&lt;p>The name comes from the
&lt;a href="https://en.wikipedia.org/wiki/Strangler_fig" target="_blank" rel="noopener">strangler fig&lt;/a>, a vine that germinates in the branches of a host tree. It grows downward, wrapping around the trunk and drawing nutrients until it reaches the ground and becomes self-sufficient. Eventually the host tree dies and rots away, leaving the fig standing in its place—same silhouette, entirely new structure. Martin Fowler borrowed the metaphor in 2004 to describe a style of legacy modernization where a new system is built up around an old one, taking over its responsibilities piece by piece.&lt;/p>
&lt;p>Architecturally, the pattern relies on a façade (also called a router or a proxy) that sits between clients and the systems doing the work. The façade intercepts every incoming request and decides, feature by feature, whether the legacy system or the new system should handle it. Early on, almost everything goes to the legacy system. As you migrate functionality, you flip more routes over to the new system. Clients never know the difference—they keep hitting the same façade throughout the whole migration.&lt;/p>
&lt;p>This is not a GoF pattern in the classic sense—there&amp;rsquo;s no single class diagram to memorize. It&amp;rsquo;s an architectural strategy, and in C# it&amp;rsquo;s usually implemented with routing middleware, a reverse proxy, or a feature-flag-driven dispatcher rather than a small set of cooperating objects.&lt;/p>
&lt;h2 id="the-problem-it-solves">The Problem It Solves&lt;/h2>
&lt;p>Imagine a monolithic ASP.NET application, &lt;code>LegacyOrderSystem&lt;/code>, that has handled order creation, invoicing, and shipping for a decade. It&amp;rsquo;s slow to change, tightly coupled to an aging database schema, and everyone is afraid to touch it. Leadership wants it replaced with a modern set of services.&lt;/p>
&lt;p>&lt;strong>Option 1: Big bang rewrite&lt;/strong>&lt;/p>
&lt;p>Freeze the legacy system, spend a year building &lt;code>NewOrderSystem&lt;/code> from scratch, then cut over all at once. This sounds clean on a slide, but in practice:&lt;/p>
&lt;ul>
&lt;li>The business can&amp;rsquo;t accept a year without new features or bug fixes to the system still generating revenue.&lt;/li>
&lt;li>Nobody fully remembers every edge case the legacy system handles, so the rewrite is guaranteed to miss behavior—some of it undocumented but load-bearing.&lt;/li>
&lt;li>The cutover is a single high-risk event. If something&amp;rsquo;s wrong, you&amp;rsquo;re rolling back a year of work under pressure.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Option 2: Strangler Fig migration&lt;/strong>&lt;/p>
&lt;p>Introduce a façade in front of &lt;code>LegacyOrderSystem&lt;/code>. Move one capability—say, invoicing—into a new service. Update the façade to route invoicing requests to the new service while everything else still goes to the legacy system. Ship it, watch it work in production, then repeat for the next capability.&lt;/p>
&lt;pre>&lt;code class="language-csharp">// Before: clients call the legacy system directly
public class OrderController : ControllerBase
{
private readonly LegacyOrderSystem _legacySystem;
[HttpPost(&amp;quot;invoices&amp;quot;)]
public IActionResult CreateInvoice(InvoiceRequest request)
=&amp;gt; Ok(_legacySystem.CreateInvoice(request));
}
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-csharp">// After: a façade decides who handles the request
public class OrderController : ControllerBase
{
private readonly IOrderRequestRouter _router;
public OrderController(IOrderRequestRouter router) =&amp;gt; _router = router;
[HttpPost(&amp;quot;invoices&amp;quot;)]
public async Task&amp;lt;IActionResult&amp;gt; CreateInvoice(InvoiceRequest request)
=&amp;gt; Ok(await _router.RouteInvoiceCreationAsync(request));
}
&lt;/code>&lt;/pre>
&lt;p>The controller no longer knows or cares which system ultimately handles the request. That decision lives in the router, which means the migration can proceed feature by feature without touching every call site each time.&lt;/p>
&lt;h2 id="core-structure-and-roles">Core Structure and Roles&lt;/h2>
&lt;p>The pattern has three participants:&lt;/p>
&lt;p>&lt;strong>Legacy system&lt;/strong>: The existing application. It keeps running and keeps serving whatever functionality hasn&amp;rsquo;t been migrated yet.&lt;/p>
&lt;p>&lt;strong>New system&lt;/strong>: The replacement, built incrementally. Early on it might handle only one narrow capability; eventually it handles everything.&lt;/p>
&lt;p>&lt;strong>Façade (router)&lt;/strong>: The single entry point clients talk to. It inspects each request and decides where it goes.&lt;/p>
&lt;pre>&lt;code class="language-csharp">public interface IOrderRequestRouter
{
Task&amp;lt;InvoiceResult&amp;gt; RouteInvoiceCreationAsync(InvoiceRequest request);
}
public class OrderRequestRouter : IOrderRequestRouter
{
private readonly LegacyOrderSystem _legacySystem;
private readonly IInvoiceService _newInvoiceService;
private readonly IMigrationFlags _flags;
public OrderRequestRouter(
LegacyOrderSystem legacySystem,
IInvoiceService newInvoiceService,
IMigrationFlags flags)
{
_legacySystem = legacySystem;
_newInvoiceService = newInvoiceService;
_flags = flags;
}
public async Task&amp;lt;InvoiceResult&amp;gt; RouteInvoiceCreationAsync(InvoiceRequest request)
{
if (_flags.IsMigrated(MigrationFeature.Invoicing))
{
return await _newInvoiceService.CreateInvoiceAsync(request);
}
return _legacySystem.CreateInvoice(request);
}
}
&lt;/code>&lt;/pre>
&lt;p>&lt;code>IMigrationFlags&lt;/code> is deliberately simple here—a lookup of which capabilities have been cut over. In a real system this might be backed by configuration, a feature-flag service, or a percentage-based rollout so you can shift traffic gradually rather than as an on/off switch.&lt;/p>
&lt;h2 id="implementing-the-façade">Implementing the Façade&lt;/h2>
&lt;p>For HTTP-based systems, the façade is often implemented as middleware or a reverse proxy rather than in-process routing logic. .NET&amp;rsquo;s
&lt;a href="https://microsoft.github.io/reverse-proxy/" target="_blank" rel="noopener">YARP&lt;/a> (Yet Another Reverse Proxy) is commonly used for exactly this: you configure routes that send some paths to the legacy application and others to new services, all behind a single public endpoint.&lt;/p>
&lt;pre>&lt;code class="language-csharp">// Program.cs - routing configured declaratively
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection(&amp;quot;ReverseProxy&amp;quot;));
app.MapReverseProxy();
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-json">{
&amp;quot;ReverseProxy&amp;quot;: {
&amp;quot;Routes&amp;quot;: {
&amp;quot;invoicing-new&amp;quot;: {
&amp;quot;ClusterId&amp;quot;: &amp;quot;new-invoice-service&amp;quot;,
&amp;quot;Match&amp;quot;: { &amp;quot;Path&amp;quot;: &amp;quot;/api/invoices/{**catch-all}&amp;quot; }
},
&amp;quot;everything-else&amp;quot;: {
&amp;quot;ClusterId&amp;quot;: &amp;quot;legacy-order-system&amp;quot;,
&amp;quot;Match&amp;quot;: { &amp;quot;Path&amp;quot;: &amp;quot;/{**catch-all}&amp;quot; }
}
},
&amp;quot;Clusters&amp;quot;: {
&amp;quot;new-invoice-service&amp;quot;: {
&amp;quot;Destinations&amp;quot;: { &amp;quot;d1&amp;quot;: { &amp;quot;Address&amp;quot;: &amp;quot;https://invoices.internal/&amp;quot; } }
},
&amp;quot;legacy-order-system&amp;quot;: {
&amp;quot;Destinations&amp;quot;: { &amp;quot;d1&amp;quot;: { &amp;quot;Address&amp;quot;: &amp;quot;https://legacy.internal/&amp;quot; } }
}
}
}
}
&lt;/code>&lt;/pre>
&lt;p>Whether you route in-process (as with &lt;code>IOrderRequestRouter&lt;/code>) or at the network edge (as with YARP) depends on your architecture. In-process routing is simpler when the new and old code share a process and a deployment. A reverse proxy is a better fit once the new system is a genuinely separate service.&lt;/p>
&lt;h2 id="strangler-fig-vs-similar-patterns">Strangler Fig vs Similar Patterns&lt;/h2>
&lt;p>&lt;strong>Strangler Fig vs Big Bang Rewrite&lt;/strong>: A big bang rewrite replaces the whole system in one cutover. Strangler Fig replaces it incrementally, with the old and new systems coexisting throughout. Use Strangler Fig whenever the legacy system is too large, too risky, or too business-critical to freeze for the length of a full rewrite.&lt;/p>
&lt;p>&lt;strong>Strangler Fig vs Branch by Abstraction&lt;/strong>:
&lt;a href="https://martinfowler.com/bliki/BranchByAbstraction.html" target="_blank" rel="noopener">Branch by Abstraction&lt;/a> introduces an abstraction layer &lt;em>inside&lt;/em> a single codebase so you can swap an implementation without long-lived branches. Strangler Fig operates at a system or service boundary, often across process and deployment boundaries. They&amp;rsquo;re complementary—you might use Branch by Abstraction inside the new system while using Strangler Fig to migrate traffic to it.&lt;/p>
&lt;p>&lt;strong>Strangler Fig vs Anti-Corruption Layer&lt;/strong>: An
&lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer" target="_blank" rel="noopener">Anti-Corruption Layer&lt;/a> translates requests and models between two systems that need to talk to each other without leaking one system&amp;rsquo;s assumptions into the other. Strangler Fig often &lt;em>uses&lt;/em> an anti-corruption layer internally—the new system needs one to call unmigrated legacy functionality without absorbing legacy quirks into its own domain model.&lt;/p>
&lt;p>&lt;strong>Strangler Fig vs Feature Toggles&lt;/strong>: A feature toggle turns functionality on or off within a single deployed system. Strangler Fig uses similar mechanics (a flag or rule deciding what happens) but the two branches are entirely separate systems, not two code paths in the same codebase.&lt;/p>
&lt;h2 id="testing">Testing&lt;/h2>
&lt;p>The router is the highest-value thing to test, because it encodes your migration state. You want confidence that flipping a flag actually changes which system handles a request, and that the two systems produce compatible results while both are live.&lt;/p>
&lt;pre>&lt;code class="language-csharp">[Fact]
public async Task RouteInvoiceCreationAsync_WhenNotMigrated_UsesLegacyGateway()
{
var legacyGateway = new Mock&amp;lt;ILegacyOrderGateway&amp;gt;();
var newService = new Mock&amp;lt;IInvoiceService&amp;gt;();
var flags = new Mock&amp;lt;IMigrationFlags&amp;gt;();
flags.Setup(f =&amp;gt; f.IsMigrated(MigrationFeature.Invoicing)).Returns(false);
var router = new OrderRequestRouter(legacyGateway.Object, newService.Object, flags.Object);
await router.RouteInvoiceCreationAsync(new InvoiceRequest());
legacyGateway.Verify(g =&amp;gt; g.CreateInvoice(It.IsAny&amp;lt;InvoiceRequest&amp;gt;()), Times.Once);
newService.Verify(s =&amp;gt; s.CreateInvoiceAsync(It.IsAny&amp;lt;InvoiceRequest&amp;gt;()), Times.Never);
}
[Fact]
public async Task RouteInvoiceCreationAsync_WhenMigrated_UsesNewService()
{
var legacyGateway = new Mock&amp;lt;ILegacyOrderGateway&amp;gt;();
var newService = new Mock&amp;lt;IInvoiceService&amp;gt;();
var flags = new Mock&amp;lt;IMigrationFlags&amp;gt;();
flags.Setup(f =&amp;gt; f.IsMigrated(MigrationFeature.Invoicing)).Returns(true);
var router = new OrderRequestRouter(legacyGateway.Object, newService.Object, flags.Object);
await router.RouteInvoiceCreationAsync(new InvoiceRequest());
newService.Verify(s =&amp;gt; s.CreateInvoiceAsync(It.IsAny&amp;lt;InvoiceRequest&amp;gt;()), Times.Once);
legacyGateway.Verify(g =&amp;gt; g.CreateInvoice(It.IsAny&amp;lt;InvoiceRequest&amp;gt;()), Times.Never);
}
&lt;/code>&lt;/pre>
&lt;p>If both systems can run side by side against the same inputs, it&amp;rsquo;s also worth writing comparison tests—call both, compare the results, and log any divergence in production before you fully cut over. This &amp;ldquo;shadow traffic&amp;rdquo; approach catches mismatches long before real users depend on the new system.&lt;/p>
&lt;h2 id="common-pitfalls-and-code-smells">Common Pitfalls and Code Smells&lt;/h2>
&lt;p>&lt;strong>Never removing the façade&lt;/strong>: The façade is transitional architecture. Once the legacy system is fully decommissioned, the router should be removed and clients should call the new system directly. Leaving it in place forever adds a permanent layer of indirection and a permanent single point of failure.&lt;/p>
&lt;p>&lt;strong>No rollback path&lt;/strong>: If flipping a migration flag can&amp;rsquo;t be reversed quickly, you&amp;rsquo;ve turned every cutover into a big-bang risk again, just spread out over more releases. Keep the old code path alive and working until you&amp;rsquo;re confident the new one has proven itself.&lt;/p>
&lt;p>&lt;strong>Letting the legacy system keep growing&lt;/strong>: If new features keep landing in the legacy system because &amp;ldquo;it&amp;rsquo;s easier for now,&amp;rdquo; the strangling process runs backward—the host tree keeps getting bigger instead of shrinking. New functionality should go into the new system, even before the migration is finished.&lt;/p>
&lt;p>&lt;strong>Ignoring shared data&lt;/strong>: If both systems read and write the same database, you need a clear answer for who owns which tables during the transition. Splitting data ownership badly is one of the most common ways these migrations stall out.&lt;/p>
&lt;p>&lt;strong>Treating the façade as free&lt;/strong>: A router or reverse proxy adds latency and becomes a dependency every request goes through. Monitor it like any other production service—it can become a bottleneck or a single point of failure if it&amp;rsquo;s an afterthought.&lt;/p>
&lt;h2 id="when-to-use-it">When to Use It&lt;/h2>
&lt;p>Reach for Strangler Fig when:&lt;/p>
&lt;ul>
&lt;li>The legacy system is large, business-critical, and can&amp;rsquo;t be frozen for the duration of a full rewrite.&lt;/li>
&lt;li>You can intercept requests to the legacy system (you have access to its source, its network boundary, or both).&lt;/li>
&lt;li>You&amp;rsquo;re comfortable running two systems in parallel for an extended period.&lt;/li>
&lt;/ul>
&lt;p>It&amp;rsquo;s probably not worth it when:&lt;/p>
&lt;ul>
&lt;li>The system is small enough that a full rewrite is genuinely low-risk.&lt;/li>
&lt;li>You can&amp;rsquo;t intercept traffic to the legacy system at all (no source access, no proxy point).&lt;/li>
&lt;li>Leadership needs the legacy system fully decommissioned on a short timeline that doesn&amp;rsquo;t allow for incremental validation.&lt;/li>
&lt;/ul>
&lt;h2 id="practical-guidelines">Practical Guidelines&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Wrap the legacy system behind an interface immediately.&lt;/strong> It makes the router testable and gives you a clean seam to delete later.&lt;/li>
&lt;li>&lt;strong>Migrate in small, independently shippable slices.&lt;/strong> Pick a single capability, migrate it, validate it in production, then move to the next one.&lt;/li>
&lt;li>&lt;strong>Make the routing decision reversible.&lt;/strong> A flag, a percentage rollout, or a config value—whatever it is, be able to flip it back without a deployment.&lt;/li>
&lt;li>&lt;strong>Plan for the façade&amp;rsquo;s removal from day one.&lt;/strong> It&amp;rsquo;s a means to an end, not permanent infrastructure.&lt;/li>
&lt;li>&lt;strong>Watch for shared state.&lt;/strong> Decide early who owns which data while both systems are live, and keep that ownership boundary explicit.&lt;/li>
&lt;li>&lt;strong>Treat the migration as a project with an end date&lt;/strong>, not an open-ended architectural aspiration. Strangler Fig works because it&amp;rsquo;s incremental and visible—not because it&amp;rsquo;s slow for its own sake.&lt;/li>
&lt;/ul></description></item></channel></rss>