Use Case: Internal API Calls with Manual Clients
About
This use case demonstrates how one Spring Boot service (Service 1) can invoke a GET API exposed by another internal service (Service 2) using WebClient
, a modern, non-blocking HTTP client introduced in Spring 5.
Service 2
Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
SampleController.java
package org.example.api;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SampleController {
@GetMapping("/api/service-2/details")
public ResponseEntity<String> getInterServiceDetails() {
return ResponseEntity.ok("Service 2 processed successfully");
}
}
application.yaml
server:
port: 8082
Build and run the application
Service 1
Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
SampleController.java
package org.example.api;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.service.SampleService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@Slf4j
@RequiredArgsConstructor
@RestController
public class SampleController {
private final SampleService sampleService;
@GetMapping("/api/service-1/details")
public Mono<String> getInterServiceDetails() {
return sampleService.fetchService2Details();
}
}
WebClientConfig.java
package org.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("http://localhost:8082") // Set the base URL for the requests
.defaultCookie("cookie-name", "cookie-value") // Set a default cookie for the requests
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) // Set a default header for the requests
.build();
}
}
SampleService.java
package org.example.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
@Slf4j
@RequiredArgsConstructor
@Service
public class SampleService {
private static final String SERVICE_2_ENDPOINT_PATH = "/api/service-2/details";
private final WebClient webClient;
// Method to retrieve service 2 details using a GET request
public Mono<String> fetchService2Details() {
log.info("Calling service 2 API via WebClient");
return webClient.get()
.uri(SERVICE_2_ENDPOINT_PATH)
.retrieve()
.bodyToMono(String.class)
.doOnNext(data -> log.info("Received data from Service-2: {}", data)) // Do logging after receiving response
.onErrorReturn("Failed to retrieve data from Service-2"); // Default error message
}
}
application.yaml
server:
port: 8081
logging:
level:
org.springframework.web.reactive.function.client: DEBUG
reactor.netty.http.client: DEBUG
Build and run the application
Verification
Execute the service 1 API via Postman

Monitor the logs (have a look at the threads executing the code)

Handling error based on Http status
Sample Code to Create an employee, Fetch all employees and handling error based on Http status
package org.example.service;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RequiredArgsConstructor
@Service
public class EmployeeService {
private final WebClient webClient;
public Flux<Employee> getAllEmployees() {
return webClient.get()
.uri("/employees")
.retrieve()
.onStatus(httpStatus -> !httpStatus.is2xxSuccessful(),
clientResponse -> handleErrorResponse(clientResponse.statusCode()))
.bodyToFlux(Employee.class)
.onErrorResume(Exception.class, e -> Flux.empty()); // Return an empty collection on error
}
public Mono<ResponseEntity<Employee>> createEmployee(Employee newEmployee) {
return webClient.post()
.uri("/employees")
.body(Mono.just(newEmployee), Employee.class)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> {
//logError("Client error occurred");
return Mono.error(new WebClientResponseException
(response.statusCode().value(), "Bad Request", null, null, null));
})
.onStatus(HttpStatus::is5xxServerError, response -> {
//logError("Server error occurred");
return Mono.error(new WebClientResponseException
(response.statusCode().value(), "Server Error", null, null, null));
})
.toEntity(Employee.class);
}
private Mono<? extends Throwable> handleErrorResponse(HttpStatus statusCode) {
// Handle non-success status codes
return Mono.error(new EmployeeServiceException("Failed to fetch employee. Status code: " + statusCode));
}
}
Last updated