Skip to content
Low Level Design Mastery Logo
LowLevelDesign Mastery

Strangler Fig Pattern

Evolution over revolution - migrate safely and incrementally

The Strangler Fig Pattern is a migration strategy for gradually replacing legacy systems without risky big-bang rewrites. Instead of replacing everything at once, you build new functionality alongside the old system, gradually route traffic to the new system, and eventually decommission the legacy system.

Diagram

Imagine you have an old tree house that needs to be replaced. Instead of tearing it down all at once (risky!), you build a new section next to the old one, move one room at a time to the new structure, keep the old parts working while building new ones, and eventually remove the old parts when everything is moved. This is exactly how the Strangler Fig Pattern works for software.

Real-World Scenario: The Big Rewrite Problem

Section titled “Real-World Scenario: The Big Rewrite Problem”

Big rewrites fail frequently. Statistics show that 70% of large software rewrites fail. The average time for a “complete rewrite” is 2-3 years. During the rewrite, features are frozen and the business suffers. Many rewrites are abandoned mid-way due to scope creep.

Famous failures: Netscape 6 (1998-2000) attempted a complete rewrite and lost the browser market to Internet Explorer. Mozilla Thunderbird spent years on rewrites and never caught up. Twitter (2009-2011) attempted partial rewrites that caused major outages.

The problem: Big rewrites are high-risk, high-cost, and often fail. The Strangler Fig Pattern provides a safer, incremental alternative that reduces risk and allows continuous delivery of value.


Diagram
  1. Facade/Proxy Layer

    • Routes requests to either legacy or new system
    • Gradually shifts traffic
    • Transparent to clients
  2. Legacy System

    • Continues to run
    • Gradually shrinks
    • Eventually decommissioned
  3. New System

    • Built incrementally
    • Runs alongside legacy
    • Gradually expands

Don’t extract randomly! Choose services with:

Leaf services (few dependencies): NotificationService only receives events and doesn’t depend on others. ReportingService is read-only and doesn’t affect core business. These are safe to extract first because they have minimal dependencies.

High-change areas (frequent updates): Pricing Engine has business rules that change often. Recommendation service has ML models updated frequently. Extracting these allows independent deployment and faster iteration.

Performance bottlenecks (need independent scaling): Search Service needs 50 instances. Image Processing is CPU-intensive and needs GPU. Extracting these allows independent scaling without scaling the entire monolith.

Clear business capabilities (well-defined boundaries): Payment Processing, User Authentication, and Order Management have clear boundaries. These are good candidates because they represent distinct business domains.

Core entities with many dependents: User Service has everything depending on it—extract this later. Product Service is central to business logic. Extracting these early creates too many dependencies and increases risk.

Tightly coupled modules:

OrderLine → Part of Order entity (extract together)
PaymentDetails → Embedded in Payment

Incomplete business capabilities: “Order Validation” alone is part of Order Management. “Email Sending” alone is part of Notification Service. Don’t extract incomplete capabilities—extract complete business features.

Before extracting, create clear interfaces in the monolith.

Route traffic between legacy and new system.

Build the new service with its own database.

Challenge: Legacy and new service need the same data during transition!

# In monolith during transition
def create_order(user_id, items):
# Write to legacy DB
order = Order(user_id=user_id, items=items)
legacy_db.add(order)
# ALSO write to new service
try:
httpx.post(
"http://order-service/orders",
json=order.to_dict()
)
except Exception as e:
# Log error but don't fail
# New service will eventually sync
logger.error(f"Failed to sync order: {e}")
return order

Pros: Simple Cons: Can get inconsistent if one write fails

# Monolith publishes events
def create_order(user_id, items):
order = Order(user_id=user_id, items=items)
legacy_db.add(order)
# Publish event
event_bus.publish(OrderCreatedEvent(order))
return order
# New service subscribes to events
@event_handler(OrderCreatedEvent)
def sync_order(event: OrderCreatedEvent):
# New service builds its own view
order = Order(
id=event.order_id,
user_id=event.user_id,
...
)
new_db.add(order)

Pros: Eventual consistency, reliable Cons: More complex

Use tools like Debezium to capture database changes:

# Debezium connector config
{
"name": "legacy-db-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "legacy-db",
"database.dbname": "ecommerce",
"table.include.list": "public.orders,public.users",
"transforms": "route",
"transforms.route.type": "org.apache.kafka.connect.transforms.RegexRouter",
"transforms.route.regex": ".*",
"transforms.route.replacement": "order-events"
}
}

Pros: Zero code changes in legacy system Cons: Requires CDC infrastructure

Use feature flags to control traffic:

# Start with 5% traffic to new service
NOTIFICATION_SERVICE_PERCENTAGE = 5
# Week 1: Monitor metrics
NOTIFICATION_SERVICE_PERCENTAGE = 5
# Week 2: No issues, increase
NOTIFICATION_SERVICE_PERCENTAGE = 20
# Week 3: Looking good
NOTIFICATION_SERVICE_PERCENTAGE = 50
# Week 4: Almost there
NOTIFICATION_SERVICE_PERCENTAGE = 90
# Week 5: Full migration!
NOTIFICATION_SERVICE_PERCENTAGE = 100
# Week 6: Remove legacy code
# Delete old notification code from monolith

Once 100% migrated:

  1. Verify no traffic to legacy endpoint

    -- Check logs for any legacy calls
    SELECT COUNT(*) FROM access_logs
    WHERE endpoint = '/legacy/notifications'
    AND timestamp > NOW() - INTERVAL '7 days';
  2. Remove code from monolith

    monolith/services/notification_service.py
    # Delete old notification code
    # DELETE: monolith/models/notification.py
    # DELETE: monolith/controllers/notification_controller.py
  3. Drop legacy tables

    -- Archive data first!
    CREATE TABLE notifications_archive AS
    SELECT * FROM notifications;
    -- Then drop
    DROP TABLE notifications;
  4. Update documentation

    • Update architecture diagrams
    • Update API documentation
    • Update team ownership

Extract complete features (vertical slices):

Extract complete features: Notification Service includes send email, send SMS, send push notification, query notification history, and manage notification preferences. Don’t extract incomplete features like email sender only—extract complete business capabilities.

Extract by technical layer (less common):

Phase 1: Extract presentation layer → New API Gateway
Phase 2: Extract business logic → New services
Phase 3: Extract data access → New databases

Usually not recommended - features are better boundaries.

Create abstraction, switch implementation:

Diagram

Low Risk

No big-bang rewrite. If migration fails, roll back easily. Legacy system keeps working.

Incremental Value

Deliver value continuously. Each extracted service brings immediate benefits.

Learning Opportunity

Learn microservices gradually. Mistakes in first service don’t affect the whole system.

Business Continuity

No feature freeze. Continue delivering features while migrating.


Problem: Migration takes too long, loses momentum

Solution:

  • Set clear deadlines for each service
  • Celebrate milestones
  • Measure progress (% of traffic migrated)
  • Dedicate resources

Problem: Legacy and new system out of sync

Solution:

  • Use event-driven architecture
  • Implement reconciliation jobs
  • Monitor data quality metrics

Problem: Running both systems is complex

Solution:

  • Good observability (tracing across both systems)
  • Clear ownership (team owns migration end-to-end)
  • Time-boxed migration (6-12 months max per service)

Problem: “Leaf service” turns out to have hidden dependencies

Solution:

  • Thorough dependency analysis before starting
  • Visualize dependency graph
  • Extract dependencies first

Soundcloud: Monolith to Microservices (2014-2018)

Section titled “Soundcloud: Monolith to Microservices (2014-2018)”

Starting Point:

  • Ruby on Rails monolith
  • 3M+ users
  • Growing team (50+ engineers)

Strategy:

  • Identified bounded contexts (User, Track, Playlist)
  • Extracted services gradually over 4 years
  • Used event sourcing for data sync

Results:

  • Successfully migrated to 100+ microservices
  • Improved deployment frequency (5x)
  • Reduced time-to-market for features

Key Lesson:

“We didn’t try to do it all at once. We extracted services as we needed to scale or change them.” - Soundcloud Engineering

Starting Point:

  • Monolithic DVD rental application
  • Database couldn’t scale

Strategy:

  • Started by extracting video streaming service
  • Gradually migrated over 7 years
  • Built extensive tooling for microservices

Results:

  • 700+ microservices
  • Global scale (200M+ subscribers)
  • Industry leader in microservices practices

Key Lesson:

“The strangler pattern was essential. We couldn’t pause the business for a rewrite.” - Netflix Engineering

The Mandate:

“All teams will expose their data and functionality through service interfaces. No other form of communication allowed.” - Jeff Bezos, 2002

Strategy:

  • Top-down mandate
  • Each team migrated their module
  • Built AWS tools to support migration

Results:

  • Full SOA by 2006
  • Foundation for AWS
  • Enabled massive innovation

from unleash import UnleashClient
client = UnleashClient(url="http://unleash:4242")
def notify_user(order):
if client.is_enabled("use_notification_service"):
# Call new service
notification_service.notify(order)
else:
# Use legacy code
email_sender.send(order)
# Kong API Gateway configuration
routes:
- name: notifications-new
paths: ["/api/notifications"]
service: notification-service
plugins:
- name: rate-limiting
- name: request-transformer
config:
add:
headers: ["X-Source:gateway"]
- name: notifications-legacy
paths: ["/api/notifications"]
service: legacy-monolith
plugins:
- name: canary
config:
percentage: 10 # 10% to new service
-- Replicate legacy database to new service
CREATE PUBLICATION legacy_pub FOR TABLE orders, users;
-- New service subscribes
CREATE SUBSCRIPTION new_service_sub
CONNECTION 'host=legacy-db dbname=ecommerce'
PUBLICATION legacy_pub;

Don't Rewrite!

Big rewrites fail 70% of the time. Strangler Fig pattern migrates gradually and safely.

Start with Leaves

Extract services with few dependencies first. Learn from early migrations.

Dual-Run is Key

Run old and new systems in parallel. Route traffic gradually. Validate correctness.

Set Time Limits

Migrations can drag on forever. Set clear deadlines and celebrate milestones.



  • “Monolith to Microservices” by Sam Newman (entire book on this topic!)
  • “Working Effectively with Legacy Code” by Michael Feathers
  • Martin Fowler’s Strangler Fig Article - The original
  • “Refactoring Databases” by Scott Ambler
  • Netflix Tech Blog - Real-world migration stories