🚫 Microservices Anti-Patterns and Solutions
1. 🏗️ Architectural Anti-Patterns
1.1 Distributed Monolith
What It Is: Services that are deployed independently but are so tightly coupled that they must be deployed together.
Signs You Have This:
- Changes in one service require changes in other services
- Services share databases
- Synchronous communication chains
- Tight deployment coupling
Example of the Problem:
// Anti-pattern example
@Service
public class OrderService {
@Autowired
private CustomerService customerService; // Direct service dependency
@Autowired
private PaymentService paymentService;
@Autowired
private InventoryService inventoryService;
public Order createOrder(OrderRequest request) {
// Synchronous calls creating a chain of dependencies
Customer customer = customerService.getCustomer(request.getCustomerId());
Payment payment = paymentService.processPayment(request.getPayment());
inventoryService.reserveItems(request.getItems());
return orderRepository.save(new Order(customer, payment));
}
}
Solution:
@Service
public class OrderService {
private final EventPublisher eventPublisher;
private final CustomerClient customerClient;
public Order createOrder(OrderRequest request) {
// Verify customer exists with circuit breaker
CustomerDTO customer = customerClient.getCustomerBasicInfo(
request.getCustomerId());
// Create order in pending state
Order order = orderRepository.save(
new Order(request, OrderStatus.PENDING));
// Publish event for async processing
eventPublisher.publish(new OrderCreatedEvent(
order.getId(),
request.getPayment(),
request.getItems()
));
return order;
}
@EventListener
public void onPaymentProcessed(PaymentProcessedEvent event) {
Order order = orderRepository.findById(event.getOrderId())
.orElseThrow();
order.updatePaymentStatus(event.getStatus());
orderRepository.save(order);
}
}
1.2 Data Lake Anti-Pattern
What It Is: Multiple services sharing the same database.
Signs You Have This:
- Multiple services accessing same tables
- Cross-service transactions
- Schema changes affect multiple services
Problem Example:
// Anti-pattern: Multiple services sharing database access
@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
// Used by both CustomerService and OrderService
@Query("SELECT c FROM Customer c WHERE c.id = ?1")
Customer findCustomerById(Long id);
}
@Service
public class OrderService {
@Autowired
private CustomerRepository customerRepository; // Direct database access
public Order createOrder(OrderRequest request) {
Customer customer = customerRepository.findCustomerById(
request.getCustomerId());
// Process order using customer data
}
}
Solution:
// Each service has its own database
@Configuration
public class OrderDatabaseConfig {
@Bean
@ConfigurationProperties(prefix = "order.datasource")
public DataSource orderDataSource() {
return DataSourceBuilder.create().build();
}
}
// Service uses API calls
@Service
public class OrderService {
private final CustomerClient customerClient;
public Order createOrder(OrderRequest request) {
CustomerDTO customer = customerClient.getCustomer(
request.getCustomerId());
// Store only necessary customer data
OrderCustomer orderCustomer = new OrderCustomer(
customer.getId(),
customer.getName(),
customer.getShippingAddress()
);
return orderRepository.save(new Order(orderCustomer));
}
}
2. 🔄 Communication Anti-Patterns
2.1 Chatty Communication
What It Is: Services making excessive API calls to each other.
Problem Example:
// Anti-pattern: Multiple calls for related data
@Service
public class ProductCatalogService {
private final PriceService priceService;
private final InventoryService inventoryService;
private final ReviewService reviewService;
public ProductDetails getProductDetails(String productId) {
Product product = repository.findById(productId);
Price price = priceService.getPrice(productId); // API call 1
Integer stock = inventoryService.getStock(productId); // API call 2
List<Review> reviews = reviewService.getReviews(productId); // API call 3
return new ProductDetails(product, price, stock, reviews);
}
}
Solution:
// Solution 1: API Composition
@Service
public class ProductCatalogService {
public ProductDetails getProductDetails(String productId) {
CompletableFuture<Price> priceFuture =
CompletableFuture.supplyAsync(() ->
priceService.getPrice(productId));
CompletableFuture<Integer> stockFuture =
CompletableFuture.supplyAsync(() ->
inventoryService.getStock(productId));
CompletableFuture<List<Review>> reviewsFuture =
CompletableFuture.supplyAsync(() ->
reviewService.getReviews(productId));
return CompletableFuture.allOf(
priceFuture, stockFuture, reviewsFuture)
.thenApply(v -> new ProductDetails(
product,
priceFuture.join(),
stockFuture.join(),
reviewsFuture.join()))
.join();
}
}
// Solution 2: Event-Driven Approach
@Service
public class ProductEventHandler {
@EventListener
public void onProductUpdated(ProductUpdatedEvent event) {
ProductDetails details = cache.get(event.getProductId());
switch(event.getType()) {
case PRICE_UPDATED:
details.updatePrice(event.getNewPrice());
break;
case STOCK_CHANGED:
details.updateStock(event.getNewStock());
break;
case REVIEW_ADDED:
details.addReview(event.getNewReview());
break;
}
cache.put(event.getProductId(), details);
}
}
2.2 Synchronous Chain Calls
What It Is: Long chain of synchronous service calls.
Problem Example:
// Anti-pattern: Chain of synchronous calls
@Service
public class OrderProcessor {
public OrderResult processOrder(OrderRequest request) {
// Synchronous chain
Customer customer = customerService.validate(request.getCustomerId());
Payment payment = paymentService.process(request.getPayment());
Inventory inventory = inventoryService.reserve(request.getItems());
Shipment shipment = shippingService.schedule(
customer.getAddress(), inventory.getItems());
return new OrderResult(customer, payment, inventory, shipment);
}
}
Solution:
// Choreography-based solution
@Service
public class OrderProcessor {
public OrderResult processOrder(OrderRequest request) {
// Validate customer synchronously (essential)
customerService.validateCustomer(request.getCustomerId());
// Create order in pending state
Order order = orderRepository.save(
new Order(request, OrderStatus.PENDING));
// Publish event for async processing
eventPublisher.publish(new OrderCreatedEvent(order));
return new OrderResult(order.getId(), OrderStatus.PENDING);
}
@EventListener
public void onPaymentProcessed(PaymentProcessedEvent event) {
Order order = orderRepository.findById(event.getOrderId())
.orElseThrow();
if (event.isSuccessful()) {
order.setStatus(OrderStatus.PAYMENT_COMPLETED);
eventPublisher.publish(new OrderPaidEvent(order));
} else {
order.setStatus(OrderStatus.PAYMENT_FAILED);
eventPublisher.publish(new OrderFailedEvent(order));
}
orderRepository.save(order);
}
}
3. 🧱 Design Anti-Patterns
3.1 Wrong Service Boundaries
What It Is: Services that don't align with business domains.
Problem Example:
// Anti-pattern: Technical rather than business boundaries
@Service
public class DatabaseService {
public void saveCustomer(Customer customer) { ... }
public void saveOrder(Order order) { ... }
public void savePayment(Payment payment) { ... }
}
@Service
public class ValidationService {
public void validateCustomer(Customer customer) { ... }
public void validateOrder(Order order) { ... }
public void validatePayment(Payment payment) { ... }
}
Solution:
// Domain-driven service boundaries
@Service
public class CustomerService {
public Customer createCustomer(CustomerRequest request) {
validateCustomerData(request);
Customer customer = new Customer(request);
customerRepository.save(customer);
eventPublisher.publish(new CustomerCreatedEvent(customer));
return customer;
}
}
@Service
public class OrderService {
public Order createOrder(OrderRequest request) {
validateOrderData(request);
Order order = new Order(request);
orderRepository.save(order);
eventPublisher.publish(new OrderCreatedEvent(order));
return order;
}
}
3.2 No API Versioning
What It Is: Lack of API versioning strategy leading to breaking changes.
Problem Example:
// Anti-pattern: No versioning
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
public Order createOrder(OrderRequest request) {
// Breaking changes affect all clients
return orderService.createOrder(request);
}
}
Solution:
@RestController
public class OrderController {
@PostMapping("/api/v1/orders")
public OrderV1Response createOrderV1(OrderV1Request request) {
return orderService.createOrderV1(request);
}
@PostMapping("/api/v2/orders")
public OrderV2Response createOrderV2(OrderV2Request request) {
return orderService.createOrderV2(request);
}
}
// API Version mapping
@Configuration
public class ApiVersionConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("orders-v1", r -> r
.path("/api/v1/orders/**")
.uri("lb://order-service-v1"))
.route("orders-v2", r -> r
.path("/api/v2/orders/**")
.uri("lb://order-service-v2"))
.build();
}
}
4. 📈 Scalability Anti-Patterns
4.1 No Caching Strategy
What It Is: Missing or improper caching leading to unnecessary service calls.
Solution:
@Service
public class ProductCatalogService {
private final CacheManager cacheManager;
public ProductDetails getProductDetails(String productId) {
return cacheManager.getCache("products")
.get(productId, () -> {
// Fetch from service only if not in cache
return fetchProductDetails(productId);
});
}
@CacheEvict(value = "products", key = "#productId")
public void updateProduct(String productId, ProductUpdate update) {
// Update product and invalidate cache
productRepository.update(productId, update);
}
}
4.2 No Circuit Breaker
What It Is: Missing fault tolerance in service communications.
Solution:
@Service
public class OrderService {
@CircuitBreaker(name = "paymentService",
fallbackMethod = "paymentFallback")
@RateLimiter(name = "paymentService")
@Bulkhead(name = "paymentService")
public PaymentResponse processPayment(OrderPayment payment) {
return paymentClient.processPayment(payment);
}
private PaymentResponse paymentFallback(OrderPayment payment,
Exception e) {
// Fallback logic
return PaymentResponse.builder()
.orderId(payment.getOrderId())
.status(PaymentStatus.PENDING)
.message("Payment processing delayed")
.build();
}
}
5. 🔍 Monitoring Anti-Patterns
5.1 Insufficient Monitoring
Solution:
@Aspect
@Component
public class ServiceMonitoringAspect {
private final MeterRegistry registry;
@Around("@annotation(Monitor)")
public Object monitorOperation(ProceedingJoinPoint joinPoint)
throws Throwable {
Timer.Sample sample = Timer.start(registry);
try {
Object result = joinPoint.proceed();
sample.stop(Timer.builder("service.operation")
.tag("method", joinPoint.getSignature().getName())
.tag("status", "success")
.register(registry));
return result;
} catch (Exception e) {
sample.stop(Timer.builder("service.operation")
.tag("method", joinPoint.getSignature().getName())
.tag("status", "error")
.tag("error.type", e.getClass().getSimpleName())
.register(registry));
throw e;
}
}
}
6. 📝 Best Practices to Avoid Anti-Patterns
-
Design Principles
- Follow Domain-Driven Design
- Use Bounded Contexts
- Implement proper service boundaries
- Design for failure
-
Communication Guidelines
- Prefer asynchronous communication
- Use event-driven architecture
- Implement proper error handling
- Use circuit breakers
-
Data Management
- Maintain data independence
- Use event sourcing when appropriate
- Implement proper caching strategies
- Handle data consistency
-
Monitoring and Observability
- Implement comprehensive logging
- Use distributed tracing
- Monitor service health
- Track business metrics
7. 📚 References
- "Microservices Anti-patterns and Pitfalls" by Mark Richards
- "Building Microservices" by Sam Newman
- "Release It!" by Michael Nygard
- "Designing Distributed Systems" by Brendan Burns