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)