Home CS Fundamentals Microservices Architecture: Patterns and Pitfalls for Developers
Intermediate 3 min · July 13, 2026

Microservices Architecture: Patterns and Pitfalls for Developers

Learn microservices architecture patterns, common pitfalls, and debugging strategies.

N
Naren Founder & Principal Engineer

20+ years shipping production systems from the metal up. Drawn from code that ran under real load.

Follow
Production
production tested
July 13, 2026
last updated
2,165
articles · all by Naren
Before you start⏱ 20-25 min read
  • Basic understanding of REST APIs and HTTP
  • Familiarity with Docker and containerization
  • Experience with a programming language (Java, Python, Go, etc.)
  • Basic knowledge of databases and SQL
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Microservices decompose an application into independently deployable services.
  • Key patterns: API Gateway, Service Discovery, Circuit Breaker, Saga, Event Sourcing.
  • Common pitfalls: distributed monolith, improper service boundaries, data consistency issues.
  • Debugging requires distributed tracing, centralized logging, and health checks.
  • Start with a monolith and extract microservices when needed.
✦ Definition~90s read
What is Microservices Architecture?

Microservices architecture is a software design approach where an application is built as a collection of small, autonomous services, each responsible for a specific business capability and communicating over a network.

Imagine a restaurant kitchen.
Plain-English First

Imagine a restaurant kitchen. In a monolith, one chef does everything: takes orders, cooks, plates, and cleans. In microservices, you have specialized stations: one chef for salads, another for grilling, a third for desserts. Each station works independently but communicates via tickets (APIs). If the grill station fails, salads can still be served. But now you need a system to coordinate orders (API Gateway), find which station is free (Service Discovery), and handle a burnt steak (Circuit Breaker).

Microservices architecture has become the go-to pattern for building scalable, resilient applications. Instead of a single monolithic codebase, you break your application into small, loosely coupled services that each own a specific business capability. This approach enables independent deployment, technology diversity, and team autonomy. However, microservices come with their own set of challenges: network latency, data consistency, distributed debugging, and operational complexity. In this tutorial, we'll explore essential patterns like API Gateway, Service Discovery, Circuit Breaker, and Saga, along with common pitfalls such as the distributed monolith and improper service boundaries. We'll also walk through a real-world production incident to see how things can go wrong and how to fix them. By the end, you'll have a practical understanding of when and how to adopt microservices, and how to avoid the traps that trip up many teams.

1. Core Patterns: API Gateway and Service Discovery

An API Gateway acts as a single entry point for clients, routing requests to appropriate microservices. It can handle authentication, rate limiting, and request transformation. Service Discovery allows services to find each other dynamically, typically using a registry like Consul or Eureka. Without it, you'd hardcode IP addresses, which breaks in dynamic environments like Kubernetes.

Example: A mobile app calls /api/orders. The gateway forwards to the order service. The order service needs to call the payment service; it queries the registry for payment-service and gets a healthy instance.

Implementation tip: Use a managed gateway like Kong or AWS API Gateway, or build one with Spring Cloud Gateway. For service discovery, use Kubernetes DNS or a dedicated registry.

api-gateway-config.ymlBASH
1
2
3
4
5
6
7
8
9
10
11
# Example Kong API Gateway route configuration
# Route /api/orders to order-service
curl -i -X POST http://localhost:8001/routes \
  --data 'name=orders-route' \
  --data 'paths[]=/api/orders' \
  --data 'service.name=order-service'

# Service discovery with Consul
# Register order-service
curl -X PUT http://localhost:8500/v1/agent/service/register \
  -d '{"Name": "order-service", "Address": "192.168.1.10", "Port": 8080}'
Output
HTTP/1.1 201 Created
...
💡Gateway as a facade
📊 Production Insight
In production, gateways can become a bottleneck. Use horizontal scaling and consider using a sidecar pattern (e.g., Envoy) to offload traffic.
🎯 Key Takeaway
API Gateway and Service Discovery are foundational for dynamic routing and load balancing.

2. Resilience: Circuit Breaker and Retry Patterns

In a distributed system, failures are inevitable. A circuit breaker prevents a service from repeatedly calling a failing downstream service. It monitors for failures and after a threshold, opens the circuit, returning a fallback response immediately. Retry patterns can be used for transient failures but must be combined with exponential backoff and jitter to avoid thundering herd.

Example: The order service calls payment service. If payment returns 5 consecutive errors, the circuit breaker opens. Subsequent calls to order service return a cached response or an error message without hitting payment.

Implementation: Use libraries like Hystrix (Java), Polly (.NET), or resilience4j. In Kubernetes, you can use Istio's circuit breaker.

circuit-breaker-example.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Simulate circuit breaker with curl and a simple script
# Assume payment service is at http://payment:8080
# If it fails 3 times, stop calling for 30 seconds

failures=0
threshold=3
while true; do
  response=$(curl -s -o /dev/null -w "%{http_code}" http://payment:8080/charge)
  if [ "$response" -ne 200 ]; then
    failures=$((failures + 1))
    if [ $failures -ge $threshold ]; then
      echo "Circuit open! Skipping payment."
      # Return fallback
      echo '{"status": "failed", "message": "Payment unavailable"}'
      sleep 30
      failures=0
    fi
  else
    failures=0
  fi
  sleep 1
done
Output
200
200
500
Circuit open! Skipping payment.
{"status": "failed", "message": "Payment unavailable"}
⚠ Retry storms
📊 Production Insight
Set circuit breaker timeouts based on your SLOs. Monitor open circuit counts to detect downstream issues early.
🎯 Key Takeaway
Circuit breakers and retries with backoff protect your system from cascading failures.

3. Data Management: Saga Pattern and Event Sourcing

In microservices, each service has its own database, making distributed transactions tricky. The Saga pattern manages a sequence of local transactions with compensating actions for rollback. There are two approaches: choreography (each service publishes events) and orchestration (a coordinator tells services what to do). Event sourcing stores state changes as a sequence of events, enabling audit trails and rebuilding state.

Example: An order saga: Create Order -> Reserve Inventory -> Charge Payment -> Ship. If payment fails, a compensating transaction releases inventory.

Implementation: Use a message broker like Kafka or RabbitMQ for choreography. For orchestration, use a saga orchestrator service.

saga-orchestrator.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Simplified saga orchestrator using curl and a state machine
# Steps: createOrder, reserveInventory, chargePayment, shipOrder

order_id=$(curl -s -X POST http://order/orders -d '{"item": "book"}' | jq -r '.id')

# Step 1: Reserve inventory
response=$(curl -s -o /dev/null -w "%{http_code}" -X POST http://inventory/reserve -d "{\"orderId\": $order_id}")
if [ "$response" -ne 200 ]; then
  echo "Inventory reservation failed. Compensating: cancel order."
  curl -X DELETE http://order/orders/$order_id
  exit 1
fi

# Step 2: Charge payment
response=$(curl -s -o /dev/null -w "%{http_code}" -X POST http://payment/charge -d "{\"orderId\": $order_id}")
if [ "$response" -ne 200 ]; then
  echo "Payment failed. Compensating: release inventory and cancel order."
  curl -X POST http://inventory/release -d "{\"orderId\": $order_id}"
  curl -X DELETE http://order/orders/$order_id
  exit 1
fi

# Step 3: Ship
curl -X POST http://shipping/ship -d "{\"orderId\": $order_id}"
echo "Order $order_id completed successfully."
Output
Order 12345 completed successfully.
🔥Saga vs 2PC
📊 Production Insight
Idempotency is crucial for sagas. Ensure each step can be safely retried. Use unique request IDs to detect duplicates.
🎯 Key Takeaway
Sagas and event sourcing help maintain data consistency across services without distributed transactions.

4. Communication: Synchronous vs Asynchronous

Services communicate either synchronously (HTTP/REST, gRPC) or asynchronously (message queues, events). Synchronous is simpler but introduces coupling and latency. Asynchronous improves resilience and scalability but adds complexity in message ordering and error handling.

Best practice: Use synchronous calls for queries that need immediate response (e.g., get user details). Use asynchronous events for commands that can be processed later (e.g., send email, update analytics).

Example: When an order is placed, the order service publishes an 'OrderPlaced' event. The inventory service listens and updates stock. The notification service sends an email. This decouples services.

async-communication.shBASH
1
2
3
4
5
6
7
8
9
10
# Publish event to Kafka
kafka-console-producer.sh --broker-list localhost:9092 --topic order-events <<< '{"event": "OrderPlaced", "orderId": 123}'

# Consume event (inventory service)
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic order-events --from-beginning | while read line; do
  echo "Received event: $line"
  # Process inventory update
  order_id=$(echo $line | jq -r '.orderId')
  curl -X POST http://inventory/deduct -d "{\"orderId\": $order_id}"
done
Output
Received event: {"event": "OrderPlaced", "orderId": 123}
💡Choose async by default
📊 Production Insight
Message brokers can become single points of failure. Use replication and consider using a cloud-managed service like AWS SQS or Azure Service Bus.
🎯 Key Takeaway
Mix synchronous and asynchronous communication based on use case. Async is better for resilience and scalability.

5. Observability: Logging, Metrics, and Tracing

With many services, you need centralized logging, metrics, and distributed tracing to debug issues. Use structured logging (JSON) and ship logs to a central system like ELK or Splunk. Metrics (CPU, memory, request rates) help with monitoring and alerting. Distributed tracing (e.g., with OpenTelemetry) tracks a request across services, showing where time is spent.

Example: A user reports a slow checkout. You look at the trace and see that the payment service took 5 seconds due to a database query. Without tracing, you'd have to guess.

Implementation: Add a correlation ID to every request. Use OpenTelemetry SDK to instrument your services. Export traces to Jaeger or Zipkin.

distributed-tracing.shBASH
1
2
3
4
5
6
7
# Example using OpenTelemetry with curl (simplified)
# Inject trace headers
curl -H "traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" http://order-service/orders

# Check Jaeger UI for trace
# (Assume Jaeger is running at http://jaeger:16686)
curl http://jaeger:16686/api/traces?service=order-service | jq '.data[0].spans'
Output
[
{
"traceID": "0af7651916cd43dd8448eb211c80319c",
"spanID": "b7ad6b7169203331",
"operationName": "GET /orders",
"duration": 1200
}
]
🔥Correlation IDs
📊 Production Insight
Sampling is important for high-throughput systems. Use head-based sampling to avoid overwhelming your tracing backend.
🎯 Key Takeaway
Observability is non-negotiable in microservices. Invest in logging, metrics, and tracing from day one.

6. Deployment and Testing Strategies

Microservices enable independent deployment, but require robust CI/CD pipelines and testing strategies. Use containerization (Docker) and orchestration (Kubernetes) for consistent environments. Testing includes unit tests, integration tests (with contract testing), and end-to-end tests for critical flows. Canary deployments and blue-green deployments reduce risk.

Example: Deploy a new version of the payment service to 10% of traffic. Monitor error rates. If stable, roll out to 100%.

Implementation: Use Kubernetes Deployments with rolling updates. For contract testing, use tools like Pact or Spring Cloud Contract.

k8s-canary-deploy.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
# Canary deployment using Kubernetes
# Create a canary deployment with 10% replicas
kubectl create deployment payment-canary --image=payment:v2 --replicas=1
kubectl scale deployment payment --replicas=9

# Use a service with label selector to split traffic
# (Simplified: manually adjust replicas)
# Monitor errors
kubectl logs -l app=payment-canary --tail=10

# If stable, promote
kubectl set image deployment/payment payment=payment:v2
kubectl delete deployment payment-canary
Output
deployment.apps/payment-canary created
deployment.apps/payment scaled
⚠ Don't skip integration tests
📊 Production Insight
Feature flags can complement canary deployments. They allow you to toggle features without redeploying.
🎯 Key Takeaway
Automate deployments and use canary releases to minimize risk. Test service interactions thoroughly.

7. Common Pitfalls and How to Avoid Them

  1. Distributed Monolith: Services are too chatty or share databases. Solution: Define bounded contexts and enforce data ownership.
  2. Improper Service Boundaries: Services that change together should be merged. Use domain-driven design to find boundaries.
  3. Over-engineering: Starting with microservices when a monolith would suffice. Begin with a monolith and extract services as needed.
  4. Lack of Automation: Manual testing and deployment lead to errors. Invest in CI/CD and infrastructure as code.
  5. Ignoring Network Fallacies: Network is not reliable, latency is not zero, bandwidth is not infinite. Design for failure.

Example: A team created 20 microservices for a simple CRUD app. Each service had its own database, but they ended up joining data in the application layer, creating a distributed monolith. They refactored into a single service with modular packages.

detect-chatty-services.shBASH
1
2
3
4
5
6
# Check for high inter-service call frequency using logs
# Count calls from order-service to inventory-service
kubectl logs -l app=order-service --tail=1000 | grep "inventory-service" | wc -l

# If number is high per request, consider merging services
# Example output: 50 calls per request indicates chattiness
Output
50
💡Start with a monolith
📊 Production Insight
Use metrics like 'calls per request' to detect chattiness. If a request spans more than 3 services, reconsider boundaries.
🎯 Key Takeaway
Avoid distributed monolith by ensuring services are independent and own their data. Start simple and evolve.
● Production incidentPOST-MORTEMseverity: high

The Case of the Cascading Checkout Failures

Symptom
Users reported 'Checkout Failed' errors intermittently, with increasing frequency during peak hours.
Assumption
The developer assumed the payment service was overloaded and needed more instances.
Root cause
The payment service was slow due to a database deadlock, but the order service had no timeout on its HTTP call to payment. When payment slowed, order service threads blocked, exhausting its connection pool and causing cascading failures to other services.
Fix
Implemented a circuit breaker pattern with a timeout and fallback. Added distributed tracing to identify the bottleneck. Set up alerts for thread pool exhaustion.
Key lesson
  • Always set timeouts on inter-service calls.
  • Use circuit breakers to prevent cascading failures.
  • Implement distributed tracing to pinpoint slow services.
  • Monitor connection pools and thread pools.
  • Test failure scenarios in staging.
Production debug guideSymptom to Action4 entries
Symptom · 01
Service returns 500 intermittently
Fix
Check health endpoints and logs. Use distributed tracing to see if the error originates from a downstream service.
Symptom · 02
High latency on a specific endpoint
Fix
Trace the request through all services. Look for slow database queries, network congestion, or resource contention.
Symptom · 03
Cascading failures across services
Fix
Check if circuit breakers are open. Review timeouts and retry policies. Look for thread pool exhaustion.
Symptom · 04
Data inconsistency between services
Fix
Verify saga or event sourcing implementation. Check for missing compensating transactions or duplicate events.
★ Quick Debug Cheat SheetCommon symptoms and immediate actions for microservices issues.
Service unavailable
Immediate action
Check service registry and health endpoint.
Commands
curl -f http://service/health
kubectl get pods -l app=service
Fix now
Restart the pod or scale up.
Slow response+
Immediate action
Check distributed trace for bottleneck.
Commands
traceroute service
kubectl logs -l app=service --tail=100
Fix now
Add caching or optimize query.
Circuit breaker open+
Immediate action
Check downstream service health.
Commands
curl -f http://downstream/health
kubectl describe hpa
Fix now
Fix downstream service or increase timeout.
Data inconsistency+
Immediate action
Check saga logs and event store.
Commands
kubectl logs -l app=saga-orchestrator
curl http://event-store/events
Fix now
Replay failed compensating transactions.
PatternPurposeWhen to Use
API GatewaySingle entry point for clientsWhen you have multiple services and need centralized auth/routing
Service DiscoveryDynamic service locationIn dynamic environments like Kubernetes
Circuit BreakerPrevent cascading failuresWhen calling unreliable downstream services
SagaManage distributed transactionsWhen you need data consistency across services
Event SourcingStore state as eventsWhen you need audit trails or rebuild state
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
api-gateway-config.ymlcurl -i -X POST http://localhost:8001/routes \1. Core Patterns
circuit-breaker-example.shfailures=02. Resilience
saga-orchestrator.shorder_id=$(curl -s -X POST http://order/orders -d '{"item": "book"}' | jq -r '.i...3. Data Management
async-communication.shkafka-console-producer.sh --broker-list localhost:9092 --topic order-events <<< ...4. Communication
distributed-tracing.shcurl -H "traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" h...5. Observability
k8s-canary-deploy.shkubectl create deployment payment-canary --image=payment:v2 --replicas=16. Deployment and Testing Strategies
detect-chatty-services.shkubectl logs -l app=order-service --tail=1000 | grep "inventory-service" | wc -l7. Common Pitfalls and How to Avoid Them

Key takeaways

1
Microservices decompose applications into independently deployable services, each owning its data.
2
Essential patterns
API Gateway, Service Discovery, Circuit Breaker, Saga, and Event Sourcing.
3
Avoid distributed monolith by ensuring services are loosely coupled and have clear boundaries.
4
Invest in observability (logging, metrics, tracing) and automation (CI/CD, containerization) from the start.
5
Start with a monolith and extract microservices when the monolith becomes a bottleneck.

Common mistakes to avoid

4 patterns
×

Sharing a database between services

×

Not setting timeouts on HTTP calls

×

Over-engineering with too many services

×

Ignoring network latency and failures

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain the API Gateway pattern and its benefits.
Q02SENIOR
How does the Circuit Breaker pattern work and when would you use it?
Q03SENIOR
Compare choreography and orchestration in the Saga pattern.
Q01 of 03JUNIOR

Explain the API Gateway pattern and its benefits.

ANSWER
An API Gateway is a single entry point for clients that routes requests to appropriate microservices. Benefits include centralized authentication, rate limiting, request transformation, and load balancing. It simplifies client code and provides a layer of abstraction.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
When should I use microservices instead of a monolith?
02
How do microservices handle database transactions?
03
What is the biggest challenge in microservices?
04
How do I ensure data consistency across services?
N
Naren Founder & Principal Engineer

20+ years shipping production systems from the metal up. Drawn from code that ran under real load.

Follow
Verified
production tested
July 13, 2026
last updated
2,165
articles · all by Naren
🔥

That's . Mark it forged?

3 min read · try the examples if you haven't

Previous
DevOps: Culture, Practices, and Toolchain
1 / 1 ·
Next
Event-Driven Architecture: Patterns and Implementation