Calling Another Service with RestClient

RestClient

RestClient (Spring Framework 6.1, Spring Boot 3.2+) is the modern, fluent, synchronous HTTP client. It replaces RestTemplate for new code. Spring Boot auto-configures a RestClient.Builder bean that already has Jackson (JSON) set up, so you inject the builder and set a base URL.

ClientStyleUse it for
RestTemplateOld template methodsExisting code only (maintenance mode)
RestClientFluent, blockingNew Spring MVC services (this course)
WebClientFluent, reactiveSpring WebFlux / non-blocking apps
OpenFeign / HTTP InterfaceDeclarative interfaceMedium level (Inter-service Communication)

The Product Client in order-service

package com.shopease.order.client;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import java.math.BigDecimal;

@Component
public class ProductClient {

    private final RestClient restClient;

    public ProductClient(RestClient.Builder builder,
                         @Value("${shopease.services.product-url}") String productUrl) {
        this.restClient = builder.baseUrl(productUrl).build();
    }

    /** order-service's OWN copy of the response shape: only the fields it needs. */
    public record ProductResponse(Long id, String name, BigDecimal price, int stock) {}
    public record StockRequest(int quantity) {}

    public ProductResponse getProduct(Long id) {
        return restClient.get()
                .uri("/api/products/{id}", id)
                .retrieve()
                .body(ProductResponse.class);
    }

    public ProductResponse reserve(Long id, int quantity) {
        return restClient.post()
                .uri("/api/products/{id}/reserve", id)
                .body(new StockRequest(quantity))
                .retrieve()
                .body(ProductResponse.class);
    }
}

Note: order-service does not import a class from product-service. It defines its own ProductResponse record. Sharing a JAR of DTOs between services couples their releases; copying a small record keeps them independent.

Placing an Order

package com.shopease.order;

import com.shopease.order.client.ProductClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class OrderService {

    private final ProductClient productClient;
    private final OrderRepository orders;

    public OrderService(ProductClient productClient, OrderRepository orders) {
        this.productClient = productClient;
        this.orders = orders;
    }

    public record ItemRequest(Long productId, int quantity) {}
    public record PlaceOrderRequest(Long userId, List<ItemRequest> items) {}

    @Transactional
    public Order place(PlaceOrderRequest req) {
        Order order = new Order(req.userId());
        for (ItemRequest item : req.items()) {
            // 1 HTTP call: reserve stock and get the current name/price
            var product = productClient.reserve(item.productId(), item.quantity());
            order.addItem(product.id(), product.name(), product.price(), item.quantity());
        }
        order.setStatus(OrderStatus.CREATED);
        return orders.save(order);   // stored in order_db only
    }
}

The Controller and the Result

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final OrderService service;

    public OrderController(OrderService service) { this.service = service; }

    @PostMapping
    public ResponseEntity<OrderResponse> place(@RequestBody OrderService.PlaceOrderRequest req) {
        Order saved = service.place(req);
        return ResponseEntity.created(URI.create("/api/orders/" + saved.getId()))
                .body(OrderResponse.from(saved));
    }
}

Example

With both services running:

curl -i -X POST localhost:8082/api/orders \
  -H "Content-Type: application/json" \
  -d '{"userId":7,"items":[{"productId":1,"quantity":2},{"productId":2,"quantity":1}]}'

Example output

HTTP/1.1 201 Created
Location: /api/orders/1
Content-Type: application/json

{"id":1,"userId":7,"status":"CREATED","total":3897.00,
 "items":[{"productId":1,"productName":"boAt Rockerz 450","unitPrice":1499.00,"quantity":2},
          {"productId":2,"productName":"Prestige Kettle","unitPrice":899.00,"quantity":1}]}

# product-service log
POST /api/products/1/reserve  200
POST /api/products/2/reserve  200

Practice

Exercise: Add a method listByCategory(String category) to ProductClient that calls GET /api/products?category={category} and returns a List<ProductResponse>.

Show answer
import org.springframework.core.ParameterizedTypeReference;

public List<ProductResponse> listByCategory(String category) {
    return restClient.get()
            .uri(uri -> uri.path("/api/products")
                           .queryParam("category", category)
                           .build())
            .retrieve()
            .body(new ParameterizedTypeReference<List<ProductResponse>>() {});
}