Service Communication
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