Low Risk
No big-bang rewrite. If migration fails, roll back easily. Legacy system keeps working.
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.
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.
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.
Facade/Proxy Layer
Legacy System
New System
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 PaymentIncomplete 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 transitiondef 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 orderPros: Simple Cons: Can get inconsistent if one write fails
# Monolith publishes eventsdef 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 serviceNOTIFICATION_SERVICE_PERCENTAGE = 5
# Week 1: Monitor metricsNOTIFICATION_SERVICE_PERCENTAGE = 5
# Week 2: No issues, increaseNOTIFICATION_SERVICE_PERCENTAGE = 20
# Week 3: Looking goodNOTIFICATION_SERVICE_PERCENTAGE = 50
# Week 4: Almost thereNOTIFICATION_SERVICE_PERCENTAGE = 90
# Week 5: Full migration!NOTIFICATION_SERVICE_PERCENTAGE = 100
# Week 6: Remove legacy code# Delete old notification code from monolithOnce 100% migrated:
Verify no traffic to legacy endpoint
-- Check logs for any legacy callsSELECT COUNT(*) FROM access_logsWHERE endpoint = '/legacy/notifications'AND timestamp > NOW() - INTERVAL '7 days';Remove code from monolith
# Delete old notification code# DELETE: monolith/models/notification.py# DELETE: monolith/controllers/notification_controller.pyDrop legacy tables
-- Archive data first!CREATE TABLE notifications_archive ASSELECT * FROM notifications;
-- Then dropDROP TABLE notifications;Update documentation
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 GatewayPhase 2: Extract business logic → New servicesPhase 3: Extract data access → New databasesUsually not recommended - features are better boundaries.
Create abstraction, switch implementation:
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:
Problem: Legacy and new system out of sync
Solution:
Problem: Running both systems is complex
Solution:
Problem: “Leaf service” turns out to have hidden dependencies
Solution:
Starting Point:
Strategy:
Results:
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:
Strategy:
Results:
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:
Results:
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 configurationroutes: - 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 serviceCREATE PUBLICATION legacy_pub FOR TABLE orders, users;
-- New service subscribesCREATE SUBSCRIPTION new_service_subCONNECTION '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.