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.
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)
);
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
Implement idempotent operations and retry logic. For example, a customer update should not fail if an order update fails.
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
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"
}
]
}
}
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 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"
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"
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"
}
}
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"
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();
}
}
}
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
);
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
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"
}
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"
}
}
A service should be a meaningful business unit, not just a code organization.
Never share business logic via shared JARs or modules between services. Each service must be independently deployable.
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));
}
If two services are always called together, consider merging them.
New services should be justified by business value, not technical convenience.
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"
Synchronous calls can cause cascading failures when services are unavailable.
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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy