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:
- Teams step on each other deploying
- Different parts need different scaling
- Technology flexibility is genuinely needed
- Organization is large enough for team ownership
Microservices don’t make sense when:
- Team is small (<20 engineers)
- Application is simple
- You want microservices for résumé reasons
- Current problems are solvable with better monolith practices
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:
- Have clear boundaries
- Change frequently (benefit from independent deployment)
- Have different scaling needs
- Are owned by a specific team
Avoid starting with:
- Core domain logic (too risky)
- Highly interconnected modules
- Rarely changed components
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:
- Dedicated team
- On-call responsibility
- SLO ownership
- Roadmap control
Platform Team
As services multiply, common concerns emerge:
- Deployment pipeline
- Observability
- Service discovery
- Security
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:
- Percentage of traffic through new services
- Independent deployment frequency
- Team velocity
- Incident rate
Key Takeaways
- Validate that microservices solve actual problems before starting
- Fix monolith practices first: tests, CI/CD, monitoring, clear modules
- Use strangler pattern: extract incrementally while monolith operates
- Start with services that have clear boundaries and change frequently
- Use anti-corruption layers to protect new services from monolith internals
- Plan data migration carefully; shared database is the hardest problem
- Prefer async communication for decoupling
- Match service boundaries to team boundaries
- Avoid big bang rewrites, too many services, and distributed monoliths
Migration to microservices is a multi-year journey. Plan for the long haul.