Skip to main content

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 CaseSynchronousAsynchronous
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​

LevelDescription
NONENo log
BASICMethod, 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:

  1. Initial call
  2. If failure → Retry (up to maxAttempts)
  3. If all retries fail → Circuit Breaker
  4. 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​

PatternProblem SolvedWhen to Use
Circuit BreakerFailure cascadeUnstable external calls
RetryTransient errorsNetwork timeouts, 503
Rate LimiterOverloadExternal APIs with quotas
BulkheadResource exhaustionIsolate slow services
Time LimiterBlocking callsPotentially slow services

Patterns Order​

Request → Bulkhead → TimeLimiter → RateLimiter → CircuitBreaker → Retry → Service
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.


Proceed to Practical Work →

Back to Module 13