Back to Blog
Architecture
DevOps

Migrating a Monolith to Microservices: The Strangler Fig Approach

How to decompose a legacy monolith incrementally using the strangler fig pattern — without a big-bang rewrite that puts your business at risk.

8 min readMay 20, 2026Netvionix Team
Migrating a Monolith to Microservices: The Strangler Fig Approach

Why Big-Bang Rewrites Fail

The most common microservices migration mistake is drawing a new architecture diagram, freezing the monolith, and spending 18 months rebuilding everything from scratch in services. By the time the new system is ready, the business has changed, the monolith has received 200 new features, and the "new" system is already out of date.

The strangler fig pattern is the antidote: migrate incrementally, ship value continuously, and let the new architecture grow around the old one until the monolith can be safely decommissioned.


The Strangler Fig Pattern

Named after the strangler fig tree, which grows around a host tree and eventually replaces it, the pattern works in three phases:

  1. Identify — find a bounded capability in the monolith that can be extracted
  2. Redirect — route traffic for that capability to a new service via a proxy/facade
  3. Remove — once the new service is stable, delete the code from the monolith

The monolith continues running throughout. Users never experience a big cutover.


Step 1: Map Your Monolith's Seams

Before writing a single line of service code, map the monolith's domain model. Look for "seams" — areas of low coupling between business capabilities.

Monolith Capabilities:
┌─────────────────────────────────────────────────────┐
│                    OrderService                      │
│  ┌──────────┐  ┌──────────┐  ┌──────────────────┐   │
│  │ Inventory│  │  Pricing │  │  Notifications   │   │
│  │ (complex)│  │ (coupled)│  │ (isolated!)      │   │
│  └──────────┘  └──────────┘  └──────────────────┘   │
└─────────────────────────────────────────────────────┘

Start with the most isolated capability — high cohesion, low coupling. In this example: Notifications.


Step 2: Build the Facade

Before extracting, introduce a routing layer (API Gateway, Nginx, or application-level facade) that can direct traffic to either the monolith or the new service.

# Facade Router (lives in front of the monolith)
class NotificationRouter:
    def __init__(self, feature_flags: FeatureFlags):
        self.flags = feature_flags
        self.monolith_client = MonolithHTTPClient()
        self.notification_service = NotificationServiceClient()

    async def send_order_confirmation(self, order_id: str, user_id: str):
        if self.flags.is_enabled("new_notification_service", user_id):
            # Route to new service for enabled users
            return await self.notification_service.send_order_confirmation(
                order_id=order_id,
                user_id=user_id
            )
        else:
            # Fall back to monolith
            return await self.monolith_client.post(
                "/internal/notifications/order-confirmation",
                json={"orderId": order_id, "userId": user_id}
            )

Step 3: Run in Shadow Mode First

Before routing real traffic, run the new service in shadow mode: send every request to both systems, use the monolith's response, but log differences.

async def shadow_mode_send(self, payload: dict) -> dict:
    # Use monolith response (source of truth)
    monolith_result = await self.monolith_client.send(payload)

    # Fire-and-forget shadow call
    asyncio.create_task(
        self._shadow_compare(payload, monolith_result)
    )

    return monolith_result

async def _shadow_compare(self, payload: dict, monolith_result: dict):
    try:
        new_result = await self.notification_service.send(payload)
        if new_result != monolith_result:
            logger.warning("Shadow divergence", extra={
                "payload": payload,
                "monolith": monolith_result,
                "new_service": new_result,
            })
    except Exception as e:
        logger.error("Shadow call failed", extra={"error": str(e)})

Run shadow mode for 2–4 weeks. Fix divergences. Only then start routing real traffic.


Step 4: Gradual Traffic Migration

# Feature flag: ramp from 0% to 100% over 2 weeks
NOTIFICATION_SERVICE_ROLLOUT = {
    "enabled": True,
    "rollout_percentage": 25,  # Start at 25%
    "sticky": True,            # Same user always goes to same service
}

Rollout schedule:

  • Week 1: 5% → monitor error rates
  • Week 2: 25% → validate at scale
  • Week 3: 50% → check performance under load
  • Week 4: 100% → full migration

At each stage, error rate > 0.1% above baseline = roll back immediately.


Step 5: Shared Database — The Hard Part

The monolith and new service likely share a database. This is the hardest constraint to untangle.

Strategy:

  1. New service writes to its own schema, reads from monolith schema
  2. Add a synchronization job to keep both in sync
  3. Gradually shift reads to the new schema
  4. Eventually cut the write path
  5. Remove the sync job and the monolith tables

Never let two services write to the same table. This is the rule that prevents split-brain data corruption.


Extraction Priority Matrix

CapabilityCouplingBusiness ValueMigrate?
NotificationsLowMedium✅ First
User AuthMediumHighLater
Pricing EngineHighHighLast
Order ProcessingVery HighCriticalMaybe never

Some parts of the monolith should stay as a monolith. Microservices are not the goal — reliability and velocity are.