How to Decompose a Monolith into Microservices
How do I decompose a monolith into microservices?
TL;DR
- Bottom line: Use the Strangler Fig pattern to incrementally extract bounded contexts from your monolith into independent services — never attempt a big-bang rewrite. In 2025-2026, the industry consensus recommends starting with a modular monolith and extracting selectively only when business needs justify it.
- Key tool/command: Domain-Driven Design (DDD) bounded context mapping → Strangler Fig facade → database-per-service separation
- Watch out for: Creating a "distributed monolith" — splitting code into services that still share a database and must be deployed together defeats the entire purpose. A 2025 CNCF survey found 42% of organizations are consolidating microservices back into larger units due to operational overhead.
- Works with: Any language/framework; commonly implemented with API gateways (Kong, Envoy, AWS ALB), containers (Docker/Kubernetes), service meshes (Istio, Linkerd), and event brokers (Kafka, RabbitMQ).
Constraints
- Never attempt a big-bang rewrite — use Strangler Fig or Branch by Abstraction for incremental extraction. [src1, src2]
- Each microservice must own its own database — shared databases create distributed monoliths that are worse than the original. [src2, src6]
- Do not decompose before mapping bounded contexts with DDD — wrong service boundaries are expensive to fix and require re-merging services. [src7]
- Ensure operational maturity (CI/CD, monitoring, tracing, alerting) before extracting more than 3 services. [src5, src6]
- Budget 40-60% of total migration effort for database decomposition and eventual consistency patterns. [src2]
- Teams smaller than 10 developers should consider a modular monolith instead — microservices infrastructure costs 3.75x-6x more than a monolith for equivalent functionality. [src8]
Quick Reference
| 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 |
Decision Tree
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
Step-by-Step Guide
1. Define business objectives before touching code
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.
2. Map bounded contexts with Event Storming
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.
3. Enforce module boundaries in the monolith first
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.
4. Set up the Strangler Fig facade
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.
5. Extract the first bounded context
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.
6. Decompose the shared database
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.
7. Implement observability before scaling
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.
8. Iterate: extract more bounded contexts
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.
Code Examples
Python/FastAPI: Strangler Fig with API Gateway routing
# 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),
)
TypeScript/NestJS: Event-driven service extraction
// 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();
Go: Anti-Corruption Layer between monolith and new service
// 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),
}
}
Anti-Patterns
Wrong: Big-bang rewrite
// 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.
Correct: Incremental Strangler Fig migration
// 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]
Wrong: Sharing a database across microservices
-- 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.
Correct: Database-per-service with API calls
# 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]
Wrong: Extracting services by technical layer
// 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.
Correct: Extract services by business capability
// 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]
Wrong: Making services too small ("nanoservices")
// BAD — One service per CRUD operation
// "create-order-service"
// "get-order-service"
// "update-order-status-service"
// "delete-order-service"
// Operational overhead exceeds benefit.
Correct: Service boundaries match bounded contexts
// 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]
Wrong: Microservices for a small team (2025-2026 anti-pattern)
// 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]
Correct: Start modular, extract when justified
// 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]
Common Pitfalls
- Creating a distributed monolith: Services share a database, deploy together, and fail together. Fix: enforce database-per-service and independent CI/CD pipelines from day one. [src2]
- Ignoring data consistency challenges: Removing cross-service joins breaks queries that assumed ACID transactions. Fix: accept eventual consistency, implement the Saga pattern, or keep tightly-coupled data in the same service. [src2, src4]
- Decomposing before understanding domain boundaries: Extracting services without domain modeling leads to wrong boundaries that are expensive to fix. Fix: run Event Storming workshops before writing any new service code. [src7]
- Underestimating operational complexity: Each new service needs monitoring, logging, alerting, CI/CD, secret management, and on-call rotation. Fix: build a platform team and shared infrastructure before scaling past 5 services. [src5, src6]
- Migrating for technical purity instead of business value: Teams decompose the "easiest" modules instead of the ones causing the most business pain. Fix: prioritize extraction by business impact. [src2]
- Forgetting the Anti-Corruption Layer: New services directly consume the monolith's data model, inheriting its design flaws. Fix: always put a translation layer between legacy and new domain models. [src3]
- Skipping the modular monolith step: Going straight from a tangled monolith to microservices. Fix: first enforce module boundaries within the monolith, then extract modules that prove stable as services. [src2, src8]
- Underestimating cost: Microservices infrastructure costs 3.75x-6x more than equivalent monoliths ($40K-65K/month vs $15K/month). Fix: model total cost of ownership before committing. [src8]
Diagnostic Commands
# 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;"
Version History & Compatibility
| 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 |
When to Use / When Not to Use
| 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 |
Decision Logic
Use these if/then rules to route agent recommendations to the correct path. Each rule cites the underlying sources.
If team size is under 10 developers and no scaling bottleneck is documented
--> Stay on a modular monolith. Microservices infrastructure costs 3.75x–6x more for equivalent functionality and 42% of orgs that adopted microservices are consolidating back. [src8, src9]
If the monolith uses a single shared database and the goal is service independence
--> Plan database decomposition first (40–60% of total migration effort). Start with schema separation, then add CDC (Debezium) for dual-read during cutover. Do NOT split code while keeping a shared DB — that produces a distributed monolith. [src2, src6]
If domain boundaries are unclear or the codebase is legacy with tangled imports
--> Run Event Storming workshops with domain experts BEFORE extracting any service. Optionally use AI-assisted analysis tools (vFunction, LLM-based dependency clustering) to accelerate runtime-behavior mapping, but treat their suggestions as drafts that need human review. [src7, src6]
If an API gateway or reverse proxy can be placed in front of the monolith
--> Use the Strangler Fig pattern with traffic routing rules. Start by routing one bounded context (typically Notifications or Auth) to a new service while everything else still hits the monolith. [src1, src3]
If a proxy/gateway cannot be inserted at the perimeter
--> Use Branch by Abstraction inside the codebase: introduce an abstraction layer, route internal calls through it, then swap the implementation for an out-of-process service one path at a time. [src2]
If observability (centralized logs, distributed traces, metrics) is not in place
--> Stop. Install an OpenTelemetry + Jaeger/Prometheus/Grafana stack BEFORE extracting more than 3 services. Debugging distributed systems without traces is effectively impossible. [src5, src6]
If the team is decomposing primarily to "modernize" with no measurable pain point
--> Reject the migration. Document the specific scaling, deployment, or team-coordination bottleneck first. Recent industry data shows consolidation back to monoliths delivers 87% AWS cost reduction and 93% latency reduction when the original microservices split was not justified. [src9]
Important Caveats
- Microservices trade code complexity for operational complexity — you need mature CI/CD, monitoring, and incident response before the switch pays off.
- Database decomposition is the single hardest step. Budget 40-60% of total migration effort for data separation and eventual consistency patterns.
- Conway's Law applies: your service boundaries will mirror your team structure. Reorganize teams around bounded contexts, not technical layers.
- Network calls are 100-1000x slower than in-process calls. Every service boundary adds latency. Over-decomposition causes death by a thousand network hops.
- There is no "right number" of microservices. Let business boundaries and team structure dictate the number.
- As of 2025-2026, 42% of organizations that adopted microservices are consolidating back into larger units (CNCF Annual Survey, n=689, ±3.8%). One 2026 case study reported an 87% AWS cost reduction and 93% latency improvement after consolidating back to a monolith. Evaluate whether you truly need microservices before committing. [src8, src9]
- Infrastructure cost is now the primary constraint: microservices cost $40K-65K/month vs $15K/month for equivalent monolith functionality.