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.

This reference explains how to structure service boundaries, communication, and deployment to avoid common microservices anti-patterns. These decisions matter when designing new services or reorganizing existing ones. The guidance assumes a distributed system with multiple independently deployable components that communicate over well-defined APIs.

Define Service Boundaries Based on Business Domains

Choose bounded contexts as service boundaries

A service should represent a distinct business capability, not technical layers or organizational silos. Use domain-driven design principles to identify natural boundaries:

-- Example: Customer and Order services share data but are separate
-- Customer service owns customer identity, preferences, and contact info
-- Order service owns order lifecycle, status, and fulfillment

CREATE TABLE customers (
  id UUID PRIMARY KEY,
  email VARCHAR(255) UNIQUE,
  created_at TIMESTAMP
);

CREATE TABLE orders (
  id UUID PRIMARY KEY,
  customer_id UUID REFERENCES customers(id),
  total_amount DECIMAL(10,2),
  status VARCHAR(50)
);

Avoid transactional coupling between services

Never use shared databases or cross-service transactions. Services must own their data completely.

# Example service configuration without shared database
# Each service manages its own PostgreSQL instance

services:
  customer-service:
    database_url: postgres://customer:[email protected]:5432/customer_db
    port: 8080

  order-service:
    database_url: postgres://order:[email protected]:5432/order_db
    port: 8081

Handle eventual consistency with compensating actions

Implement idempotent operations and retry logic. For example, a customer update should not fail if an order update fails.

Implement Communication Patterns Correctly

Use synchronous calls carefully

Avoid tight coupling through direct service-to-service calls. Prefer asynchronous messaging for non-critical flows.

# Configuration for async message broker
# Example using Apache Kafka or RabbitMQ
---
message_broker:
  type: kafka
  brokers:
    - kafka-1:9092
    - kafka-2:9092
  topics:
    customer-updated:
      partitions: 6
      retention_days: 7

Design resilient APIs with proper error handling

All services must return consistent HTTP status codes and error structures. Never expose internal errors directly.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Customer email is required",
    "details": [
      {
        "field": "email",
        "reason": "required"
      }
    ]
  }
}

Implement circuit breakers

Use libraries like Hystrix or Resilience4j to prevent cascading failures.

// Example using Resilience4j circuit breaker
@CircuitBreaker(name = "order-service", fallbackMethod = "fallbackOrder")
public Order createOrder(OrderRequest request) {
    return orderClient.createOrder(request);
}

Deploy Services Independently

Use container orchestration for deployment

Deploy each service in its own container image and manage it separately.

# Kubernetes deployment example
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: customer-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: customer-service
  template:
    metadata:
      labels:
        app: customer-service
    spec:
      containers:
      - name: customer-service
        image: registry.example.com/customer-service:v1.2.3
        ports:
        - containerPort: 8080
        env:
        - name: DATABASE_URL
          value: "postgres://customer:[email protected]:5432/customer_db"

Separate configuration per service

Each service should load its configuration independently, not share global settings.

# Example startup script with service-specific config
export DATABASE_URL="postgres://customer:[email protected]:5432/customer_db"
export SERVICE_PORT="8080"
export LOG_LEVEL="info"

Implement health checks

Services must expose a /health endpoint that returns 200 for healthy, 503 for unhealthy.

GET /health HTTP/1.1
Host: customer-service.example.com

HTTP/1.1 200 OK
Content-Type: application/json

{
  "status": "healthy",
  "timestamp": "2023-01-01T12:00:00Z",
  "dependencies": {
    "database": "healthy",
    "cache": "healthy"
  }
}

Handle Data Consistency and Transactions

Implement event-driven updates

When data needs to be consistent across services, use events instead of direct calls.

# Event schema for customer update
---
event_type: customer.updated
payload:
  customer_id: "uuid-123"
  email: "[email protected]"
  updated_at: "2023-01-01T12:00:00Z"

Use Saga pattern for distributed transactions

Implement compensating actions when a long-running business process fails.

// Example saga implementation
public class OrderSaga {
    public void processOrder(OrderRequest request) {
        try {
            // Step 1: Reserve inventory
            inventoryService.reserve(request.getItems());
            
            // Step 2: Create order
            orderService.createOrder(request);
            
            // Step 3: Charge payment
            paymentService.charge(request.getPayment());
        } catch (Exception e) {
            // Compensate for previous steps
            compensationService.compensate();
        }
    }
}

Avoid data duplication where it causes complexity

Do not duplicate data unnecessarily. Only duplicate when performance or availability requires it.

-- Good: Customer data duplicated for performance
CREATE TABLE customer_cache (
  id UUID PRIMARY KEY,
  email VARCHAR(255),
  name VARCHAR(255),
  last_updated TIMESTAMP
);

-- Bad: Customer data duplicated without clear reason
CREATE TABLE customer_copy (
  id UUID PRIMARY KEY,
  email VARCHAR(255),
  name VARCHAR(255),
  address TEXT,
  phone VARCHAR(20),
  created_at TIMESTAMP,
  updated_at TIMESTAMP
);

Monitor and Debug Services

Implement distributed tracing

Each service should propagate trace IDs to enable end-to-end request tracking.

GET /orders/123 HTTP/1.1
Host: order-service.example.com
X-B3-TraceId: 1234567890abcdef1234567890abcdef
X-B3-SpanId: abcdef1234567890

Set up service-specific logging

Services must log consistently, with structured output for monitoring systems.

{
  "timestamp": "2023-01-01T12:00:00Z",
  "service": "customer-service",
  "level": "info",
  "message": "Customer created",
  "request_id": "req-12345",
  "customer_id": "cust-67890"
}

Use circuit breaker metrics

Monitor failure rates and timeouts to detect service degradation early.

# Example metrics endpoint
curl http://customer-service.example.com/metrics
{
  "circuit_breaker": {
    "failure_rate": 0.02,
    "total_requests": 1000,
    "failed_requests": 20,
    "state": "OPEN"
  }
}

Common Pitfalls and How to Avoid Them

Don't create too many small services

A service should be a meaningful business unit, not just a code organization.

Avoid tight coupling through shared libraries

Never share business logic via shared JARs or modules between services. Each service must be independently deployable.

Don't ignore network failures

Network calls are always unreliable. Always implement timeouts and retry logic.

// Example with proper timeout and retry
@Retryable(
    value = {WebClientRequestException.class},
    maxAttempts = 3,
    backoff = @Backoff(delay = 1000, multiplier = 2)
)
public Customer getCustomer(String id) {
    return webClient.get()
        .uri("/customers/" + id)
        .retrieve()
        .bodyToMono(Customer.class)
        .block(Duration.ofSeconds(5));
}

Don't mix service responsibilities

If two services are always called together, consider merging them.

Avoid service sprawl

New services should be justified by business value, not technical convenience.

Never share databases between services

This creates the classic anti-pattern of transactional coupling.

# Example: Bad - shared database
---
services:
  customer-service:
    database_url: "postgres://shared:[email protected]:5432/shared_db"
  
  order-service:
    database_url: "postgres://shared:[email protected]:5432/shared_db"

# Example: Good - separate databases
---
services:
  customer-service:
    database_url: "postgres://customer:[email protected]:5432/customer_db"
  
  order-service:
    database_url: "postgres://order:[email protected]:5432/order_db"

Don't use synchronous calls for non-critical operations

Synchronous calls can cause cascading failures when services are unavailable.

Avoid hard-coded dependencies between services

All service dependencies must be configurable at runtime.

The key to successful microservices is maintaining clear boundaries and resilient communication patterns. When these principles are followed, services remain independently deployable and maintainable.

This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.