Microservices communicate through synchronous (request-response) or asynchronous (event-driven) patterns. Choosing the right pattern affects coupling, resilience, and performance.

Synchronous Communication

REST (HTTP/JSON)

Most common, simplest to implement:

  @FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping("/api/users/{id}")
    UserDto getUser(@PathVariable Long id);
}
  

Pros: simple, widely understood, easy to debug Cons: tight coupling, cascading failures, latency accumulation

gRPC (HTTP/2 + Protocol Buffers)

High-performance binary protocol:

  UserResponse response = userServiceStub.getUser(
    GetUserRequest.newBuilder().setId(userId).build());
  

Pros: fast, strongly typed, streaming support Cons: not browser-friendly, requires proto definitions

Asynchronous Communication

Message Broker (Kafka, RabbitMQ)

  Order Service ──publish──▶ Kafka ──subscribe──▶ Inventory Service
                                          ──subscribe──▶ Notification Service
  

Pros: loose coupling, absorbs spikes, replay capability Cons: eventual consistency, harder to debug, message ordering

Event-Driven Example

  // Order Service — publish event
@Service
public class OrderService {
    private final KafkaTemplate<String, OrderCreatedEvent> kafka;

    public Order createOrder(CreateOrderRequest request) {
        Order order = orderRepository.save(new Order(request));
        kafka.send("order-events", new OrderCreatedEvent(order.getId(), order.getItems()));
        return order;
    }
}

// Inventory Service — consume event
@KafkaListener(topics = "order-events")
public void handleOrderCreated(OrderCreatedEvent event) {
    event.items().forEach(item -> inventoryService.reserve(item.productId(), item.quantity()));
}
  

Comparison

Pattern Coupling Latency Consistency Failure handling
Sync REST Tight Adds up Strong Circuit breaker
Sync gRPC Tight Lower Strong Circuit breaker
Async messaging Loose Decoupled Eventual Retry, DLQ
Event sourcing Loose Decoupled Eventual Event replay

Resilience Patterns

Circuit Breaker

  @CircuitBreaker(name = "user-service", fallbackMethod = "getUserFallback")
public UserDto getUser(Long id) {
    return userClient.getUser(id);
}

public UserDto getUserFallback(Long id, Exception ex) {
    return UserDto.unknown(id);
}
  

States: Closed (normal) → Open (failing, fast-fail) → Half-Open (test recovery)

Retry with Backoff

  @Retry(name = "user-service")
@GetMapping("/api/users/{id}")
public UserDto getUser(@PathVariable Long id) { /* ... */ }
  
  resilience4j:
  retry:
    instances:
      user-service:
        max-attempts: 3
        wait-duration: 1s
        exponential-backoff-multiplier: 2
  

Timeout

Always set timeouts on remote calls:

  @FeignClient(name = "user-service", configuration = FeignConfig.class)
public interface UserClient { /* ... */ }

@Configuration
public class FeignConfig {
    @Bean
    public Request.Options options() {
        return new Request.Options(3, TimeUnit.SECONDS, 5, TimeUnit.SECONDS, true);
    }
}
  

Service Mesh (Istio/Linkerd)

Handles cross-cutting concerns at infrastructure level:

  • mTLS between services
  • Traffic routing and splitting
  • Observability (metrics, tracing)
  • Retry and circuit breaking
  Service A → Sidecar Proxy → Sidecar Proxy → Service B
              (Envoy)                        (Envoy)
  

API Gateway Pattern

Single entry point for clients:

  Mobile App ──┐
Web App ─────┼──▶ API Gateway ──▶ Backend Services
Partner API ─┘     (routing, auth, rate limiting)
  

Best Practices

  • Prefer async messaging for non-critical paths (notifications, analytics)
  • Use sync calls only when immediate response is required
  • Always implement circuit breakers and timeouts for sync calls
  • Use contract testing (Pact) to verify service interfaces
  • Avoid chatty communication — batch requests when possible