Chapter 5: Inter-Service Communication
🎯 Chapter Objectives
- Understand synchronous vs asynchronous communication modes
- Implement REST calls with OpenFeign
- Apply resilience patterns with Resilience4j
- Master Circuit Breaker, Retry, Rate Limiter
- Implement effective fallbacks
1. Communication Modes
1.1 Synchronous Communication
Characteristics:
- Client waits for response
- Simple to implement
- Temporal coupling (dependency on availability)
- Cumulative latency
1.2 Asynchronous Communication
Characteristics:
- Fire and forget
- Temporal decoupling
- More resilient
- More complex (eventual consistency)
1.3 When to Use What?
| Use Case | Synchronous | Asynchronous |
|---|---|---|
| User validation | ✅ | |
| Get product details | ✅ | |
| Create an order | ✅ | |
| Send notification | ✅ | |
| Update analytics | ✅ | |
| Synchronize data | ✅ |
2. OpenFeign - Declarative HTTP Client
2.1 What is Feign?
Feign (now OpenFeign) is a declarative HTTP client that allows defining REST calls via Java interfaces.
// Without Feign (RestTemplate)
String url = "http://user-service/api/users/" + userId;
User user = restTemplate.getForObject(url, User.class);
// With Feign (declarative)
User user = userClient.getUserById(userId);
2.2 Maven Dependencies
<dependencies>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Eureka Client (for discovery) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Resilience4j -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
</dependencies>
2.3 Enable Feign
package com.example.orderservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients // Enable Feign
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
2.4 Define a Feign Client
package com.example.orderservice.client;
import com.example.orderservice.dto.UserDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@FeignClient(name = "user-service") // Service name in Eureka
public interface UserClient {
@GetMapping("/api/users/{id}")
UserDto getUserById(@PathVariable("id") Long id);
@GetMapping("/api/users")
List<UserDto> getAllUsers();
@GetMapping("/api/users/{id}/exists")
Boolean userExists(@PathVariable("id") Long id);
}
2.5 DTO
package com.example.orderservice.dto;
public class UserDto {
private Long id;
private String username;
private String email;
private String firstName;
private String lastName;
// Constructors
public UserDto() {}
public UserDto(Long id, String username, String email,
String firstName, String lastName) {
this.id = id;
this.username = username;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
2.6 Usage
package com.example.orderservice.service;
import com.example.orderservice.client.UserClient;
import com.example.orderservice.client.ProductClient;
import com.example.orderservice.dto.UserDto;
import com.example.orderservice.dto.ProductDto;
import com.example.orderservice.model.Order;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final UserClient userClient;
private final ProductClient productClient;
private final OrderRepository orderRepository;
public OrderService(UserClient userClient,
ProductClient productClient,
OrderRepository orderRepository) {
this.userClient = userClient;
this.productClient = productClient;
this.orderRepository = orderRepository;
}
public Order createOrder(Long userId, Long productId, int quantity) {
// Synchronous call to User Service
UserDto user = userClient.getUserById(userId);
if (user == null) {
throw new RuntimeException("User not found: " + userId);
}
// Synchronous call to Product Service
ProductDto product = productClient.getProductById(productId);
if (product == null) {
throw new RuntimeException("Product not found: " + productId);
}
// Create the order
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setQuantity(quantity);
order.setTotalPrice(product.getPrice() * quantity);
order.setUserEmail(user.getEmail());
return orderRepository.save(order);
}
}
2.7 Feign Configuration
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 10000
loggerLevel: BASIC
user-service:
connectTimeout: 3000
readTimeout: 5000
loggerLevel: FULL
# Logging Feign
logging:
level:
com.example.orderservice.client: DEBUG
2.8 Feign Logging Levels
| Level | Description |
|---|---|
NONE | No log |
BASIC | Method, URL, status, time |
HEADERS | + Headers |
FULL | + Body request/response |
3. Resilience4j - Resilience Patterns
3.1 Why Resilience?
3.2 Dependencies
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!-- If you want the annotations @CircuitBreaker, @Retry, etc. -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
<!-- AOP for annotations -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
4. Circuit Breaker Pattern
4.1 Circuit States
4.2 YAML Configuration
resilience4j:
circuitbreaker:
configs:
default:
# Sliding window size
slidingWindowSize: 10
# Window type (COUNT_BASED or TIME_BASED)
slidingWindowType: COUNT_BASED
# Minimum number of calls before calculation
minimumNumberOfCalls: 5
# Failure threshold to open (%)
failureRateThreshold: 50
# Duration in OPEN state (ms)
waitDurationInOpenState: 10000
# Permitted calls in HALF_OPEN
permittedNumberOfCallsInHalfOpenState: 3
# Auto transition OPEN -> HALF_OPEN
automaticTransitionFromOpenToHalfOpenEnabled: true
# Exceptions considered as failures
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- feign.FeignException
# Ignored exceptions
ignoreExceptions:
- com.example.BusinessException
instances:
userService:
baseConfig: default
productService:
baseConfig: default
failureRateThreshold: 30
4.3 With Annotations
package com.example.orderservice.service;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
@Service
public class UserServiceClient {
private final UserClient userClient;
public UserServiceClient(UserClient userClient) {
this.userClient = userClient;
}
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public UserDto getUser(Long userId) {
return userClient.getUserById(userId);
}
// Fallback - MUST have the same signature + Exception
public UserDto getUserFallback(Long userId, Exception ex) {
// Log the error
log.warn("Circuit breaker triggered for user {}. Error: {}",
userId, ex.getMessage());
// Return a default or cached value
return new UserDto(userId, "unknown", "unknown@example.com",
"Unknown", "User");
}
}
4.4 With Feign (Fallback Factory)
package com.example.orderservice.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(
name = "user-service",
fallbackFactory = UserClientFallbackFactory.class
)
public interface UserClient {
@GetMapping("/api/users/{id}")
UserDto getUserById(@PathVariable("id") Long id);
@GetMapping("/api/users/{id}/exists")
Boolean userExists(@PathVariable("id") Long id);
}
package com.example.orderservice.client;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
private static final Logger log = LoggerFactory.getLogger(UserClientFallbackFactory.class);
@Override
public UserClient create(Throwable cause) {
log.error("UserClient fallback triggered. Cause: {}", cause.getMessage());
return new UserClient() {
@Override
public UserDto getUserById(Long id) {
return new UserDto(id, "fallback-user", "fallback@example.com",
"Fallback", "User");
}
@Override
public Boolean userExists(Long id) {
return false; // Pessimistic in case of error
}
};
}
}
Enable Feign fallbacks:
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true
5. Retry Pattern
5.1 Configuration
resilience4j:
retry:
configs:
default:
maxAttempts: 3
waitDuration: 1000
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- feign.RetryableException
ignoreExceptions:
- com.example.BusinessException
instances:
userService:
baseConfig: default
maxAttempts: 5
5.2 With Annotations
@Service
public class ProductServiceClient {
@Retry(name = "productService", fallbackMethod = "getProductFallback")
public ProductDto getProduct(Long productId) {
return productClient.getProductById(productId);
}
public ProductDto getProductFallback(Long productId, Exception ex) {
log.warn("Retry exhausted for product {}. Error: {}",
productId, ex.getMessage());
throw new ServiceUnavailableException("Product service unavailable");
}
}
5.3 Combine Retry and Circuit Breaker
@Service
public class UserServiceClient {
// Order matters! Retry THEN CircuitBreaker
@Retry(name = "userService")
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public UserDto getUser(Long userId) {
return userClient.getUserById(userId);
}
public UserDto getUserFallback(Long userId, Exception ex) {
return getCachedUser(userId);
}
}
Execution Order:
- Initial call
- If failure → Retry (up to maxAttempts)
- If all retries fail → Circuit Breaker
- If Circuit OPEN → Immediate fallback
6. Rate Limiter Pattern
6.1 Configuration
resilience4j:
ratelimiter:
configs:
default:
# Number of permissions per period
limitForPeriod: 10
# Duration of the period
limitRefreshPeriod: 1s
# Timeout waiting for a permission
timeoutDuration: 500ms
instances:
userService:
baseConfig: default
limitForPeriod: 100
limitRefreshPeriod: 1s
6.2 With Annotations
@Service
public class ExternalApiClient {
@RateLimiter(name = "externalApi", fallbackMethod = "rateLimitFallback")
public Response callExternalApi(Request request) {
return externalClient.call(request);
}
public Response rateLimitFallback(Request request, RequestNotPermitted ex) {
log.warn("Rate limit exceeded");
throw new TooManyRequestsException("Rate limit exceeded. Try again later.");
}
}
7. Bulkhead Pattern
7.1 Concept
The Bulkhead isolates resources to prevent a slow service from blocking everything.
7.2 Configuration
resilience4j:
bulkhead:
configs:
default:
maxConcurrentCalls: 10
maxWaitDuration: 500ms
instances:
userService:
baseConfig: default
maxConcurrentCalls: 5
thread-pool-bulkhead:
configs:
default:
maxThreadPoolSize: 10
coreThreadPoolSize: 5
queueCapacity: 20
instances:
productService:
baseConfig: default
7.3 With Annotations
@Service
public class OrderService {
@Bulkhead(name = "userService", type = Bulkhead.Type.SEMAPHORE)
public UserDto getUser(Long userId) {
return userClient.getUserById(userId);
}
@Bulkhead(name = "productService", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<ProductDto> getProductAsync(Long productId) {
return CompletableFuture.supplyAsync(() ->
productClient.getProductById(productId)
);
}
}
8. Time Limiter Pattern
8.1 Configuration
resilience4j:
timelimiter:
configs:
default:
timeoutDuration: 5s
cancelRunningFuture: true
instances:
userService:
baseConfig: default
timeoutDuration: 3s
8.2 With Annotations
@Service
public class SlowServiceClient {
@TimeLimiter(name = "slowService", fallbackMethod = "timeoutFallback")
public CompletableFuture<Response> callSlowService() {
return CompletableFuture.supplyAsync(() -> {
// Potentially slow call
return slowClient.call();
});
}
public CompletableFuture<Response> timeoutFallback(TimeoutException ex) {
return CompletableFuture.completedFuture(Response.timeout());
}
}
9. Combine All Patterns
9.1 Complete Configuration
resilience4j:
circuitbreaker:
instances:
userService:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 3
retry:
instances:
userService:
maxAttempts: 3
waitDuration: 500ms
enableExponentialBackoff: true
ratelimiter:
instances:
userService:
limitForPeriod: 100
limitRefreshPeriod: 1s
bulkhead:
instances:
userService:
maxConcurrentCalls: 10
timelimiter:
instances:
userService:
timeoutDuration: 3s
9.2 Service with All Patterns
@Service
@Slf4j
public class ResilientUserService {
private final UserClient userClient;
public ResilientUserService(UserClient userClient) {
this.userClient = userClient;
}
// Execution order: Bulkhead → TimeLimiter → RateLimiter → CircuitBreaker → Retry
@Bulkhead(name = "userService")
@TimeLimiter(name = "userService")
@RateLimiter(name = "userService")
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
@Retry(name = "userService")
public CompletableFuture<UserDto> getUser(Long userId) {
return CompletableFuture.supplyAsync(() -> {
log.info("Calling user service for user: {}", userId);
return userClient.getUserById(userId);
});
}
public CompletableFuture<UserDto> getUserFallback(Long userId, Exception ex) {
log.warn("Fallback triggered for user: {}. Error: {}",
userId, ex.getMessage());
return CompletableFuture.completedFuture(
new UserDto(userId, "fallback", "fallback@example.com", "Fallback", "User")
);
}
}
10. Monitoring Resilience4j
10.1 Actuator Endpoints
management:
endpoints:
web:
exposure:
include: health,circuitbreakers,circuitbreakerevents,retries,ratelimiters
endpoint:
health:
show-details: always
health:
circuitbreakers:
enabled: true
ratelimiters:
enabled: true
10.2 Available Endpoints
# Circuit breakers state
curl http://localhost:8083/actuator/circuitbreakers
# Circuit breakers events
curl http://localhost:8083/actuator/circuitbreakerevents
# Prometheus metrics
curl http://localhost:8083/actuator/prometheus | grep resilience4j
10.3 Example Response
{
"circuitBreakers": {
"userService": {
"failureRate": "20.0%",
"slowCallRate": "0.0%",
"state": "CLOSED",
"bufferedCalls": 10,
"failedCalls": 2,
"slowCalls": 0,
"notPermittedCalls": 0
}
}
}
11. Summary
Recap Table
| Pattern | Problem Solved | When to Use |
|---|---|---|
| Circuit Breaker | Failure cascade | Unstable external calls |
| Retry | Transient errors | Network timeouts, 503 |
| Rate Limiter | Overload | External APIs with quotas |
| Bulkhead | Resource exhaustion | Isolate slow services |
| Time Limiter | Blocking calls | Potentially slow services |
Patterns Order
Request → Bulkhead → TimeLimiter → RateLimiter → CircuitBreaker → Retry → Service
Recommended Production Configuration
resilience4j:
circuitbreaker:
instances:
default:
slidingWindowSize: 100
failureRateThreshold: 50
waitDurationInOpenState: 30000
permittedNumberOfCallsInHalfOpenState: 10
retry:
instances:
default:
maxAttempts: 3
waitDuration: 1000ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
ratelimiter:
instances:
default:
limitForPeriod: 1000
limitRefreshPeriod: 1s
12. Validation Quiz
Question 1
What is the difference between @FeignClient(fallback) and @FeignClient(fallbackFactory)?
See the answer
- fallback: Static class, no access to the exception
- fallbackFactory: Receives the exception as a parameter, allows logging or varying the fallback based on the error
Question 2
In what order do Retry and CircuitBreaker execute?
See the answer
Retry executes FIRST (closer to the service), then CircuitBreaker.
If all retries fail, then the CircuitBreaker counts a failure. If the CircuitBreaker is OPEN, retries are not even attempted.