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.
# BEFORE: Direct implementationclass OrderService: def create_order(self, user_id: str, items: list) -> Order: # Direct database access user = db.query(User).get(user_id)
# Create order order = Order(user_id=user_id, items=items) db.add(order)
# Send notification (tightly coupled!) email_sender.send(user.email, "Order created", ...) sms_sender.send(user.phone, "Order created")
return order
# AFTER: Interface abstractionfrom abc import ABC, abstractmethod
class NotificationService(ABC): """Abstract interface for notifications""" @abstractmethod def notify_order_created(self, order: Order): pass
class LocalNotificationService(NotificationService): """Implementation using local email/SMS""" def notify_order_created(self, order: Order): email_sender.send(...) sms_sender.send(...)
class RemoteNotificationService(NotificationService): """Implementation calling external service""" def notify_order_created(self, order: Order): # Call microservice httpx.post( "http://notification-service/notify", json={"order_id": order.id, "type": "order_created"} )
class OrderService: def __init__(self, notification_service: NotificationService): self._notifier = notification_service # Inject!
def create_order(self, user_id: str, items: list) -> Order: user = db.query(User).get(user_id) order = Order(user_id=user_id, items=items) db.add(order)
# Use abstraction (doesn't know if local or remote!) self._notifier.notify_order_created(order)
return order// BEFORE: Direct implementationpublic class OrderService { public Order createOrder(String userId, List<Item> items) { User user = userRepository.findById(userId);
Order order = new Order(userId, items); orderRepository.save(order);
// Tightly coupled! emailSender.send(user.getEmail(), "Order created"); smsSender.send(user.getPhone(), "Order created");
return order; }}
// AFTER: Interface abstractionpublic interface NotificationService { void notifyOrderCreated(Order order);}
public class LocalNotificationService implements NotificationService { @Override public void notifyOrderCreated(Order order) { emailSender.send(...); smsSender.send(...); }}
public class RemoteNotificationService implements NotificationService { private final RestTemplate restTemplate;
@Override public void notifyOrderCreated(Order order) { // Call microservice restTemplate.postForObject( "http://notification-service/notify", new NotifyRequest(order.getId(), "order_created"), Void.class ); }}
public class OrderService { private final NotificationService notificationService;
public OrderService(NotificationService notificationService) { this.notificationService = notificationService; }
public Order createOrder(String userId, List<Item> items) { User user = userRepository.findById(userId); Order order = new Order(userId, items); orderRepository.save(order);
// Use abstraction (doesn't know if local or remote!) notificationService.notifyOrderCreated(order);
return order; }}// BEFORE: Direct implementationclass OrderService { async createOrder(userId: string, items: Item[]): Promise<Order> { // Direct database access const user = await db.query(User).where({ id: userId }).first();
// Create order const order = new Order(userId, items); await db.add(order);
// Send notification (tightly coupled!) await emailSender.send(user.email, "Order created", ...); await smsSender.send(user.phone, "Order created");
return order; }}
// AFTER: Interface abstractioninterface NotificationService { notifyOrderCreated(order: Order): Promise<void>;}
class LocalNotificationService implements NotificationService { async notifyOrderCreated(order: Order): Promise<void> { await emailSender.send(...); await smsSender.send(...); }}
class RemoteNotificationService implements NotificationService { async notifyOrderCreated(order: Order): Promise<void> { // Call microservice await fetch("http://notification-service/notify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ order_id: order.id, type: "order_created" }) }); }}
class OrderService { constructor(private notificationService: NotificationService) {}
async createOrder(userId: string, items: Item[]): Promise<Order> { const user = await db.query(User).where({ id: userId }).first(); const order = new Order(userId, items); await db.add(order);
// Use abstraction (doesn't know if local or remote!) await this.notificationService.notifyOrderCreated(order);
return order; }}// BEFORE: Direct implementationclass OrderService {public: Order createOrder(const std::string& userId, const std::vector<Item>& items) { // Direct database access User user = db->query<User>().where("id", userId).first();
// Create order Order order(userId, items); db->add(order);
// Send notification (tightly coupled!) emailSender->send(user.getEmail(), "Order created", ...); smsSender->send(user.getPhone(), "Order created");
return order; }};
// AFTER: Interface abstractionclass NotificationService {public: virtual ~NotificationService() = default; virtual void notifyOrderCreated(const Order& order) = 0;};
class LocalNotificationService : public NotificationService {public: void notifyOrderCreated(const Order& order) override { emailSender->send(...); smsSender->send(...); }};
class RemoteNotificationService : public NotificationService {private: HttpClient* httpClient;
public: void notifyOrderCreated(const Order& order) override { // Call microservice HttpRequest request("http://notification-service/notify"); request.setMethod("POST"); request.setBody(Json::object{ {"order_id", order.getId()}, {"type", "order_created"} }); httpClient->send(request); }};
class OrderService {private: NotificationService* notificationService;
public: OrderService(NotificationService* notificationService) : notificationService(notificationService) {}
Order createOrder(const std::string& userId, const std::vector<Item>& items) { User user = db->query<User>().where("id", userId).first(); Order order(userId, items); db->add(order);
// Use abstraction (doesn't know if local or remote!) notificationService->notifyOrderCreated(order);
return order; }};// BEFORE: Direct implementationpublic class OrderService { public async Task<Order> CreateOrderAsync(string userId, List<Item> items) { // Direct database access User user = await db.Query<User>().Where(u => u.Id == userId).FirstAsync();
// Create order Order order = new Order(userId, items); await db.AddAsync(order);
// Send notification (tightly coupled!) await emailSender.SendAsync(user.Email, "Order created", ...); await smsSender.SendAsync(user.Phone, "Order created");
return order; }}
// AFTER: Interface abstractionpublic interface INotificationService { Task NotifyOrderCreatedAsync(Order order);}
public class LocalNotificationService : INotificationService { public async Task NotifyOrderCreatedAsync(Order order) { await emailSender.SendAsync(...); await smsSender.SendAsync(...); }}
public class RemoteNotificationService : INotificationService { private readonly HttpClient httpClient;
public async Task NotifyOrderCreatedAsync(Order order) { // Call microservice await httpClient.PostAsync( "http://notification-service/notify", new StringContent(JsonSerializer.Serialize(new { order_id = order.Id, type = "order_created" })) ); }}
public class OrderService { private readonly INotificationService notificationService;
public OrderService(INotificationService notificationService) { this.notificationService = notificationService; }
public async Task<Order> CreateOrderAsync(string userId, List<Item> items) { User user = await db.Query<User>().Where(u => u.Id == userId).FirstAsync(); Order order = new Order(userId, items); await db.AddAsync(order);
// Use abstraction (doesn't know if local or remote!) await notificationService.NotifyOrderCreatedAsync(order);
return order; }}Route traffic between legacy and new system.
from fastapi import FastAPI, Requestimport httpx
app = FastAPI()
class StranglerRouter: def __init__(self): self._legacy_client = httpx.AsyncClient( base_url="http://legacy-monolith:8080" ) self._new_client = httpx.AsyncClient( base_url="http://notification-service:8080" )
# Feature flags control routing self._migrated_endpoints = { "/api/notifications": 100, # 100% to new service "/api/orders": 50, # 50% to new service "/api/users": 0, # 0% to new service (still legacy) }
async def route_request(self, request: Request): path = request.url.path
# Check if endpoint is migrated migration_percentage = self._migrated_endpoints.get(path, 0)
# Random routing based on percentage import random route_to_new = random.random() * 100 < migration_percentage
client = self._new_client if route_to_new else self._legacy_client
# Forward request response = await client.request( method=request.method, url=path, headers=dict(request.headers), content=await request.body() )
return response
@app.api_route("/{full_path:path}", methods=["GET", "POST", "PUT", "DELETE"])async def proxy(request: Request): router = StranglerRouter() return await router.route_request(request)@RestControllerpublic class StranglerRouter {
private final RestTemplate legacyClient; private final RestTemplate newClient; private final Map<String, Integer> migratedEndpoints;
public StranglerRouter() { this.legacyClient = new RestTemplate(); this.legacyClient.setUriTemplateHandler( new DefaultUriBuilderFactory("http://legacy-monolith:8080") );
this.newClient = new RestTemplate(); this.newClient.setUriTemplateHandler( new DefaultUriBuilderFactory("http://notification-service:8080") );
// Feature flags control routing this.migratedEndpoints = Map.of( "/api/notifications", 100, // 100% to new service "/api/orders", 50, // 50% to new service "/api/users", 0 // 0% to new service (still legacy) ); }
@RequestMapping("/**") public ResponseEntity<String> proxy( HttpServletRequest request, @RequestBody(required = false) String body ) { String path = request.getRequestURI();
// Check if endpoint is migrated int migrationPercentage = migratedEndpoints.getOrDefault(path, 0);
// Random routing based on percentage boolean routeToNew = Math.random() * 100 < migrationPercentage;
RestTemplate client = routeToNew ? newClient : legacyClient;
// Forward request HttpEntity<String> entity = new HttpEntity<>(body); return client.exchange( path, HttpMethod.valueOf(request.getMethod()), entity, String.class ); }}// BEFORE: Direct implementationclass OrderService { async createOrder(userId: string, items: Item[]): Promise<Order> { // Direct database access const user = await db.query(User).where({ id: userId }).first();
// Create order const order = new Order(userId, items); await db.add(order);
// Send notification (tightly coupled!) await emailSender.send(user.email, "Order created", ...); await smsSender.send(user.phone, "Order created");
return order; }}
// AFTER: Interface abstractioninterface NotificationService { notifyOrderCreated(order: Order): Promise<void>;}
class LocalNotificationService implements NotificationService { async notifyOrderCreated(order: Order): Promise<void> { await emailSender.send(...); await smsSender.send(...); }}
class RemoteNotificationService implements NotificationService { async notifyOrderCreated(order: Order): Promise<void> { // Call microservice await fetch("http://notification-service/notify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ order_id: order.id, type: "order_created" }) }); }}
class OrderService { constructor(private notificationService: NotificationService) {}
async createOrder(userId: string, items: Item[]): Promise<Order> { const user = await db.query(User).where({ id: userId }).first(); const order = new Order(userId, items); await db.add(order);
// Use abstraction (doesn't know if local or remote!) await this.notificationService.notifyOrderCreated(order);
return order; }}// BEFORE: Direct implementationclass OrderService {public: Order createOrder(const std::string& userId, const std::vector<Item>& items) { // Direct database access User user = db->query<User>().where("id", userId).first();
// Create order Order order(userId, items); db->add(order);
// Send notification (tightly coupled!) emailSender->send(user.getEmail(), "Order created", ...); smsSender->send(user.getPhone(), "Order created");
return order; }};
// AFTER: Interface abstractionclass NotificationService {public: virtual ~NotificationService() = default; virtual void notifyOrderCreated(const Order& order) = 0;};
class LocalNotificationService : public NotificationService {public: void notifyOrderCreated(const Order& order) override { emailSender->send(...); smsSender->send(...); }};
class RemoteNotificationService : public NotificationService {private: HttpClient* httpClient;
public: void notifyOrderCreated(const Order& order) override { // Call microservice HttpRequest request("http://notification-service/notify"); request.setMethod("POST"); request.setBody(Json::object{ {"order_id", order.getId()}, {"type", "order_created"} }); httpClient->send(request); }};
class OrderService {private: NotificationService* notificationService;
public: OrderService(NotificationService* notificationService) : notificationService(notificationService) {}
Order createOrder(const std::string& userId, const std::vector<Item>& items) { User user = db->query<User>().where("id", userId).first(); Order order(userId, items); db->add(order);
// Use abstraction (doesn't know if local or remote!) notificationService->notifyOrderCreated(order);
return order; }};// BEFORE: Direct implementationpublic class OrderService { public async Task<Order> CreateOrderAsync(string userId, List<Item> items) { // Direct database access User user = await db.Query<User>().Where(u => u.Id == userId).FirstAsync();
// Create order Order order = new Order(userId, items); await db.AddAsync(order);
// Send notification (tightly coupled!) await emailSender.SendAsync(user.Email, "Order created", ...); await smsSender.SendAsync(user.Phone, "Order created");
return order; }}
// AFTER: Interface abstractionpublic interface INotificationService { Task NotifyOrderCreatedAsync(Order order);}
public class LocalNotificationService : INotificationService { public async Task NotifyOrderCreatedAsync(Order order) { await emailSender.SendAsync(...); await smsSender.SendAsync(...); }}
public class RemoteNotificationService : INotificationService { private readonly HttpClient httpClient;
public async Task NotifyOrderCreatedAsync(Order order) { // Call microservice await httpClient.PostAsync( "http://notification-service/notify", new StringContent(JsonSerializer.Serialize(new { order_id = order.Id, type = "order_created" })) ); }}
public class OrderService { private readonly INotificationService notificationService;
public OrderService(INotificationService notificationService) { this.notificationService = notificationService; }
public async Task<Order> CreateOrderAsync(string userId, List<Item> items) { User user = await db.Query<User>().Where(u => u.Id == userId).FirstAsync(); Order order = new Order(userId, items); await db.AddAsync(order);
// Use abstraction (doesn't know if local or remote!) await notificationService.NotifyOrderCreatedAsync(order);
return order; }}Build the new service with its own database.
from fastapi import FastAPIfrom sqlalchemy import create_enginefrom sqlalchemy.orm import Session
app = FastAPI()
# Own database!engine = create_engine("postgresql://localhost/notification_db")
@app.post("/notify")async def notify(request: NotifyRequest): """New microservice implementation""" with Session(engine) as session: # Store notification in OWN database notification = Notification( order_id=request.order_id, type=request.type, status="pending" ) session.add(notification) session.commit()
# Send via external services await email_service.send(notification) await sms_service.send(notification)
notification.status = "sent" session.commit()
return {"status": "sent"}
@app.get("/notifications/{order_id}")async def get_notifications(order_id: str): """Query OWN database""" with Session(engine) as session: notifications = session.query(Notification)\ .filter_by(order_id=order_id)\ .all() return notifications@RestController@RequestMapping("/")public class NotificationController {
// Own database! @Autowired private NotificationRepository notificationRepo;
@Autowired private EmailService emailService;
@Autowired private SmsService smsService;
@PostMapping("/notify") public NotifyResponse notify(@RequestBody NotifyRequest request) { // Store notification in OWN database Notification notification = new Notification( request.getOrderId(), request.getType(), "pending" ); notificationRepo.save(notification);
// Send via external services emailService.send(notification); smsService.send(notification);
notification.setStatus("sent"); notificationRepo.save(notification);
return new NotifyResponse("sent"); }
@GetMapping("/notifications/{orderId}") public List<Notification> getNotifications(@PathVariable String orderId) { // Query OWN database return notificationRepo.findByOrderId(orderId); }}// BEFORE: Direct implementationclass OrderService { async createOrder(userId: string, items: Item[]): Promise<Order> { // Direct database access const user = await db.query(User).where({ id: userId }).first();
// Create order const order = new Order(userId, items); await db.add(order);
// Send notification (tightly coupled!) await emailSender.send(user.email, "Order created", ...); await smsSender.send(user.phone, "Order created");
return order; }}
// AFTER: Interface abstractioninterface NotificationService { notifyOrderCreated(order: Order): Promise<void>;}
class LocalNotificationService implements NotificationService { async notifyOrderCreated(order: Order): Promise<void> { await emailSender.send(...); await smsSender.send(...); }}
class RemoteNotificationService implements NotificationService { async notifyOrderCreated(order: Order): Promise<void> { // Call microservice await fetch("http://notification-service/notify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ order_id: order.id, type: "order_created" }) }); }}
class OrderService { constructor(private notificationService: NotificationService) {}
async createOrder(userId: string, items: Item[]): Promise<Order> { const user = await db.query(User).where({ id: userId }).first(); const order = new Order(userId, items); await db.add(order);
// Use abstraction (doesn't know if local or remote!) await this.notificationService.notifyOrderCreated(order);
return order; }}// BEFORE: Direct implementationclass OrderService {public: Order createOrder(const std::string& userId, const std::vector<Item>& items) { // Direct database access User user = db->query<User>().where("id", userId).first();
// Create order Order order(userId, items); db->add(order);
// Send notification (tightly coupled!) emailSender->send(user.getEmail(), "Order created", ...); smsSender->send(user.getPhone(), "Order created");
return order; }};
// AFTER: Interface abstractionclass NotificationService {public: virtual ~NotificationService() = default; virtual void notifyOrderCreated(const Order& order) = 0;};
class LocalNotificationService : public NotificationService {public: void notifyOrderCreated(const Order& order) override { emailSender->send(...); smsSender->send(...); }};
class RemoteNotificationService : public NotificationService {private: HttpClient* httpClient;
public: void notifyOrderCreated(const Order& order) override { // Call microservice HttpRequest request("http://notification-service/notify"); request.setMethod("POST"); request.setBody(Json::object{ {"order_id", order.getId()}, {"type", "order_created"} }); httpClient->send(request); }};
class OrderService {private: NotificationService* notificationService;
public: OrderService(NotificationService* notificationService) : notificationService(notificationService) {}
Order createOrder(const std::string& userId, const std::vector<Item>& items) { User user = db->query<User>().where("id", userId).first(); Order order(userId, items); db->add(order);
// Use abstraction (doesn't know if local or remote!) notificationService->notifyOrderCreated(order);
return order; }};// BEFORE: Direct implementationpublic class OrderService { public async Task<Order> CreateOrderAsync(string userId, List<Item> items) { // Direct database access User user = await db.Query<User>().Where(u => u.Id == userId).FirstAsync();
// Create order Order order = new Order(userId, items); await db.AddAsync(order);
// Send notification (tightly coupled!) await emailSender.SendAsync(user.Email, "Order created", ...); await smsSender.SendAsync(user.Phone, "Order created");
return order; }}
// AFTER: Interface abstractionpublic interface INotificationService { Task NotifyOrderCreatedAsync(Order order);}
public class LocalNotificationService : INotificationService { public async Task NotifyOrderCreatedAsync(Order order) { await emailSender.SendAsync(...); await smsSender.SendAsync(...); }}
public class RemoteNotificationService : INotificationService { private readonly HttpClient httpClient;
public async Task NotifyOrderCreatedAsync(Order order) { // Call microservice await httpClient.PostAsync( "http://notification-service/notify", new StringContent(JsonSerializer.Serialize(new { order_id = order.Id, type = "order_created" })) ); }}
public class OrderService { private readonly INotificationService notificationService;
public OrderService(INotificationService notificationService) { this.notificationService = notificationService; }
public async Task<Order> CreateOrderAsync(string userId, List<Item> items) { User user = await db.Query<User>().Where(u => u.Id == userId).FirstAsync(); Order order = new Order(userId, items); await db.AddAsync(order);
// Use abstraction (doesn't know if local or remote!) await notificationService.NotifyOrderCreatedAsync(order);
return order; }}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.