DEV Community

Cover image for How Microservices Talk to Each Other Using WebClient
Pabodha Wanniarachchi
Pabodha Wanniarachchi

Posted on

How Microservices Talk to Each Other Using WebClient

What is a Microservice?

Instead of building one big app that does everything, you split it into small independent services. Each service does one job and runs separately.

I have two services:

  • product-service - manages products (port 8081)
  • order-service - manages orders (port 8082)

These two services live in separate Spring Boot apps with separate databases. But when a customer places an order, order-service needs to know about the product — so they need to talk to each other.


The Problem

Order-service doesn't have product data. It only knows the productId from the request. So before saving an order it needs to ask product-service:

"Hey, does this product exist? How much stock do you have?"


The Solution - WebClient

WebClient is Spring's HTTP client that lets one service call another service's REST API.

Step 1 - Configure WebClient as a Bean

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient productWebClient() {
        return WebClient.builder()
                .baseUrl("http://localhost:8081/api/products")
                .build();
    }
}
Enter fullscreen mode Exit fullscreen mode

You set the base URL once here. Now anywhere you inject productWebClient it already knows where product-service lives. Notice the bean is named productWebClient — this matches the field name in the service so Spring knows which WebClient to inject.


Step 2 - Inject and Use It in OrderService

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final OrderMapper orderMapper;
    private final WebClient productWebClient; // injected from config
Enter fullscreen mode Exit fullscreen mode

@RequiredArgsConstructor from Lombok automatically creates the constructor and injects all final fields — including productWebClient.


Step 3 - Call product-service to fetch product

ProductResponse product = productWebClient.get()
        .uri(uriBuilder -> uriBuilder.path("/{id}").build(itemId))
        .retrieve()
        .bodyToMono(ProductResponse.class)
        .block();
Enter fullscreen mode Exit fullscreen mode

Breaking this down line by line:

productWebClient.get()
Enter fullscreen mode Exit fullscreen mode

Make a GET request. Combined with baseUrl it calls GET http://localhost:8081/api/products/{id}

.uri(uriBuilder -> uriBuilder.path("/{id}").build(itemId))
Enter fullscreen mode Exit fullscreen mode

Append the product id to the URL path safely. itemId replaces {id}.

.retrieve()
Enter fullscreen mode Exit fullscreen mode

Actually send the request and get the response.

.bodyToMono(ProductResponse.class)
Enter fullscreen mode Exit fullscreen mode

Convert the JSON response body into a ProductResponse object. Mono means "a single value coming in the future" — WebClient is async by nature.

.block()
Enter fullscreen mode Exit fullscreen mode

Wait for the result synchronously. Since we need the product data before continuing, we block here.


Step 4 - Use the product data to build the order

// check if enough stock
if (product.getStockQuantity() < orderRequest.getQuantity()) {
    throw new Exception("Insufficient stock Available: " + product.getStockQuantity());
}

// calculate total price from product price × quantity
BigDecimal totalPrice = product.getPrice()
        .multiply(BigDecimal.valueOf(orderRequest.getQuantity()));

// save order
Order order = orderMapper.toEntity(orderRequest);
order.setTotalPrice(totalPrice);
order.setStatus(Order.OrderStatus.PENDING);
order.setCreatedAt(LocalDateTime.now());
order.setUpdatedAt(LocalDateTime.now());

return orderMapper.toResponse(orderRepository.save(order));
Enter fullscreen mode Exit fullscreen mode

The totalPrice is calculated automatically from product-service data — the client only sends productId and quantity. Everything else comes from the inter-service call.


The Full Flow Visualized

Client (Postman)
      |
      | POST /api/orders
      ↓
 order-service (8082)
      |
      | GET /api/products/{id}   ← WebClient call
      ↓
 product-service (8081)
      |
      | returns ProductResponse (name, price, stock)
      ↓
 order-service
      |
      | checks stock, calculates price, saves order
      ↓
 returns OrderResponse to client ✅
Enter fullscreen mode Exit fullscreen mode

About the ENUM — @JdbcTypeCode(SqlTypes.NAMED_ENUM)

PostgreSQL supports custom ENUM types. In your migration you created:

CREATE TYPE order_status AS ENUM ('PENDING', 'CONFIRMED', 'CANCELLED');
Enter fullscreen mode Exit fullscreen mode

By default Hibernate saves enums as plain strings, but PostgreSQL's custom ENUM type is stricter, it refuses plain strings. The annotation tells Hibernate to treat this field as a PostgreSQL named ENUM type so they speak the same language:

@Enumerated(EnumType.STRING)
@JdbcTypeCode(SqlTypes.NAMED_ENUM)
private OrderStatus status;
Enter fullscreen mode Exit fullscreen mode

Key Things to Remember

WebClient needs a base URL - set it once in config, reuse everywhere.

.block() makes it synchronous - we need the product data before saving, so we wait.

Services only share DTOs - order-service doesn't import product-service's entity or repository, only its ProductResponse DTO. Each service owns its own data.

Each service has its own database - order-service never directly queries product-service's database. It always goes through the REST API.

Top comments (0)