Strangler Fig Pattern

The Strangler Fig pattern is one of the most powerful and proven strategies for modernizing legacy systems. Named after the strangler fig tree that gradually grows around and eventually replaces its host tree, this pattern allows you to incrementally replace a legacy system without a risky "big bang" rewrite.

This approach has been successfully used by companies like Spotify, Netflix, and SoundCloud to migrate monolithic applications to microservices, modernize frontend frameworks, and replace entire legacy platforms—all while keeping production systems running.

Video Summary

The accompanying video presentation provides a deep dive into the Strangler Fig pattern, covering:

  • The biological metaphor and how it applies to software systems
  • Step-by-step implementation approach with real-world examples
  • How to route traffic between old and new implementations
  • Strategies for identifying which functionality to strangle first
  • Common challenges and how to overcome them
  • Success stories from companies that used this pattern at scale

Key Concepts

1. Incremental Replacement Over Big Rewrites

Instead of shutting down the legacy system and building a replacement from scratch, the Strangler Fig pattern advocates for incremental replacement. You build new functionality alongside the old system, gradually routing more traffic to the new implementation until the legacy system can be safely decommissioned.

Why this works:

  • Business continues operating normally during migration
  • You can stop or pivot if the approach isn't working
  • Smaller changes are easier to test and validate
  • Team learns as they go, improving the new system
  • Lower risk than all-or-nothing rewrites

2. The Facade or Router Layer

The key technical component is an abstraction layer that routes requests to either the old or new implementation:

// Simple routing based on feature flags
class PaymentFacade {
  constructor(
    private legacyPaymentService: LegacyPaymentService,
    private modernPaymentService: ModernPaymentService,
    private featureFlags: FeatureFlags
  ) {}

  async processPayment(payment: Payment): Promise<PaymentResult> {
    if (this.featureFlags.isEnabled('modern-payment-service')) {
      return this.modernPaymentService.process(payment);
    }
    return this.legacyPaymentService.process(payment);
  }
}

3. Gradual Traffic Migration

Don't switch everything at once. Migrate traffic gradually:

  1. Start with 0% - New code exists but isn't used
  2. Internal testing - Route developer traffic to new system
  3. Canary - 5% of production traffic
  4. Progressive rollout - 25%, 50%, 75%, 100%
  5. Decommission - Remove old system once fully strangled

4. Functionality-First, Not Technology-First

Choose what to strangle based on business value and risk, not just technical cleanliness:

Good candidates to strangle first:

  • New features (build in new system from the start)
  • Frequently changing functionality (high business value)
  • Well-bounded domains with clear interfaces
  • Lower-risk areas with good test coverage

Bad candidates to start with:

  • Complex, tangled code with unclear boundaries
  • Rarely-changed stable functionality
  • Core business logic with poor understanding
  • Features with many dependencies

Real-World Applications

Example 1: E-commerce Checkout Migration

Scenario: Legacy PHP monolith, migrating to Node.js microservices.

Approach:

  1. Create new checkout service in Node.js
  2. Add routing layer at API gateway
  3. Route new users to new checkout, existing users to old
  4. Gradually migrate existing users
  5. Remove old checkout code after 6 months

Results:

  • Zero downtime during migration
  • 60% faster checkout flow in new system
  • Found and fixed bugs in new system using parallel run
  • Learned lessons applied to migrating other services

Example 2: Frontend Framework Migration

Scenario: AngularJS app migrating to React.

Approach:

// Route-based strangling
function AppRouter() {
  const route = useCurrentRoute();

  // New routes use React
  if (MODERNIZED_ROUTES.includes(route)) {
    return <ModernReactApp route={route} />;
  }

  // Old routes still use AngularJS
  return <LegacyAngularApp route={route} />;
}

Migration sequence:

  1. New features built in React
  2. Migrate high-traffic pages (home, search)
  3. Migrate frequently-changing pages (marketing)
  4. Migrate stable pages (settings, help)
  5. Remove AngularJS after 18 months

Example 3: Database Migration

Scenario: MySQL to PostgreSQL for better JSON support.

Approach:

  1. Set up PostgreSQL with replicated data
  2. All reads from MySQL, writes go to both (dual writes)
  3. Gradually move reads to PostgreSQL table by table
  4. Verify data consistency continuously
  5. Switch writes to PostgreSQL only
  6. Decommission MySQL

Common Pitfalls

1. Strangling Too Much at Once

Problem: Trying to migrate an entire module in one go.

Why it fails: Large changes are risky and hard to roll back. If something goes wrong, blast radius is huge.

Solution: Break migrations into smaller pieces. Strangle individual API endpoints or UI routes, not entire systems.

2. Ignoring the Facade/Router Cost

Problem: The routing layer becomes complex and buggy.

Why it fails: Maintaining two systems plus routing logic is more work than maintaining one system.

Solution: Keep routing logic simple. Set time limits for parallel operation. Don't let the transitional state become permanent.

3. No Clear Completion Criteria

Problem: "We'll know we're done when we're done."

Why it fails: Without clear criteria, migration drags on forever. Both systems run in parallel indefinitely.

Solution: Define specific milestones:

  • "When new system handles 100% of traffic for 30 days with no issues"
  • "When legacy system receives zero requests for 14 days"
  • "When all data validations show 100% consistency"

4. Premature Decommissioning

Problem: Removing old system too quickly after migration.

Why it fails: Edge cases emerge after months. If you deleted old system, you can't easily roll back or reference it.

Solution: Keep old system running (but idle) for 3-6 months. Archive code and data before final deletion. Have a clear rollback plan.

Implementation Checklist

Planning Phase

  • [ ] Identify functionality to strangle first
  • [ ] Define success metrics and completion criteria
  • [ ] Design facade/routing layer
  • [ ] Plan gradual rollout strategy
  • [ ] Document rollback procedures

Development Phase

  • [ ] Build new implementation
  • [ ] Create comprehensive tests
  • [ ] Implement routing layer
  • [ ] Add monitoring and observability
  • [ ] Test both old and new paths

Migration Phase

  • [ ] Deploy new system (0% traffic)
  • [ ] Route internal/test traffic
  • [ ] Canary release to 5% of users
  • [ ] Monitor error rates and performance
  • [ ] Gradually increase traffic percentage
  • [ ] Validate business metrics

Completion Phase

  • [ ] All traffic flowing to new system
  • [ ] Old system idle for defined period
  • [ ] Data migration validated
  • [ ] Archive legacy code and data
  • [ ] Remove routing layer
  • [ ] Document lessons learned

Key Takeaways

  • Strangler Fig pattern enables safe, incremental replacement of legacy systems
  • The routing/facade layer is crucial for controlling traffic flow
  • Migrate functionality piece by piece, not all at once
  • Choose what to strangle based on business value and risk
  • Set clear completion criteria to avoid indefinite parallel operation
  • Keep old system available for rollback until you're confident in new system
  • This pattern has been proven at scale by major technology companies

Further Reading

Sunsetting - Strangler Fig Pattern | Sunsetting Learn