Microservices Migration: A Practical Approach

July 1, 2019

The promise of microservices—independent deployments, team autonomy, technology flexibility—is compelling. But the migration from monolith to microservices has claimed many victims. Projects that promised 6 months deliver partially after 2 years.

Here’s a practical approach that works.

Before You Start

Validate the Need

Not every application benefits from microservices:

Microservices make sense when:

Microservices don’t make sense when:

Fix the Monolith First

Many “monolith problems” are just bad practices:

Before microservices:
✓ Automated tests
✓ CI/CD pipeline
✓ Deployment in < 15 minutes
✓ Monitoring and alerting
✓ Clear module boundaries

If you can’t run a good monolith, you can’t run good microservices.

The Strangler Pattern

Incremental Extraction

Extract services gradually while the monolith continues operating:

Phase 1: All traffic to monolith
┌─────────────────────────────┐
│        Monolith             │
│  ┌─────┐ ┌─────┐ ┌─────┐   │
│  │Users│ │Orders│ │Pay  │   │
│  └─────┘ └─────┘ └─────┘   │
└─────────────────────────────┘

Phase 2: First service extracted
┌─────────────────────────────┐   ┌──────────┐
│        Monolith             │   │ Payment  │
│  ┌─────┐ ┌─────┐           │   │ Service  │
│  │Users│ │Orders│ ──────────┼──→│          │
│  └─────┘ └─────┘           │   └──────────┘
└─────────────────────────────┘

Phase N: Fully extracted
┌──────────┐  ┌──────────┐  ┌──────────┐
│  Users   │  │  Orders  │  │ Payment  │
│ Service  │  │ Service  │  │ Service  │
└──────────┘  └──────────┘  └──────────┘

Choosing What to Extract

Start with services that:

Avoid starting with:

The Anti-Corruption Layer

Protect new service from monolith internals:

# In new service: translate monolith concepts
class PaymentAdapter:
    def process_payment(self, order_id):
        # Fetch order from monolith
        monolith_order = self.monolith_client.get_order(order_id)

        # Translate to service domain
        payment_request = PaymentRequest(
            amount=monolith_order['total_cents'] / 100,
            currency=monolith_order['currency_code'],
            customer_id=monolith_order['user_uuid']
        )

        return self.payment_processor.process(payment_request)

Shared Database Challenges

The Problem

Monolith services share a database:

Users Table ← Used by: Auth, Orders, Billing, Notifications
         Cannot extract one without affecting others

Strategies

1. Database View as Interface:

-- Create view that new service reads
CREATE VIEW payments_order_view AS
SELECT
    id,
    total_cents,
    currency,
    user_id,
    status
FROM orders
WHERE status IN ('pending_payment', 'paid');

2. Event-Based Sync:

Monolith → OrderCreated event → Payment Service
Payment Service maintains own order cache

3. API Instead of Direct DB:

Before: Payment reads orders table
After: Payment calls orders API

4. Gradual Data Migration:

Phase 1: Payment reads from monolith DB
Phase 2: Payment reads from own DB, synced from monolith
Phase 3: Payment owns its data, monolith reads from Payment
Phase 4: Remove old payment data from monolith

Communication Patterns

Synchronous (HTTP/gRPC)

def create_order(self, order_data):
    order = Order.create(order_data)

    # Sync call to inventory service
    inventory_response = inventory_client.reserve(order.items)
    if not inventory_response.success:
        order.cancel()
        raise InsufficientInventory()

    return order

Simple but creates coupling and cascading failures.

Asynchronous (Events/Messages)

def create_order(self, order_data):
    order = Order.create(order_data)

    # Publish event
    event_bus.publish(OrderCreated(
        order_id=order.id,
        items=order.items,
        user_id=order.user_id
    ))

    return order  # Don't wait for downstream processing

Decoupled but eventually consistent.

Saga Pattern

For distributed transactions:

class OrderSaga:
    def execute(self, order_data):
        # Step 1
        order = order_service.create(order_data)

        try:
            # Step 2
            reservation = inventory_service.reserve(order.items)
        except InventoryError:
            order_service.cancel(order.id)
            raise

        try:
            # Step 3
            payment = payment_service.charge(order.total)
        except PaymentError:
            inventory_service.release(reservation.id)
            order_service.cancel(order.id)
            raise

        return order

Team Structure

Conway’s Law

Your architecture will mirror your organization:

One team → Monolith works fine
Multiple teams → Communication overhead drives service boundaries

Service Ownership

Each service needs clear ownership:

Platform Team

As services multiply, common concerns emerge:

Platform team provides these as services.

Common Mistakes

Big Bang Rewrite

❌ "We'll rewrite everything as microservices, launch in 6 months"
✓ "We'll extract one service, validate, then continue"

Too Many Services Too Fast

❌ 50 services for 20 engineers
✓ One service per team that needs independent deployment

Distributed Monolith

❌ Services that must deploy together
❌ Shared database with coupled schemas
❌ Synchronous calls everywhere
✓ Truly independent services

Ignoring Data

❌ Extract service, leave data in monolith
✓ Plan data ownership and migration

Metrics for Success

Track migration progress:

Key Takeaways

Migration to microservices is a multi-year journey. Plan for the long haul.