How to Avoid Microservices Anti-Patterns: Architecture Decision Guide
Identify and fix the 8 most common microservices mistakes. Covers distributed monoliths, service granularity, data ownership, and when NOT to use microservices.
Microservices are not inherently good architecture. They’re a trade-off — you gain deployment independence at the cost of operational complexity. The anti-patterns below are the most common reasons microservices projects fail, and they all share a root cause: teams adopt the architecture without understanding the constraints that make it work.
Anti-Pattern 1: The Distributed Monolith
Symptom: All services must be deployed together. Changing one service breaks others. Teams can’t ship independently.
Root Cause: Services share databases, deploy in lockstep, or communicate via synchronous chains.
❌ Distributed Monolith
┌─────┐ sync ┌─────┐ sync ┌─────┐
│ Svc │──────────▶│ Svc │──────────▶│ Svc │
│ A │ │ B │ │ C │
└──┬──┘ └──┬──┘ └──┬──┘
│ │ │
└────────┬────────┘ │
▼ ▼
┌────────────┐ ┌────────────┐
│ Shared DB │ │ Shared DB │
└────────────┘ └────────────┘
Fix: Database-per-service, asynchronous communication, API contracts.
✅ Proper Microservices
┌─────┐ event ┌─────┐ event ┌─────┐
│ Svc │──────────▶│ Svc │──────────▶│ Svc │
│ A │ (async) │ B │ (async) │ C │
└──┬──┘ └──┬──┘ └──┬──┘
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│ DB A │ │ DB B │ │ DB C │
└──────┘ └──────┘ └──────┘
How to Diagnose
| Symptom | Distributed Monolith | True Microservices |
|---|---|---|
| Deploy frequency | All services together | Each service independently |
| Shared database | Multiple services write to same tables | Each service owns its data |
| Breaking changes | Service B change breaks Service A | Contract tests catch breaks before deploy |
| Team coupling | Teams coordinate releases | Teams ship independently |
| Failure blast radius | One service down = everything down | One service down = graceful degradation |
Anti-Pattern 2: Wrong Service Granularity
Too Fine-Grained: Every CRUD operation is a service. You have 200 services for a 10-developer team. Deployment overhead exceeds development capacity.
Too Coarse-Grained: “We split our monolith into 3 services.” The services are still 500K lines of code each. Nothing changed except you added a network call.
The Right Granularity
| Team Size | Service Count | Ratio |
|---|---|---|
| 5-10 devs | 3-8 services | 1-2 services per dev |
| 10-25 devs | 8-20 services | ~1 service per dev |
| 25-50 devs | 15-40 services | Team-aligned services |
| 50+ devs | Domain-count services | Bounded context per team |
Granularity Decision Questions
| Question | If Yes → | If No → |
|---|---|---|
| Does this component need independent scaling? | Separate service | Keep together |
| Does a different team own this? | Separate service | Keep together |
| Does this need a different tech stack? | Separate service | Keep together |
| Does this change at a different rate? | Consider separating | Keep together |
| Does this have different SLA requirements? | Separate service | Keep together |
:::tip[Two-Pizza Rule Applied] Each service should be owned by a team that can be fed by two pizzas (5-8 people). If a service requires more than that to maintain, it’s too big. If a developer maintains more than 2 services, they’re too small. :::
Anti-Pattern 3: Synchronous Everything
Symptom: Request chains that create 10 network calls before returning a response. Latency is additive across every hop.
# ❌ Synchronous chain — latency = sum of all calls
async def process_order(order):
customer = await customer_svc.get(order.customer_id) # 50ms
inventory = await inventory_svc.check(order.items) # 80ms
pricing = await pricing_svc.calculate(order) # 40ms
payment = await payment_svc.charge(order, pricing) # 200ms
shipping = await shipping_svc.create(order, customer) # 100ms
notification = await email_svc.send(customer, order) # 150ms
# Total: 620ms minimum
Fix: Use events for non-blocking operations.
# ✅ Event-driven — only synchronous for what you need NOW
async def process_order(order):
customer = await customer_svc.get(order.customer_id) # 50ms
inventory = await inventory_svc.check(order.items) # 80ms
pricing = await pricing_svc.calculate(order) # 40ms
payment = await payment_svc.charge(order, pricing) # 200ms
# Total: 370ms
# Everything else happens asynchronously via events
await event_bus.publish("order.created", {
"order_id": order.id,
"customer": customer,
"items": order.items
})
# Shipping, notifications, analytics — all async consumers
Sync vs Async Decision
| Communication Type | Use When | Avoid When |
|---|---|---|
| Synchronous (HTTP/gRPC) | Response is needed immediately (e.g., payment confirmation) | Response can be deferred |
| Asynchronous (events) | Fire-and-forget, eventual consistency OK | Strong consistency required (rare) |
| Request-reply (async) | Long-running operations with callback | Sub-second response required |
Anti-Pattern 4: No API Contract Management
Symptom: Service B deploys a breaking change. Service A discovers it at 3 AM when production breaks.
Fix: Consumer-Driven Contract Testing.
# Pact — consumer-driven contract test
# Consumer side (Service A)
from pact import Consumer, Provider
pact = Consumer('ServiceA').has_pact_with(Provider('ServiceB'))
pact.given('a customer exists') \
.upon_receiving('a request for customer by ID') \
.with_request('GET', '/api/customers/123') \
.will_respond_with(200, body={
'id': '123',
'name': Like('John Doe'),
'email': Like('john@example.com')
})
API Versioning Strategy
| Strategy | How It Works | When to Use |
|---|---|---|
URL versioning (/v2/customers) | Version in the path | External APIs, clear boundaries |
Header versioning (Accept: v2) | Version in headers | Internal APIs, less URL clutter |
| No versioning (additive only) | Never remove fields, only add | Simple services, fast iteration |
Anti-Pattern 5: Shared Data Ownership
Symptom: Multiple services write to the same database table. Impossible to know who owns the data.
Fix: Single-writer principle — one service owns each data entity.
| Entity | Owner Service | Read Access | Write Access |
|---|---|---|---|
| Customers | Customer Service | All (via API) | Customer Service only |
| Orders | Order Service | Customer, Shipping | Order Service only |
| Products | Catalog Service | Order, Search | Catalog Service only |
| Payments | Payment Service | Order (via events) | Payment Service only |
Data Sharing Patterns
| Pattern | How It Works | Trade-off |
|---|---|---|
| API query (real-time) | Service A calls Service B’s API | Coupling, latency |
| Event replication | Service B publishes events, A maintains local copy | Eventual consistency |
| Shared read-only view | Database view exposed to consumers | Read-only, schema coupling |
| CQRS | Separate read/write models | Complexity, eventual consistency |
Anti-Pattern 6: Missing Observability
You cannot operate what you cannot observe. Every microservice needs:
# OpenTelemetry configuration
exporters:
otlp:
endpoint: "otel-collector:4317"
service:
pipelines:
traces:
receivers: [otlp]
exporters: [otlp]
metrics:
receivers: [otlp]
exporters: [otlp]
logs:
receivers: [otlp]
exporters: [otlp]
Three Pillars:
- Traces — Follow a request across all services (Jaeger, Zipkin)
- Metrics — Latency, error rate, throughput per service (Prometheus)
- Logs — Structured, correlated by trace ID (ELK, Loki)
Observability Checklist per Service
| Requirement | Tool | Non-Negotiable? |
|---|---|---|
| Distributed tracing | OpenTelemetry + Jaeger | Yes |
| Request/error/duration metrics | Prometheus + Grafana | Yes |
| Structured logging with trace IDs | Loki / ELK | Yes |
Health check endpoint (/health) | Built-in | Yes |
Dependency health (/ready) | Built-in | Yes |
| Alerting on SLO breach | PagerDuty / OpsGenie | Yes |
Anti-Pattern 7: Premature Microservices
Symptom: Starting a new project with 15 microservices before product-market fit.
When to Use Microservices
| Signal | Monolith | Microservices |
|---|---|---|
| Team size < 10 | ✅ | ❌ |
| Product is evolving rapidly | ✅ | ❌ |
| You need independent scaling | Consider | ✅ |
| Teams can’t deploy independently | Consider | ✅ |
| Different services need different tech stacks | Consider | ✅ |
| You have DevOps/platform team capacity | Required | ✅ |
The Migration Path
Phase 1: Well-structured monolith with clear module boundaries
↓ (when specific modules need independent scaling/deployment)
Phase 2: Extract highest-value bounded context as first service
↓ (validate the approach works)
Phase 3: Extract remaining contexts as justified by need
↓ (never extract just because "microservices")
Phase 4: Steady state — monolith core + extracted services
:::caution[Start Monolith, Extract Later] The safest path: build a well-structured monolith with clear module boundaries. When specific modules need independent scaling or deployment, extract them into services. This is cheaper and faster than starting distributed. :::
Architecture Decision Checklist
- Can each service be deployed independently? (if not, it’s a distributed monolith)
- Does each service own its own database? (single-writer principle)
- Is inter-service communication primarily asynchronous? (avoid sync chains)
- Are API contracts tested and versioned? (consumer-driven contracts)
- Is there a single-writer for each data entity?
- Can you trace a request across all services? (distributed tracing)
- Is team size sufficient for the number of services?
- Does every service have health check + readiness endpoints?
- Is observability in place before adding more services?
- Have you validated the need for microservices? (monolith-first default)
:::note[Source] This guide is derived from operational intelligence at Garnet Grid Consulting. For architecture audits, visit garnetgrid.com. :::