Microservices · Lesson 6
Service-to-Service Communication
Inter-Service Communication Patterns
Microservices need to communicate with each other to accomplish business goals. Synchronous communication (REST API calls, gRPC) is straightforward but creates coupling — Service A blocks waiting for Service B. Asynchronous communication (message queues, event streams) decouples services — Service A publishes an event; Service B consumes it later. Choose based on whether you need immediate response or can tolerate delay.
Synchronous Calls with RestTemplate and WebClient
RestTemplate and WebClient make HTTP calls from one service to another. RestTemplate is blocking (old); WebClient is non-blocking and reactive (modern). Both let you call another service's REST API, wait for response, and continue. This is simple but creates temporal coupling — if the called service is slow, the caller waits.
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.beans.factory.annotation.Autowired;
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate; // Spring Bean for HTTP calls
public void placeOrder(int userId, String productId, int quantity) {
// Call Product Service synchronously
ProductResponse product = restTemplate.getForObject(
"http://product-service:8001/api/products/" + productId,
ProductResponse.class
);
if (product == null) {
throw new RuntimeException("Product not found");
}
// Call Inventory Service synchronously
boolean available = restTemplate.getForObject(
"http://inventory-service:8002/api/inventory/check?product=" + productId + "&qty=" + quantity,
Boolean.class
);
if (!available) {
throw new RuntimeException("Insufficient inventory");
}
// Call Payment Service synchronously
PaymentRequest paymentReq = new PaymentRequest(userId, product.getPrice() * quantity);
PaymentResponse payment = restTemplate.postForObject(
"http://payment-service:8003/api/payments",
paymentReq,
PaymentResponse.class
);
if (payment != null && payment.isSuccess()) {
System.out.println("Order placed successfully");
}
}
}
// Spring configuration to create RestTemplate bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RestClientConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
class ProductResponse {
private String id;
private String name;
private double price;
public String getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
}
class PaymentRequest {
private int userId;
private double amount;
PaymentRequest(int userId, double amount) {
this.userId = userId;
this.amount = amount;
}
public int getUserId() { return userId; }
public double getAmount() { return amount; }
}
class PaymentResponse {
private boolean success;
private String transactionId;
public boolean isSuccess() { return success; }
public String getTransactionId() { return transactionId; }
}
Asynchronous Communication with Message Queues
Message queues (RabbitMQ, Apache Kafka, AWS SQS) allow services to communicate asynchronously. Service A publishes a message; Service B consumes it whenever ready. This decouples services in time — A doesn't wait for B. If B is down, the message waits in the queue until B recovers. This pattern is resilient and scalable but adds latency and complexity.
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
// Event to publish
class OrderCreatedEvent {
public int orderId;
public int userId;
public String productId;
public int quantity;
public double totalPrice;
OrderCreatedEvent(int orderId, int userId, String productId, int quantity, double totalPrice) {
this.orderId = orderId;
this.userId = userId;
this.productId = productId;
this.quantity = quantity;
this.totalPrice = totalPrice;
}
}
// Order Service: publishes events
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void placeOrder(int userId, String productId, int quantity) {
// Create order
int orderId = 1001;
double totalPrice = 99.99;
// Publish event (non-blocking)
OrderCreatedEvent event = new OrderCreatedEvent(orderId, userId, productId, quantity, totalPrice);
rabbitTemplate.convertAndSend("order.events", event);
System.out.println("Order event published, orderId: " + orderId);
// Return immediately; other services process asynchronously
}
}
// Payment Service: consumes events
@Service
public class PaymentService {
@RabbitListener(queues = "order.events.payment")
public void handleOrderCreatedEvent(OrderCreatedEvent event) {
System.out.println("Processing payment for order: " + event.orderId);
// Process payment
boolean success = processPayment(event.userId, event.totalPrice);
if (success) {
System.out.println("Payment successful");
}
}
private boolean processPayment(int userId, double amount) {
// Simulate payment processing
return true;
}
}
// Inventory Service: consumes events
@Service
public class InventoryService {
@RabbitListener(queues = "order.events.inventory")
public void handleOrderCreatedEvent(OrderCreatedEvent event) {
System.out.println("Updating inventory for order: " + event.orderId);
// Decrement inventory
decrementStock(event.productId, event.quantity);
}
private void decrementStock(String productId, int quantity) {
// Update database
}
}
// Spring configuration for RabbitMQ
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
@Bean
public Queue paymentQueue() {
return new Queue("order.events.payment");
}
@Bean
public Queue inventoryQueue() {
return new Queue("order.events.inventory");
}
@Bean
public TopicExchange orderExchange() {
return new TopicExchange("order.events");
}
@Bean
public Binding paymentBinding(Queue paymentQueue, TopicExchange orderExchange) {
return BindingBuilder.bind(paymentQueue).to(orderExchange).with("#");
}
@Bean
public Binding inventoryBinding(Queue inventoryQueue, TopicExchange orderExchange) {
return BindingBuilder.bind(inventoryQueue).to(orderExchange).with("#");
}
}
Handling Failures and Timeouts
Network calls fail — services go down, networks are slow. Implement timeouts to fail fast instead of hanging. Use retry logic with exponential backoff for transient failures. Use circuit breakers to stop calling a failing service temporarily, preventing cascading failures.
import org.springframework.retry.annotation.Retry;
import org.springframework.retry.annotation.Backoff;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
// Retry up to 3 times with exponential backoff
@Retry(maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2.0))
public ProductResponse getProductWithRetry(String productId) {
try {
return restTemplate.getForObject(
"http://product-service:8001/api/products/" + productId,
ProductResponse.class
);
} catch (Exception e) {
System.err.println("Failed to get product: " + e.getMessage());
throw e; // Throw to trigger retry
}
}
}
// Circuit breaker with resilience4j
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
@Service
public class PaymentService {
private RestTemplate restTemplate;
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResponse processPayment(double amount) {
// Call payment service
return restTemplate.postForObject(
"http://payment-service:8003/api/payments",
new PaymentRequest(1, amount),
PaymentResponse.class
);
}
// Fallback when circuit is open
public PaymentResponse paymentFallback(double amount, Exception ex) {
System.err.println("Payment service unavailable, using fallback");
PaymentResponse response = new PaymentResponse();
// Return cached response or retry later
return response;
}
}
| Communication Pattern | Coupling | Latency | Resilience | Use Case |
|---|---|---|---|---|
| Synchronous (REST) | Tight | Low | Poor | Need immediate response |
| Asynchronous (Queues) | Loose | High | Excellent | Can tolerate delay |
| Retry/Backoff | N/A | Increases | Good | Transient failures |
| Circuit Breaker | N/A | N/A | Excellent | Cascading failure prevention |
Common Mistake: Calling many services synchronously. If Order Service calls Product, Inventory, and Payment services all synchronously, and one is slow, the entire order takes forever. Use timeouts (fail fast), async calls where possible, and circuit breakers to prevent cascading failures. Also, avoid calling in series — call independent services in parallel to reduce latency.
Best Practices for Service Communication
- Use synchronous calls only when you need immediate response
- Set timeouts on HTTP calls to fail fast
- Use asynchronous messaging for decoupling and resilience
- Implement retry logic with exponential backoff for transient failures
- Use circuit breakers to prevent cascading failures
- Call independent services in parallel to reduce overall latency
- Log all inter-service calls for debugging and monitoring
- Implement distributed tracing (Zipkin, Jaeger) to track requests across services
- Design services to be idempotent — handle duplicate messages gracefully
Real-World Example: Order Processing Across Services
When an Order Service receives an order, it calls Product Service (synchronous, immediate) to check prices. If successful, it publishes an OrderCreated event to a message broker. Payment Service listens and processes payment (with retries and circuit breaker). If payment fails, it publishes a PaymentFailed event. Inventory Service listens for OrderCreated and decrements stock. Notification Service listens for success/failure events and sends emails. By using synchronous calls for immediate needs and asynchronous messaging for eventual consistency, the system is both responsive and resilient. If Payment Service is down, orders still process (notifications delayed); if Inventory Service is slow, orders complete while inventory updates in background.
Quiz Check