Incrementally replace legacy systems without big-bang migrations. Covers the strangler fig approach, facade routing, feature toggles for migration, data synchronization during transition, and the patterns that let you modernize production systems safely.
The strangler fig pattern is a migration strategy for replacing legacy systems by gradually wrapping new components around existing ones, allowing both to coexist during transition. This approach matters when migrating monolithic applications or legacy services to modern architectures without requiring complete system downtime or risk.
The core of the strangler fig is a routing layer that intercepts requests to the legacy system and routes them appropriately. In a typical implementation, you create an API gateway or reverse proxy that handles all incoming traffic and makes decisions about where to route requests.
# Example HTTP proxy configuration using nginx
server {
listen 80;
server_name example.com;
location /api/v1/ {
# Route to new service for specific endpoints
if ($request_uri ~ "^/api/v1/users/") {
proxy_pass http://new-users-service:8080/;
}
# Route to legacy system for other endpoints
proxy_pass http://legacy-backend:8080/;
}
}
The routing logic should be based on path patterns, headers, or query parameters. A common pattern is to route specific resource types through the new system while keeping others in the legacy system.
In containerized environments, you can use service discovery mechanisms to determine which services are available for routing:
# Example using kubectl to examine service endpoints
kubectl get endpoints -n production
NAME ENDPOINTS AGE
legacy-backend 10.244.0.5:8080 1d
new-users-service 10.244.0.6:8080 1d
# Example service definition that enables routing
apiVersion: v1
kind: Service
metadata:
name: api-gateway
spec:
selector:
app: api-gateway
ports:
- port: 80
targetPort: 8080
The key is to avoid hardcoding service addresses in the routing logic, using instead dynamic discovery mechanisms that update automatically when services scale or change.
Implement a migration strategy that moves traffic gradually from legacy to new systems. This often involves:
# Example of feature flag implementation in code
if (feature_flag_enabled('users_migration')) {
// Route to new service
return forward_to_new_service('/api/v1/users');
} else {
// Route to legacy system
return forward_to_legacy_service('/api/v1/users');
}
When the new system fails, requests must fall back gracefully to the legacy system:
# Example error handling in proxy configuration
location /api/v1/users/ {
proxy_pass http://new-users-service:8080/;
# If new service is unavailable, fallback to legacy
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_next_upstream_tries 3;
}
The system should log these failures and potentially trigger alerts for operations teams. The fallback behavior must be consistent across all client applications.
New systems often need access to legacy data, which requires careful consideration of read patterns:
# Example database connection configuration
DATABASE_URL="postgresql://user:password@legacy-db:5432/datastore"
# Use read-only connections for new services accessing legacy data
DATABASE_READONLY_URL="postgresql://readonly_user:password@legacy-db:5432/datastore"
The new system should treat legacy data as a source of truth for reads, not attempt to write back to it directly.
When writing data, the system must handle scenarios where both systems need to be updated:
# Example write operation with dual persistence
function createUser(userData) {
// Write to new system
const newRecord = await newService.createUser(userData);
// If legacy system is still needed for writes, write there too
if (legacyWriteEnabled) {
await legacySystem.createUser(userData);
}
return newRecord;
}
Data consistency between systems often requires implementing eventual consistency patterns rather than immediate consistency. The implementation must be explicit about when data is considered "synchronized."
Configuration changes to routing rules should be version-controlled and deployed through standard CI/CD pipelines:
# Example configuration file for routing rules
{
"routes": [
{
"path": "/api/v1/users/*",
"target": "new-users-service"
},
{
"path": "/api/v1/orders/*",
"target": "legacy-backend"
}
]
}
Changes to routing should be validated before deployment and rolled back automatically if they cause failures.
Feature flags controlling which routes are active should be configurable without code changes:
# Example configuration for feature flag
{
"features": {
"users_migration": true,
"orders_migration": false
}
}
Both legacy and new systems must be monitored during migration, with specific metrics for:
The monitoring should track which paths are being taken and how often.
A rollback plan requires that the system can quickly revert to using only the legacy system:
# Example rollback configuration
{
"migration_state": "rollback",
"routes": [
{
"path": "/api/v1/*",
"target": "legacy-backend"
}
]
}
The migration should be integrated into standard deployment practices, with clear steps for:
When a new system fails to write data correctly, requests may succeed but without persistence. This often manifests as successful HTTP responses while data is only stored in memory or temporary storage.
The circuit breaker pattern can mask failures when it's configured to allow traffic to flow through even when the downstream service is failing. A misconfigured circuit breaker might allow requests to pass to a new system that is down and never fallback to the legacy system.
# Example of incorrect circuit breaker configuration
# This setup allows traffic even when service is down
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_next_upstream_tries 1;
When the new system returns different response formats than the legacy system, client applications may fail to parse responses correctly. This often happens with status codes or error message structures.
The proxy or gateway that implements routing can accumulate memory over time if it doesn't properly clean up connections or caches:
# Example of memory leak in proxy configuration
location /api/v1/ {
# Missing connection cleanup
proxy_pass http://new-service;
# Should include proper connection handling
proxy_set_header Connection "";
}
When both systems access the same data source, race conditions can occur:
# Example race condition scenario
function updateUser(userId, userData) {
// Legacy system updates first
legacySystem.updateUser(userId, userData);
// New system may read stale data
const currentData = newService.getUser(userId);
// Updates may conflict
newService.updateUser(userId, userData);
}
Configuration files that define routing rules can drift from the actual deployed services, causing requests to be routed incorrectly:
# Example configuration drift issue
{
"routes": [
{
"path": "/api/v1/users/*",
"target": "non-existent-service" # Service no longer exists
}
]
}
Start by allowing the new system to read from the legacy system without writing back:
# Example read-only access pattern
function getUser(userId) {
// Read from legacy system
const userData = await legacySystem.getUser(userId);
// Process in new system
return processUserData(userData);
}
Once the new system can handle writes, implement bidirectional data flow:
# Example read-write access pattern
function updateUser(userId, userData) {
// Write to both systems
const result = await Promise.all([
legacySystem.updateUser(userId, userData),
newSystem.updateUser(userId, userData)
]);
return result[1]; // Return result from new system
}
Gradually reduce reliance on the legacy system until it's fully replaced:
# Example complete migration
function processRequest(request) {
// All requests go to new system
return newService.handleRequest(request);
}
The pattern works best when the new system is built to interface with the legacy system, not as a completely independent replacement.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy