DEV Community

Dev Cookies
Dev Cookies

Posted on

Calling REST Services in Spring Boot: Complete CRUD Operations With and Without Feign Client

Introduction

In modern microservices architecture, service-to-service communication is fundamental to building distributed applications. Services need to communicate with each other to perform full CRUD (Create, Read, Update, Delete) operations, exchange data, and maintain system cohesion. This communication typically happens over HTTP using REST APIs, where one service acts as a client calling another service's endpoints.

Spring Boot offers several approaches for making HTTP calls to external services. The most popular methods include using Spring Cloud OpenFeign for declarative REST clients, or traditional approaches like RestTemplate and WebClient for more programmatic control. Each approach has its own advantages and use cases.

This blog post will explore both approaches through a practical example with complete CRUD operations, helping you understand when and how to use each method effectively.

Scenario Setup

Let's consider a comprehensive microservices scenario where we have:

  • OrderService: Manages customer orders and needs to perform CRUD operations on users
  • UserService: Handles user information and profiles with full REST API

Our example will demonstrate all CRUD operations:

  1. CREATE: Adding new users
  2. READ: Fetching user details by ID and listing all users
  3. UPDATE: Modifying existing user information
  4. DELETE: Removing users from the system

The UserService exposes these REST endpoints:

  • POST /api/users - Create a new user
  • GET /api/users/{userId} - Get user by ID
  • GET /api/users - Get all users
  • PUT /api/users/{userId} - Update user by ID
  • DELETE /api/users/{userId} - Delete user by ID

Approach 1 – Using Spring Cloud OpenFeign

Spring Cloud OpenFeign provides a declarative approach to create REST clients. It generates the implementation at runtime based on interface annotations, reducing boilerplate code significantly.

Dependencies

Maven (pom.xml):

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2023.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> 
Enter fullscreen mode Exit fullscreen mode

Gradle (build.gradle):

dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' implementation 'org.springframework.boot:spring-boot-starter-validation' } dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.0" } } 
Enter fullscreen mode Exit fullscreen mode

Main Application Class

package com.example.orderservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableFeignClients public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } } 
Enter fullscreen mode Exit fullscreen mode

User Models

package com.example.orderservice.model; import com.fasterxml.jackson.annotation.JsonFormat; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; import java.time.LocalDateTime; public class User { private Long id; @NotBlank(message = "Name is required") @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") private String name; @NotBlank(message = "Email is required") @Email(message = "Email should be valid") private String email; private String phone; private String address; @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") private LocalDateTime createdAt; @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") private LocalDateTime updatedAt; // Constructors public User() {} public User(String name, String email, String phone, String address) { this.name = name; this.email = email; this.phone = phone; this.address = address; this.createdAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now(); } // Getters and Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; this.updatedAt = LocalDateTime.now(); } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; this.updatedAt = LocalDateTime.now(); } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; this.updatedAt = LocalDateTime.now(); } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; this.updatedAt = LocalDateTime.now(); } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } @Override public String toString() { return "User{id=" + id + ", name='" + name + "', email='" + email + "'}"; } } 
Enter fullscreen mode Exit fullscreen mode
package com.example.orderservice.dto; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; public class CreateUserRequest { @NotBlank(message = "Name is required") @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") private String name; @NotBlank(message = "Email is required") @Email(message = "Email should be valid") private String email; private String phone; private String address; // Constructors public CreateUserRequest() {} public CreateUserRequest(String name, String email, String phone, String address) { this.name = name; this.email = email; this.phone = phone; this.address = address; } // Getters and Setters public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } } 
Enter fullscreen mode Exit fullscreen mode
package com.example.orderservice.dto; import javax.validation.constraints.Email; import javax.validation.constraints.Size; public class UpdateUserRequest { @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") private String name; @Email(message = "Email should be valid") private String email; private String phone; private String address; // Constructors public UpdateUserRequest() {} // Getters and Setters public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } } 
Enter fullscreen mode Exit fullscreen mode

Feign Client Interface with Full CRUD

package com.example.orderservice.client; import com.example.orderservice.dto.CreateUserRequest; import com.example.orderservice.dto.UpdateUserRequest; import com.example.orderservice.model.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; import java.util.List; @FeignClient(name = "user-service", url = "${user-service.url}") public interface UserServiceClient { @PostMapping("/api/users") User createUser(@RequestBody CreateUserRequest request); @GetMapping("/api/users/{userId}") User getUserById(@PathVariable("userId") Long userId); @GetMapping("/api/users") List<User> getAllUsers(); @GetMapping("/api/users") List<User> getAllUsers(@RequestParam(value = "page", defaultValue = "0") int page, @RequestParam(value = "size", defaultValue = "10") int size); @PutMapping("/api/users/{userId}") User updateUser(@PathVariable("userId") Long userId, @RequestBody UpdateUserRequest request); @DeleteMapping("/api/users/{userId}") void deleteUser(@PathVariable("userId") Long userId); } 
Enter fullscreen mode Exit fullscreen mode

Service Class Using Feign Client - Full CRUD Operations

package com.example.orderservice.service; import com.example.orderservice.client.UserServiceClient; import com.example.orderservice.dto.CreateUserRequest; import com.example.orderservice.dto.UpdateUserRequest; import com.example.orderservice.model.User; import feign.FeignException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class OrderService { private static final Logger logger = LoggerFactory.getLogger(OrderService.class); @Autowired private UserServiceClient userServiceClient; // CREATE Operation public User createUser(CreateUserRequest request) { try { User createdUser = userServiceClient.createUser(request); logger.info("Successfully created user: {}", createdUser.getName()); return createdUser; } catch (FeignException.BadRequest e) { logger.error("Invalid user data: {}", e.getMessage()); throw new RuntimeException("Invalid user data provided"); } catch (FeignException.Conflict e) { logger.error("User already exists: {}", e.getMessage()); throw new RuntimeException("User with this email already exists"); } catch (FeignException e) { logger.error("Error creating user: {}", e.getMessage()); throw new RuntimeException("Failed to create user due to service error"); } } // READ Operations public User getUserById(Long userId) { try { User user = userServiceClient.getUserById(userId); logger.info("Retrieved user: {}", user.getName()); return user; } catch (FeignException.NotFound e) { logger.error("User not found for ID: {}", userId); throw new RuntimeException("User not found with ID: " + userId); } catch (FeignException e) { logger.error("Error retrieving user: {}", e.getMessage()); throw new RuntimeException("Failed to retrieve user due to service error"); } } public List<User> getAllUsers() { try { List<User> users = userServiceClient.getAllUsers(); logger.info("Retrieved {} users", users.size()); return users; } catch (FeignException e) { logger.error("Error retrieving users: {}", e.getMessage()); throw new RuntimeException("Failed to retrieve users due to service error"); } } public List<User> getAllUsers(int page, int size) { try { List<User> users = userServiceClient.getAllUsers(page, size); logger.info("Retrieved {} users for page {} with size {}", users.size(), page, size); return users; } catch (FeignException e) { logger.error("Error retrieving paginated users: {}", e.getMessage()); throw new RuntimeException("Failed to retrieve users due to service error"); } } // UPDATE Operation public User updateUser(Long userId, UpdateUserRequest request) { try { User updatedUser = userServiceClient.updateUser(userId, request); logger.info("Successfully updated user: {}", updatedUser.getName()); return updatedUser; } catch (FeignException.NotFound e) { logger.error("User not found for update, ID: {}", userId); throw new RuntimeException("User not found with ID: " + userId); } catch (FeignException.BadRequest e) { logger.error("Invalid update data: {}", e.getMessage()); throw new RuntimeException("Invalid user data provided for update"); } catch (FeignException e) { logger.error("Error updating user: {}", e.getMessage()); throw new RuntimeException("Failed to update user due to service error"); } } // DELETE Operation public void deleteUser(Long userId) { try { userServiceClient.deleteUser(userId); logger.info("Successfully deleted user with ID: {}", userId); } catch (FeignException.NotFound e) { logger.error("User not found for deletion, ID: {}", userId); throw new RuntimeException("User not found with ID: " + userId); } catch (FeignException e) { logger.error("Error deleting user: {}", e.getMessage()); throw new RuntimeException("Failed to delete user due to service error"); } } // Business Logic Methods public void processOrderWithUserCreation(Long orderId, CreateUserRequest userRequest) { User user = createUser(userRequest); logger.info("Processing order {} for newly created user: {}", orderId, user.getId()); // Order processing logic here } public void processOrderWithUserValidation(Long orderId, Long userId) { User user = getUserById(userId); validateUser(user); logger.info("Processing order {} for validated user: {}", orderId, user.getName()); // Order processing logic here } private void validateUser(User user) { if (user.getEmail() == null || user.getEmail().isEmpty()) { throw new RuntimeException("User email is required for order processing"); } if (user.getName() == null || user.getName().trim().isEmpty()) { throw new RuntimeException("User name is required for order processing"); } } } 
Enter fullscreen mode Exit fullscreen mode

REST Controller for Testing

package com.example.orderservice.controller; import com.example.orderservice.dto.CreateUserRequest; import com.example.orderservice.dto.UpdateUserRequest; import com.example.orderservice.model.User; import com.example.orderservice.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.List; @RestController @RequestMapping("/api/orders") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/users") public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) { User user = orderService.createUser(request); return new ResponseEntity<>(user, HttpStatus.CREATED); } @GetMapping("/users/{userId}") public ResponseEntity<User> getUserById(@PathVariable Long userId) { User user = orderService.getUserById(userId); return ResponseEntity.ok(user); } @GetMapping("/users") public ResponseEntity<List<User>> getAllUsers( @RequestParam(value = "page", defaultValue = "0") int page, @RequestParam(value = "size", defaultValue = "10") int size) { List<User> users = orderService.getAllUsers(page, size); return ResponseEntity.ok(users); } @PutMapping("/users/{userId}") public ResponseEntity<User> updateUser(@PathVariable Long userId, @Valid @RequestBody UpdateUserRequest request) { User user = orderService.updateUser(userId, request); return ResponseEntity.ok(user); } @DeleteMapping("/users/{userId}") public ResponseEntity<Void> deleteUser(@PathVariable Long userId) { orderService.deleteUser(userId); return ResponseEntity.noContent().build(); } } 
Enter fullscreen mode Exit fullscreen mode

Configuration Properties

application.yml:

server: port: 8080 user-service: url: http://localhost:8081 feign: client: config: user-service: connect-timeout: 5000 read-timeout: 10000 logger-level: full error-decoder: feign.codec.ErrorDecoder.Default logging: level: com.example.orderservice.client: DEBUG feign: DEBUG 
Enter fullscreen mode Exit fullscreen mode

Approach 2 – Without Feign (Plain Spring)

Using RestTemplate - Full CRUD

RestTemplate is the traditional synchronous HTTP client in Spring. Although in maintenance mode since Spring 5, it's still widely used and supported.

Configuration Class

package com.example.orderservice.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; import org.springframework.boot.web.client.RestTemplateBuilder; import java.time.Duration; @Configuration public class RestConfig { @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder .setConnectTimeout(Duration.ofSeconds(5)) .setReadTimeout(Duration.ofSeconds(10)) .build(); } } 
Enter fullscreen mode Exit fullscreen mode

Service Implementation with RestTemplate - Full CRUD

package com.example.orderservice.service; import com.example.orderservice.dto.CreateUserRequest; import com.example.orderservice.dto.UpdateUserRequest; import com.example.orderservice.model.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import java.util.List; @Service public class OrderServiceRestTemplate { private static final Logger logger = LoggerFactory.getLogger(OrderServiceRestTemplate.class); @Autowired private RestTemplate restTemplate; @Value("${user-service.url}") private String userServiceUrl; // CREATE Operation public User createUser(CreateUserRequest request) { try { String url = userServiceUrl + "/api/users"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<CreateUserRequest> entity = new HttpEntity<>(request, headers); ResponseEntity<User> response = restTemplate.exchange( url, HttpMethod.POST, entity, User.class); User createdUser = response.getBody(); logger.info("Successfully created user: {}", createdUser.getName()); return createdUser; } catch (HttpClientErrorException.BadRequest e) { logger.error("Invalid user data: {}", e.getMessage()); throw new RuntimeException("Invalid user data provided"); } catch (HttpClientErrorException.Conflict e) { logger.error("User already exists: {}", e.getMessage()); throw new RuntimeException("User with this email already exists"); } catch (ResourceAccessException e) { logger.error("Connection error to user service: {}", e.getMessage()); throw new RuntimeException("User service unavailable"); } catch (Exception e) { logger.error("Unexpected error creating user: {}", e.getMessage()); throw new RuntimeException("Failed to create user"); } } // READ Operations public User getUserById(Long userId) { try { String url = userServiceUrl + "/api/users/" + userId; User user = restTemplate.getForObject(url, User.class); if (user != null) { logger.info("Retrieved user: {}", user.getName()); return user; } else { throw new RuntimeException("User not found"); } } catch (HttpClientErrorException.NotFound e) { logger.error("User not found for ID: {}", userId); throw new RuntimeException("User not found with ID: " + userId); } catch (ResourceAccessException e) { logger.error("Connection error to user service: {}", e.getMessage()); throw new RuntimeException("User service unavailable"); } catch (Exception e) { logger.error("Unexpected error retrieving user: {}", e.getMessage()); throw new RuntimeException("Failed to retrieve user"); } } public List<User> getAllUsers() { try { String url = userServiceUrl + "/api/users"; ResponseEntity<List<User>> response = restTemplate.exchange( url, HttpMethod.GET, null, new ParameterizedTypeReference<List<User>>() {}); List<User> users = response.getBody(); logger.info("Retrieved {} users", users != null ? users.size() : 0); return users; } catch (ResourceAccessException e) { logger.error("Connection error to user service: {}", e.getMessage()); throw new RuntimeException("User service unavailable"); } catch (Exception e) { logger.error("Unexpected error retrieving users: {}", e.getMessage()); throw new RuntimeException("Failed to retrieve users"); } } public List<User> getAllUsers(int page, int size) { try { String url = userServiceUrl + "/api/users?page=" + page + "&size=" + size; ResponseEntity<List<User>> response = restTemplate.exchange( url, HttpMethod.GET, null, new ParameterizedTypeReference<List<User>>() {}); List<User> users = response.getBody(); logger.info("Retrieved {} users for page {} with size {}", users != null ? users.size() : 0, page, size); return users; } catch (ResourceAccessException e) { logger.error("Connection error to user service: {}", e.getMessage()); throw new RuntimeException("User service unavailable"); } catch (Exception e) { logger.error("Unexpected error retrieving paginated users: {}", e.getMessage()); throw new RuntimeException("Failed to retrieve users"); } } // UPDATE Operation public User updateUser(Long userId, UpdateUserRequest request) { try { String url = userServiceUrl + "/api/users/" + userId; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<UpdateUserRequest> entity = new HttpEntity<>(request, headers); ResponseEntity<User> response = restTemplate.exchange( url, HttpMethod.PUT, entity, User.class); User updatedUser = response.getBody(); logger.info("Successfully updated user: {}", updatedUser.getName()); return updatedUser; } catch (HttpClientErrorException.NotFound e) { logger.error("User not found for update, ID: {}", userId); throw new RuntimeException("User not found with ID: " + userId); } catch (HttpClientErrorException.BadRequest e) { logger.error("Invalid update data: {}", e.getMessage()); throw new RuntimeException("Invalid user data provided for update"); } catch (ResourceAccessException e) { logger.error("Connection error to user service: {}", e.getMessage()); throw new RuntimeException("User service unavailable"); } catch (Exception e) { logger.error("Unexpected error updating user: {}", e.getMessage()); throw new RuntimeException("Failed to update user"); } } // DELETE Operation public void deleteUser(Long userId) { try { String url = userServiceUrl + "/api/users/" + userId; restTemplate.delete(url); logger.info("Successfully deleted user with ID: {}", userId); } catch (HttpClientErrorException.NotFound e) { logger.error("User not found for deletion, ID: {}", userId); throw new RuntimeException("User not found with ID: " + userId); } catch (ResourceAccessException e) { logger.error("Connection error to user service: {}", e.getMessage()); throw new RuntimeException("User service unavailable"); } catch (Exception e) { logger.error("Unexpected error deleting user: {}", e.getMessage()); throw new RuntimeException("Failed to delete user"); } } } 
Enter fullscreen mode Exit fullscreen mode

Using WebClient - Full CRUD

WebClient is the modern reactive HTTP client introduced in Spring 5, designed for both synchronous and asynchronous operations.

Configuration Class

package com.example.orderservice.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; import java.time.Duration; @Configuration public class WebClientConfig { @Value("${user-service.url}") private String userServiceUrl; @Bean public WebClient webClient() { return WebClient.builder() .baseUrl(userServiceUrl) .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024)) .build(); } } 
Enter fullscreen mode Exit fullscreen mode

Service Implementation with WebClient - Full CRUD

package com.example.orderservice.service; import com.example.orderservice.dto.CreateUserRequest; import com.example.orderservice.dto.UpdateUserRequest; import com.example.orderservice.model.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; 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.Mono; import java.time.Duration; import java.util.List; @Service public class OrderServiceWebClient { private static final Logger logger = LoggerFactory.getLogger(OrderServiceWebClient.class); @Autowired private WebClient webClient; // CREATE Operation (Synchronous) public User createUser(CreateUserRequest request) { try { User createdUser = webClient.post() .uri("/api/users") .bodyValue(request) .retrieve() .bodyToMono(User.class) .timeout(Duration.ofSeconds(10)) .block(); logger.info("Successfully created user: {}", createdUser.getName()); return createdUser; } catch (WebClientResponseException.BadRequest e) { logger.error("Invalid user data: {}", e.getMessage()); throw new RuntimeException("Invalid user data provided"); } catch (WebClientResponseException.Conflict e) { logger.error("User already exists: {}", e.getMessage()); throw new RuntimeException("User with this email already exists"); } catch (Exception e) { logger.error("Error creating user: {}", e.getMessage()); throw new RuntimeException("Failed to create user due to service error"); } } // CREATE Operation (Reactive) public Mono<User> createUserReactive(CreateUserRequest request) { return webClient.post() .uri("/api/users") .bodyValue(request) .retrieve() .bodyToMono(User.class) .timeout(Duration.ofSeconds(10)) .doOnSuccess(user -> logger.info("Successfully created user: {}", user.getName())) .onErrorResume(WebClientResponseException.BadRequest.class, e -> { logger.error("Invalid user data: {}", e.getMessage()); return Mono.error(new RuntimeException("Invalid user data provided")); }) .onErrorResume(WebClientResponseException.Conflict.class, e -> { logger.error("User already exists: {}", e.getMessage()); return Mono.error(new RuntimeException("User with this email already exists")); }) .onErrorResume(Exception.class, e -> { logger.error("Error creating user: {}", e.getMessage()); return Mono.error(new RuntimeException("Failed to create user")); }); } // READ Operations public User getUserById(Long userId) { try { User user = webClient.get() .uri("/api/users/{userId}", userId) .retrieve() .bodyToMono(User.class) .timeout(Duration.ofSeconds(10)) .block(); if (user != null) { logger.info("Retrieved user: {}", user.getName()); return user; } else { throw new RuntimeException("User not found"); } } catch (WebClientResponseException.NotFound e) { logger.error("User not found for ID: {}", userId); throw new RuntimeException("User not found with ID: " + userId); } catch (Exception e) { logger.error("Error retrieving user: {}", e.getMessage()); throw new RuntimeException("Failed to retrieve user due to service error"); } } public Mono<User> getUserByIdReactive(Long userId) { return webClient.get() .uri("/api/users/{userId}", userId) .retrieve() .bodyToMono(User.class) .timeout(Duration.ofSeconds(10)) .doOnSuccess(user -> logger.info("Retrieved user: {}", user.getName())) .onErrorResume(WebClientResponseException.NotFound.class, e -> { logger.error("User not found for ID: {}", userId); return Mono.error(new RuntimeException("User not found with ID: " + userId)); }) .onErrorResume(Exception.class, e -> { logger.error("Error retrieving user: {}", e.getMessage()); return Mono.error(new RuntimeException("Failed to retrieve user")); }); } public List<User> getAllUsers() { try { List<User> users = webClient.get() .uri("/api/users") .retrieve() .bodyToMono(new ParameterizedTypeReference<List<User>>() {}) .timeout(Duration.ofSeconds(10)) .block(); logger.info("Retrieved {} users", users != null ? users.size() : 0); return users; } catch (Exception e) { logger.error("Error retrieving users: {}", e.getMessage()); throw new RuntimeException("Failed to retrieve users due to service error"); } } public Mono<List<User>> getAllUsersReactive() { return webClient.get() .uri("/api/users") .retrieve() .bodyToMono(new ParameterizedTypeReference<List<User>>() {}) .timeout(Duration.ofSeconds(10)) .doOnSuccess(users -> logger.info("Retrieved {} users", users != null ? users.size() : 0)) .onErrorResume(Exception.class, e -> { logger.error("Error retrieving users: {}", e.getMessage()); return Mono.error(new RuntimeException("Failed to retrieve users")); }); } public List<User> getAllUsers(int page, int size) { try { List<User> users = webClient.get() .uri(uriBuilder -> uriBuilder .path("/api/users") .queryParam("page", page) .queryParam("size", size) .build()) .retrieve() .bodyToMono(new ParameterizedTypeReference<List<User>>() {}) .timeout(Duration.ofSeconds(10)) .block(); logger.info("Retrieved {} users for page {} with size {}", users != null ? users.size() : 0, page, size); return users; } catch (Exception e) { logger.error("Error retrieving paginated users: {}", e.getMessage()); throw new RuntimeException("Failed to retrieve users due to service error"); } } // UPDATE Operation public User updateUser(Long userId, UpdateUserRequest request) { try { User updatedUser = webClient.put() .uri("/api/users/{userId}", userId) .bodyValue(request) .retrieve() .bodyToMono(User.class) .timeout(Duration.ofSeconds(10)) .block(); logger.info("Successfully updated user: {}", updatedUser.getName()); return updatedUser; } catch (WebClientResponseException.NotFound e) { logger.error("User not found for update, ID: {}", userId); throw new RuntimeException("User not found with ID: " + userId); } catch (WebClientResponseException.BadRequest e) { logger.error("Invalid update data: {}", e.getMessage()); throw new RuntimeException("Invalid user data provided for update"); } catch (Exception e) { logger.error("Error updating user: {}", e.getMessage()); throw new RuntimeException("Failed to update user due to service error"); } } public Mono<User> updateUserReactive(Long userId, UpdateUserRequest request) { return webClient.put() .uri("/api/users/{userId}", userId) .bodyValue(request) .retrieve() .bodyToMono(User.class) .timeout(Duration.ofSeconds(10)) .doOnSuccess(user -> logger.info("Successfully updated user: {}", user.getName())) .onErrorResume(WebClientResponseException.NotFound.class, e -> { logger.error("User not found for update, ID: {}", userId); return Mono.error(new RuntimeException("User not found with ID: " + userId)); }) .onErrorResume(WebClientResponseException.BadRequest.class, e -> { logger.error("Invalid update data: {}", e.getMessage()); return Mono.error(new RuntimeException("Invalid user data provided for update")); }) .onErrorResume(Exception.class, e -> { logger.error("Error updating user: {}", e.getMessage()); return Mono.error(new RuntimeException("Failed to update user")); }); } // DELETE Operation public void deleteUser(Long userId) { try { webClient.delete() .uri("/api/users/{userId}", userId) .retrieve() .toBodilessEntity() .timeout(Duration.ofSeconds(10)) .block(); logger.info("Successfully deleted user with ID: {}", userId); } catch (WebClientResponseException.NotFound e) { logger.error("User not found for deletion, ID: {}", userId); throw new RuntimeException("User not found with ID: " + userId); } catch (Exception e) { logger.error("Error deleting user: {}", e.getMessage()); throw new RuntimeException("Failed to delete user due to service error"); } } public Mono<Void> deleteUserReactive(Long userId) { return webClient.delete() .uri("/api/users/{userId}", userId) .retrieve() .toBodilessEntity() .timeout(Duration.ofSeconds(10)) .doOnSuccess(response -> logger.info("Successfully deleted user with ID: {}", userId)) .onErrorResume(WebClientResponseException.NotFound.class, e -> { logger.error("User not found for deletion, ID: {}", userId); return Mono.error(new RuntimeException("User not found with ID: " + userId)); }) .onErrorResume(Exception.class, e -> { logger.error("Error deleting user: {}", e.getMessage()); return Mono.error(new RuntimeException("Failed to delete user")); }) .then(); } } 
Enter fullscreen mode Exit fullscreen mode

Dependencies for WebClient

Add these to your pom.xml if using WebClient:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> 
Enter fullscreen mode Exit fullscreen mode

Error Handling Configuration

package com.example.orderservice.exception; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(RuntimeException.class) public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException e) { logger.error("Runtime exception: {}", e.getMessage()); Map<String, Object> error = new HashMap<>(); error.put("timestamp", LocalDateTime.now()); error.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value()); error.put("error", "Internal Server Error"); error.put("message", e.getMessage()); return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, Object>> handleValidationException(MethodArgumentNotValidException e) { Map<String, Object> error = new HashMap<>(); Map<String, String> validationErrors = new HashMap<>(); e.getBindingResult().getFieldErrors().forEach(fieldError -> { validationErrors.put(fieldError.getField(), fieldError.getDefaultMessage()); }); error.put("timestamp", LocalDateTime.now()); error.put("status", HttpStatus.BAD_REQUEST.value()); error.put("error", "Validation Failed"); error.put("validationErrors", validationErrors); return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); } } 
Enter fullscreen mode Exit fullscreen mode

Comparison Table - Full CRUD Operations

Aspect Spring Cloud OpenFeign RestTemplate WebClient
Code Complexity Low - declarative interfaces High - manual request building Medium - fluent API with reactive support
CRUD Implementation Simple method annotations Verbose HTTP method calls Clean functional style
Request Body Handling Automatic with @RequestBody Manual HttpEntity creation Automatic with bodyValue()
Response Deserialization Automatic JSON to POJO Manual with ParameterizedTypeReference Automatic with bodyToMono()
Error Handling Built-in FeignException types Manual HTTP status code checking Reactive error handling with onErrorResume
Pagination Support Simple @RequestParam mapping Manual URL parameter building Clean URI builder support
Performance (CRUD) Good - connection pooling Good - synchronous blocking Excellent - non-blocking I/O
Testing CRUD Operations Easy interface mocking Standard RestTemplate mocking Reactive testing with StepVerifier
Configuration Overhead Minimal - just @EnableFeignClients Medium - RestTemplate bean setup Medium - WebClient configuration
Reactive Support None - blocking operations only None - synchronous only Full reactive and blocking support
Bulk Operations Manual loop implementation Manual loop implementation Reactive streams with flatMap
Request Interceptors Built-in Feign interceptors RestTemplate interceptors WebClient filters
Timeout Configuration Declarative in properties Programmatic setup Per-request timeout support
Connection Pooling Automatic with HTTP client Manual configuration needed Built-in with Reactor Netty
Circuit Breaker Integration Native Spring Cloud support Manual Hystrix integration Manual resilience4j integration
Metrics & Monitoring Built-in Feign metrics Manual RestTemplate metrics Built-in WebClient metrics

Testing Examples

Testing Feign Client

package com.example.orderservice.service; import com.example.orderservice.client.UserServiceClient; import com.example.orderservice.dto.CreateUserRequest; import com.example.orderservice.model.User; import feign.FeignException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; @ExtendWith(MockitoExtension.class) public class OrderServiceTest { @Mock private UserServiceClient userServiceClient; @InjectMocks private OrderService orderService; @Test public void testCreateUser_Success() { // Given CreateUserRequest request = new CreateUserRequest("John Doe", "john@example.com", "1234567890", "123 Main St"); User expectedUser = new User("John Doe", "john@example.com", "1234567890", "123 Main St"); expectedUser.setId(1L); when(userServiceClient.createUser(any(CreateUserRequest.class))).thenReturn(expectedUser); // When User result = orderService.createUser(request); // Then assertNotNull(result); assertEquals("John Doe", result.getName()); assertEquals("john@example.com", result.getEmail()); verify(userServiceClient, times(1)).createUser(request); } @Test public void testGetUserById_UserNotFound() { // Given Long userId = 999L; when(userServiceClient.getUserById(userId)).thenThrow(FeignException.NotFound.class); // When & Then RuntimeException exception = assertThrows(RuntimeException.class, () -> { orderService.getUserById(userId); }); assertTrue(exception.getMessage().contains("User not found with ID: 999")); } @Test public void testDeleteUser_Success() { // Given Long userId = 1L; doNothing().when(userServiceClient).deleteUser(userId); // When orderService.deleteUser(userId); // Then verify(userServiceClient, times(1)).deleteUser(userId); } } 
Enter fullscreen mode Exit fullscreen mode

Integration Testing with WebClient

package com.example.orderservice.service; import com.example.orderservice.dto.CreateUserRequest; import com.example.orderservice.model.User; import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.web.reactive.function.client.WebClient; import reactor.test.StepVerifier; import java.io.IOException; import java.time.Duration; public class OrderServiceWebClientIntegrationTest { private MockWebServer mockWebServer; private OrderServiceWebClient orderService; private ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach void setup() throws IOException { mockWebServer = new MockWebServer(); mockWebServer.start(); WebClient webClient = WebClient.builder() .baseUrl(mockWebServer.url("/").toString()) .build(); orderService = new OrderServiceWebClient(); // Inject webClient using reflection or setter } @AfterEach void cleanup() throws IOException { mockWebServer.shutdown(); } @Test void testCreateUserReactive_Success() throws Exception { // Given CreateUserRequest request = new CreateUserRequest("Jane Doe", "jane@example.com", "0987654321", "456 Oak St"); User expectedUser = new User("Jane Doe", "jane@example.com", "0987654321", "456 Oak St"); expectedUser.setId(1L); mockWebServer.enqueue(new MockResponse() .setBody(objectMapper.writeValueAsString(expectedUser)) .addHeader("Content-Type", "application/json")); // When & Then StepVerifier.create(orderService.createUserReactive(request)) .expectNext(expectedUser) .verifyComplete(); } @Test void testGetUserByIdReactive_NotFound() { // Given mockWebServer.enqueue(new MockResponse().setResponseCode(404)); // When & Then StepVerifier.create(orderService.getUserByIdReactive(999L)) .expectErrorMatches(throwable -> throwable instanceof RuntimeException && throwable.getMessage().contains("User not found with ID: 999")) .verify(Duration.ofSeconds(5)); } } 
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

Connection Pooling Configuration

# application.yml feign: httpclient: enabled: true max-connections: 200 max-connections-per-route: 50 connection-timeout: 2000 connection-timer-repeat: 3000 # For WebClient spring: webflux: client: max-in-memory-size: 1MB 
Enter fullscreen mode Exit fullscreen mode

Custom Feign Configuration

@Configuration public class FeignConfiguration { @Bean public Retryer retryer() { return new Retryer.Default(100, 3000, 3); } @Bean public ErrorDecoder errorDecoder() { return new CustomErrorDecoder(); } @Bean public RequestInterceptor requestInterceptor() { return template -> { template.header("User-Agent", "OrderService/1.0"); template.header("Accept", "application/json"); }; } } 
Enter fullscreen mode Exit fullscreen mode

Conclusion

When to Choose Feign for CRUD Operations

Choose Spring Cloud OpenFeign when:

  • Building microservices with comprehensive CRUD operations
  • You want declarative, interface-based REST clients with minimal boilerplate
  • Your team prefers annotation-driven development
  • You need built-in load balancing, circuit breakers, and retry mechanisms
  • Working with synchronous request-response patterns across multiple services
  • Integration with Spring Cloud ecosystem is important
  • You want automatic request/response serialization without manual configuration

When to Choose RestTemplate for CRUD Operations

Choose RestTemplate when:

  • Working with legacy Spring applications requiring CRUD functionality
  • Your team is familiar with traditional Spring patterns
  • You need fine-grained control over HTTP request construction
  • Working with simple, straightforward CRUD operations
  • You're not ready to adopt reactive programming
  • Integration with existing RestTemplate-based code

When to Choose WebClient for CRUD Operations

Choose WebClient when:

  • Building reactive applications with non-blocking CRUD operations
  • You need both synchronous and asynchronous CRUD patterns
  • Performance and scalability are critical for high-volume operations
  • You want to handle streaming data or real-time updates
  • Building modern, high-throughput applications with complex data flows
  • You need advanced HTTP features and extensive customization options
  • Working with reactive Spring Security or other reactive components

Best Practices Summary

  1. Use Feign for declarative microservices communication with minimal effort
  2. Use RestTemplate for simple, traditional synchronous operations
  3. Use WebClient for modern, high-performance reactive applications
  4. Always implement proper error handling and timeout configurations
  5. Consider connection pooling for high-traffic applications
  6. Use appropriate testing strategies for each approach
  7. Monitor and measure performance to make informed decisions

The choice between these approaches depends on your specific architectural requirements, team expertise, performance needs, and whether you're building reactive or traditional Spring applications. Feign provides the most developer-friendly experience for comprehensive CRUD operations, while WebClient offers superior performance and flexibility for modern reactive applications.

Top comments (0)