| Decomposition Pattern | When to Use | Risk Level | Effort | Key Requirement |
|---|---|---|---|---|
| Strangler Fig | Incremental migration of any monolith | Low | Medium | API gateway or proxy to route traffic |
| Branch by Abstraction | Extract internal modules without cutting API | Low | Medium | Abstraction layer in monolith codebase |
| Parallel Run | Validate new service produces identical results | Low | High | Dual-write + comparison pipeline |
| Decompose by Business Capability | Clear business functions map to services | Medium | Medium | Business domain expertise |
| Decompose by Subdomain (DDD) | Complex domain with many bounded contexts | Medium | High | Domain modeling workshops |
| Database-per-Service | Eliminate shared DB coupling | High | Very High | Data migration strategy + eventual consistency |
| Change Data Capture (CDC) | Sync data during transition without dual-write | Medium | Medium | CDC tool (Debezium, DynamoDB Streams) |
| Anti-Corruption Layer | Isolate new service from legacy data model | Low | Medium | Translation layer between old/new models |
| Modular Monolith (intermediate) | Not ready for network boundaries yet | Very Low | Low | Clean module boundaries within monolith |
| Saga Pattern | Distributed transactions across services | High | High | Event choreography or orchestrator |
START
├── Do you have clear, measurable reasons to move to microservices?
│ ├── NO → Stop. A modular monolith may be sufficient. [src2, src8]
│ └── YES ↓
├── How many developers on the codebase?
│ ├── <10 → Strongly consider modular monolith instead. [src8]
│ └── ≥10 ↓
├── Is this a legacy system with unclear domain boundaries?
│ ├── YES → Run domain modeling / Event Storming workshops first [src7]
│ └── NO ↓
├── Can you place a proxy/gateway in front of the monolith?
│ ├── YES → Use Strangler Fig pattern (recommended default) [src1, src3]
│ └── NO → Use Branch by Abstraction inside the codebase [src2]
├── Does the monolith use a single shared database?
│ ├── YES → Plan database decomposition (hardest part) — start with schema separation [src2, src6]
│ └── NO ↓
├── Are there cross-cutting concerns (auth, logging, config)?
│ ├── YES → Extract these as shared platform services first [src5]
│ └── NO ↓
├── How many concurrent users?
│ ├── <1K → Start with 2-3 services, expand as needed
│ ├── 1K-100K → Containerize with Docker + Kubernetes [src5]
│ └── >100K → Full service mesh + event-driven architecture [src5]
└── DEFAULT → Extract one bounded context at a time, validate, repeat
Articulate exactly why you need microservices: independent deployability, team autonomy, targeted scaling, or polyglot tech stacks. If you can't name a specific pain point the monolith causes, you probably don't need microservices yet. As of 2025-2026, cost (not scalability) has become the primary architectural constraint — microservices infrastructure costs 3.75x-6x more than equivalent monoliths. [src2, src8]
# Questions to answer before starting:
1. What specific scaling bottleneck does the monolith have?
2. Which teams block each other during deployment?
3. What is the expected deployment frequency per service?
4. What is the acceptable latency budget for inter-service calls?
5. Do you have the operational maturity for distributed systems?
6. What is the infrastructure budget delta (monolith vs microservices)?
Verify: Document answers. If most answers are vague, invest in a modular monolith first.
Use Event Storming (or similar domain modeling technique) to identify bounded contexts — areas of the domain with distinct language, rules, and data ownership. Each bounded context is a candidate microservice. AI-assisted tools like vFunction can now automate much of this analysis. [src7, src2]
# Event Storming output → bounded context candidates
Domain Events:
[Order Placed] → Order context
[Payment Processed] → Payment context
[Item Shipped] → Shipping context
[Inventory Reserved] → Inventory context
Bounded Contexts identified:
1. Orders (owns: order data, order lifecycle)
2. Payments (owns: payment data, refunds)
3. Shipping (owns: shipment tracking, carrier integration)
4. Inventory (owns: stock levels, reservations)
5. Catalog (owns: product data, pricing)
Verify: Each context has a clear owner (team), distinct data, and minimal cross-context joins in the current database.
Before extracting any service across a network boundary, refactor the monolith into clean modules with internal APIs. This validates your bounded context boundaries cheaply. This intermediate step is now widely recommended as of 2025. [src2, src8]
# Enforce module boundaries in a Python monolith
# BEFORE: Tangled imports across domains
from orders.models import Order
from payments.models import Payment # direct cross-domain import
# AFTER: Each module exposes a public API facade
# payments/api.py (public interface)
def get_payment_status(order_id: str) -> str:
payment = Payment.objects.get(order_id=order_id)
return payment.status
# orders/services.py (uses public API, not internal models)
from payments.api import get_payment_status
status = get_payment_status(order.id)
Verify: No module imports another module's internal models directly. All cross-module communication goes through public API facades.
Place an API gateway or reverse proxy in front of the monolith. Initially, all traffic routes to the monolith. As you extract services, update routing rules to direct specific paths to the new service. Modern implementations often use Istio or Envoy for traffic splitting with percentage-based rollout. [src1, src3]
# Example: nginx routing configuration for Strangler Fig
upstream monolith {
server monolith-app:8080;
}
upstream orders_service {
server orders-service:8081;
}
server {
listen 80;
# New service handles /api/orders/*
location /api/orders/ {
proxy_pass http://orders_service;
}
# Everything else still goes to the monolith
location / {
proxy_pass http://monolith;
}
}
Verify: curl http://gateway/api/orders/123 returns response from new service; curl http://gateway/api/products still hits monolith.
Pick the context with the lowest coupling to the rest of the monolith. Extract it into a standalone service with its own repository, CI/CD pipeline, and data store. [src2, src6]
# Example: Extracting a Notifications service from a Django monolith
# BEFORE: Notification logic embedded in monolith
def place_order(request):
order = Order.objects.create(...)
send_email(order.user.email, "Order confirmed", ...)
send_sms(order.user.phone, "Order confirmed")
return JsonResponse({"order_id": order.id})
# AFTER: Monolith publishes event, Notification service consumes it
def place_order(request):
order = Order.objects.create(...)
publish_event("order.placed", {
"order_id": order.id,
"user_email": order.user.email,
"user_phone": order.user.phone
})
return JsonResponse({"order_id": order.id})
Verify: Place an order via the monolith → event appears in message broker → notification service sends email/SMS.
This is typically the hardest step. Start by separating schemas, then migrate to physically separate databases. Use Change Data Capture (CDC) or dual-write during transition. With 83% of data migrations failing or exceeding budgets, plan conservatively. [src2, src6]
-- Step 1: Schema separation within the same database
CREATE SCHEMA orders_service;
ALTER TABLE orders SET SCHEMA orders_service;
ALTER TABLE order_items SET SCHEMA orders_service;
-- Step 2: Replace cross-schema joins with API calls
-- BEFORE: SELECT o.*, p.name FROM orders o JOIN products p ON o.product_id = p.id;
-- AFTER: Orders service calls Catalog API: GET /api/catalog/products/123
# Step 3: Use Debezium CDC to sync data during transition
docker run -d --name debezium \
-e BOOTSTRAP_SERVERS=kafka:9092 \
-e GROUP_ID=1 \
-e CONFIG_STORAGE_TOPIC=debezium-configs \
debezium/connect:2.5
# Register a connector for the orders table
curl -X POST http://localhost:8083/connectors -H "Content-Type: application/json" -d '{
"name": "orders-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "monolith-db",
"database.dbname": "app",
"table.include.list": "public.orders,public.order_items",
"topic.prefix": "orders"
}
}'
Verify: Changes to the orders table in the monolith DB appear as events in the orders.public.orders Kafka topic within seconds.
Before extracting more services, ensure you have centralized logging, distributed tracing, and health monitoring. OpenTelemetry has become the industry standard for instrumentation as of 2025. [src5, src6]
# docker-compose.yml — minimal observability stack
services:
jaeger:
image: jaegertracing/all-in-one:1.54
ports:
- "16686:16686" # UI
- "4318:4318" # OTLP HTTP
prometheus:
image: prom/prometheus:v2.50.0
ports:
- "9090:9090"
grafana:
image: grafana/grafana:10.3.0
ports:
- "3000:3000"
Verify: Service traces visible in Jaeger. Metrics scraped by Prometheus. Dashboards rendering in Grafana.
Repeat steps 5-6 for each bounded context, prioritizing by coupling pain and business value. Expect 6-18 months for a medium monolith (100K-500K LOC) to fully decompose. The 2025-2026 recommendation: modular monolith core plus 2-5 extracted services for hot paths. [src2, src4, src8]
Verify: Each extraction reduces the monolith's LOC by 10-20%. Deployment frequency per service should increase measurably.
# Input: A monolith where /api/orders is being extracted to a new service
# Output: A FastAPI gateway that routes between monolith and new services
from fastapi import FastAPI, Request, Response
import httpx
app = FastAPI()
ROUTES = {
"/api/orders": "http://orders-service:8081",
"/api/payments": "http://payments-service:8082",
}
MONOLITH_URL = "http://monolith:8080"
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def proxy(request: Request, path: str):
target_url = MONOLITH_URL
for prefix, service_url in ROUTES.items():
if f"/{path}".startswith(prefix):
target_url = service_url
break
async with httpx.AsyncClient(timeout=30.0) as client:
url = f"{target_url}/{path}"
response = await client.request(
method=request.method,
url=url,
headers={k: v for k, v in request.headers.items() if k.lower() != "host"},
content=await request.body(),
params=request.query_params,
)
return Response(
content=response.content,
status_code=response.status_code,
headers=dict(response.headers),
)
// Input: A bounded context extracted as a NestJS microservice
// Output: Service that consumes domain events from the monolith via RabbitMQ
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { Module, Controller } from '@nestjs/common';
import { EventPattern, Payload } from '@nestjs/microservices';
@Controller()
class OrderEventController {
@EventPattern('order.placed')
async handleOrderPlaced(@Payload() data: {
orderId: string;
userId: string;
items: Array<{ productId: string; quantity: number }>;
}) {
for (const item of data.items) {
await this.reserveStock(item.productId, item.quantity);
}
}
@EventPattern('order.cancelled')
async handleOrderCancelled(@Payload() data: { orderId: string }) {
await this.releaseReservation(data.orderId);
}
private async reserveStock(productId: string, qty: number): Promise<void> {
// Update inventory in this service's own database
}
private async releaseReservation(orderId: string): Promise<void> {
// Release previously reserved stock
}
}
@Module({ controllers: [OrderEventController] })
class InventoryModule {}
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
InventoryModule,
{
transport: Transport.RMQ,
options: {
urls: [process.env.RABBITMQ_URL || 'amqp://localhost:5672'],
queue: 'inventory_queue',
queueOptions: { durable: true },
},
},
);
await app.listen();
}
bootstrap();
// Input: Legacy monolith uses a different data model than the new service
// Output: ACL translates between legacy and new domain models
package acl
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
type LegacyOrder struct {
OrderNum string `json:"order_num"`
CustID int `json:"cust_id"`
TotalCents int64 `json:"total_cents"`
StatusCode int `json:"status_code"` // 0=pending, 1=confirmed, 2=shipped
CreatedTS int64 `json:"created_ts"`
}
type Order struct {
ID string `json:"id"`
CustomerID string `json:"customer_id"`
TotalAmount float64 `json:"total_amount"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
type AntiCorruptionLayer struct {
monolithURL string
client *http.Client
}
func NewACL(monolithURL string) *AntiCorruptionLayer {
return &AntiCorruptionLayer{
monolithURL: monolithURL,
client: &http.Client{Timeout: 10 * time.Second},
}
}
func (acl *AntiCorruptionLayer) GetOrder(ctx context.Context, orderID string) (*Order, error) {
url := fmt.Sprintf("%s/legacy/orders/%s", acl.monolithURL, orderID)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
resp, err := acl.client.Do(req)
if err != nil {
return nil, fmt.Errorf("fetching from monolith: %w", err)
}
defer resp.Body.Close()
var legacy LegacyOrder
if err := json.NewDecoder(resp.Body).Decode(&legacy); err != nil {
return nil, fmt.Errorf("decoding legacy response: %w", err)
}
return acl.translate(legacy), nil
}
func (acl *AntiCorruptionLayer) translate(legacy LegacyOrder) *Order {
statusMap := map[int]string{0: "pending", 1: "confirmed", 2: "shipped"}
return &Order{
ID: legacy.OrderNum,
CustomerID: fmt.Sprintf("cust-%d", legacy.CustID),
TotalAmount: float64(legacy.TotalCents) / 100.0,
Status: statusMap[legacy.StatusCode],
CreatedAt: time.Unix(legacy.CreatedTS, 0),
}
}
// BAD — Rewriting everything from scratch before deploying anything
// "We'll freeze features for 6 months while we rebuild in microservices"
// Result: project runs over schedule, new system has different bugs,
// migration day is chaos.
// GOOD — Extract one service at a time, deploy to production, validate, repeat
// Week 1-4: Extract Notifications service → deploy → monitor
// Week 5-8: Extract Auth service → deploy → monitor
// Week 9-12: Extract Payments service → deploy → monitor
// Monolith shrinks gradually. Zero "migration day." [src1, src2]
-- BAD — Multiple services reading/writing the same tables
-- orders-service: SELECT * FROM orders WHERE user_id = 123;
-- payments-service: UPDATE orders SET payment_status = 'paid' WHERE id = 456;
-- inventory-service: SELECT * FROM orders o JOIN inventory i ON ...;
-- All services coupled through shared schema.
# GOOD — Each service owns its data; communicates via APIs or events
async def process_payment(payment_data):
result = await charge_card(payment_data)
if result.success:
await publish_event("payment.completed", {
"order_id": payment_data["order_id"],
"amount": result.amount,
"transaction_id": result.transaction_id
})
return result # [src2, src6]
// BAD — Splitting by technical concern instead of business domain
// "frontend-service" — all UI code
// "api-service" — all API endpoints
// "database-service" — all database operations
// Still tightly coupled — changing one feature touches all three.
// GOOD — Each service owns a full vertical slice of functionality
// "orders-service" — Orders API + Orders DB + Order business logic
// "payments-service" — Payments API + Payments DB + Payment logic
// "catalog-service" — Catalog API + Catalog DB + Product logic
// A change to order logic only touches orders-service. [src2, src7]
// BAD — One service per CRUD operation
// "create-order-service"
// "get-order-service"
// "update-order-status-service"
// "delete-order-service"
// Operational overhead exceeds benefit.
// GOOD — Service encompasses a complete bounded context
// "orders-service" handles: create, read, update, delete, status
// transitions, order history, and order-related events.
// One repo, one CI/CD pipeline, one team owns it. [src2, src4]
// BAD — 5-person startup adopting microservices "because Netflix does it"
// Infrastructure cost: $40K-65K/month vs $15K/month for equivalent monolith
// Result: 70% of engineering time spent on infrastructure, not features.
// Amazon Prime Video team moved BACK to monolith, cut costs by 90%. [src8]
// GOOD — Build a modular monolith first, extract hot paths as needed
// Phase 1: Modular monolith with clean internal boundaries
// Phase 2: Extract 2-5 services for independently-scaling hot paths
// Phase 3: Continue extracting only when business value justifies overhead
// The 2025-2026 industry consensus for teams with 10-100 engineers. [src8]
# Analyze coupling in a monolith codebase (Java)
jdeps --summary --multi-release 21 target/*.jar
# Analyze Python import dependencies
pydeps myapp --max-bacon 2 --cluster
# Count cross-module database joins
grep -rn 'JOIN' --include='*.py' --include='*.java' --include='*.ts' | wc -l
# Check for shared database tables across services
psql -c "SELECT schemaname, tablename, tableowner FROM pg_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema');"
# Verify service independence — can each service start alone?
docker compose up orders-service --no-deps
curl http://localhost:8081/health
# Measure inter-service latency
curl -w "time_total: %{time_total}s\n" -o /dev/null -s http://orders-service/api/orders/123
# Verify database isolation — no cross-service schema access
psql -c "SELECT grantee, table_schema, table_name, privilege_type FROM information_schema.table_privileges WHERE grantee NOT IN ('postgres', 'PUBLIC') ORDER BY grantee, table_schema;"
| Approach | Status | Key Development | Notes |
|---|---|---|---|
| Strangler Fig (Fowler, 2004) | Industry standard | Pattern matured with cloud-native adoption | Still the recommended starting point |
| DDD Bounded Contexts (Evans, 2003) | Industry standard | Refined by Newman (2019) for microservices | Required for correct service boundaries |
| Saga Pattern (Garcia-Molina, 1987) | Widely adopted | Orchestration vs. choreography variants | Use for distributed transactions |
| Modular Monolith (2020s trend) | Growing rapidly (2025) | Recommended intermediate step by 42% of orgs | Consider before microservices |
| Event-Driven Decomposition | Current best practice | Kafka/RabbitMQ as decoupling mechanism | Enables async communication |
| AI-Assisted Migration (2025+) | Emerging | vFunction, automated dependency analysis | Saves hours of manual assessment |
| Hybrid Architecture (2025-2026) | Industry consensus | Modular monolith core + extracted hot paths | Best of both worlds for mid-size teams |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Teams block each other during deployments | Small team (<10 developers) | Modular monolith |
| Specific components need independent scaling | Simple CRUD application | Well-structured monolith |
| Different components need different tech stacks | Unclear domain boundaries | DDD modeling + modular monolith first |
| Regulatory compliance requires isolation (PCI, HIPAA) | Startup still finding product-market fit | Monolith (iterate faster) |
| >100 developers working on same codebase | Budget doesn't cover operational overhead (3.75x-6x) | Vertical slice architecture |
| Deployment frequency needs vary per component | All components have similar load patterns | Monolith with horizontal scaling |