Chuyển đến nội dung chính

Lesson 13: REST Client — Synchronous communication between Services

MicroProfile REST Client, type-safe REST calls, exception handling, timeout configuration, response caching, and retry policies.

💻 Programming — Lesson 12 Lesson 13: REST Client — Synchronous communication between Services

Quarkus Microservices: From Basics to Production

Part 4: Inter-service Communication

xdev.asia

Introduction

In microservices, services need to communicate with each other. Synchronous communication (REST Client) is suitable for the request-response pattern — when service A needs data immediately from service B. Quarkus provides a MicroProfile REST Client that helps call REST APIs type-safe, just like calling local methods.

MicroProfile REST Client

Dependency

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>

Declare the REST Client Interface

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;

@RegisterRestClient(configKey = "product-service")
@Path("/api/v1/products")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface ProductServiceClient {

    @GET
    List<ProductInfo> list(
        @QueryParam("page") int page,
        @QueryParam("size") int size);

    @GET @Path("/{id}")
    ProductInfo getById(@PathParam("id") Long id);

    @POST @Path("/{id}/reserve-stock")
    StockResult reserveStock(
        @PathParam("id") Long id,
        @QueryParam("quantity") int quantity);

    @POST @Path("/{id}/release-stock")
    void releaseStock(
        @PathParam("id") Long id,
        @QueryParam("quantity") int quantity);

    @GET @Path("/{id}/check-stock")
    StockInfo checkStock(
        @PathParam("id") Long id,
        @QueryParam("quantity") int quantity);
}

Configuration

# application.properties
quarkus.rest-client.product-service.url=http://localhost:8081
quarkus.rest-client.product-service.scope=jakarta.inject.Singleton

# Timeouts
quarkus.rest-client.product-service.connect-timeout=5000
quarkus.rest-client.product-service.read-timeout=10000

# Override URL cho mỗi environment
%dev.quarkus.rest-client.product-service.url=http://localhost:8081
%prod.quarkus.rest-client.product-service.url=http://product-service:8080

Use in Service

@ApplicationScoped
public class OrderService {

    @Inject
    @RestClient
    ProductServiceClient productClient;

    @Transactional
    public OrderDTO createOrder(String customerId,
                                CreateOrderRequest request) {
        Order order = new Order();
        order.customerId = customerId;

        for (var item : request.items()) {
            // Gọi Product Service
            ProductInfo product =
                productClient.getById(item.productId());

            if (product == null) {
                throw new BusinessException(400,
                    "Product " + item.productId() + " not found");
            }

            order.addItem(product.id(), product.name(),
                Money.vnd(product.price()), item.quantity());
        }

        order.persist();
        return OrderDTO.from(order);
    }
}

Exception Handling for REST Client

Response Exception Mapper

import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.WebApplicationException;

@Provider
public class ProductServiceExceptionMapper
        implements ResponseExceptionMapper<RuntimeException> {

    @Override
    public RuntimeException toThrowable(Response response) {
        int status = response.getStatus();

        return switch (status) {
            case 404 -> new ResourceNotFoundException(
                "Product", "unknown");
            case 400 -> {
                String body = response.readEntity(String.class);
                yield new BusinessException(400, body);
            }
            case 503 -> new ServiceUnavailableException(
                "Product Service is unavailable");
            default -> new WebApplicationException(
                "Product Service error: " + status, status);
        };
    }

    @Override
    public boolean handles(int status, jakarta.ws.rs.core.MultivaluedMap headers) {
        return status >= 400;
    }
}

Register on Client

@RegisterRestClient(configKey = "product-service")
@RegisterProvider(ProductServiceExceptionMapper.class)
@Path("/api/v1/products")
public interface ProductServiceClient {
    // ...
}

Request/Response Logging

import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.client.ClientResponseContext;
import jakarta.ws.rs.client.ClientResponseFilter;
import io.quarkus.logging.Log;

@Provider
public class RestClientLoggingFilter
        implements ClientRequestFilter, ClientResponseFilter {

    @Override
    public void filter(ClientRequestContext request) {
        Log.infof("→ REST Client: %s %s",
            request.getMethod(), request.getUri());
    }

    @Override
    public void filter(ClientRequestContext request,
                       ClientResponseContext response) {
        Log.infof("← REST Client: %s %s → %d",
            request.getMethod(), request.getUri(),
            response.getStatus());
    }
}

Headers & Custom Interceptors

Add Custom Headers

import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;

@Provider
public class CorrelationIdFilter
        implements ClientRequestFilter {

    @Inject
    io.vertx.core.http.HttpServerRequest serverRequest;

    @Override
    public void filter(ClientRequestContext ctx) {
        // Propagate correlation ID
        String correlationId = serverRequest
            .getHeader("X-Correlation-ID");
        if (correlationId == null) {
            correlationId = UUID.randomUUID().toString();
        }
        ctx.getHeaders().putSingle(
            "X-Correlation-ID", correlationId);

        // Thêm service identifier
        ctx.getHeaders().putSingle(
            "X-Source-Service", "order-service");
    }
}

Programmatic REST Client

When you need to create a client dynamically (URL changes):

import org.eclipse.microprofile.rest.client.RestClientBuilder;
import java.net.URI;

@ApplicationScoped
public class DynamicServiceCaller {

    public ProductInfo getProduct(String serviceUrl,
                                  Long productId) {
        ProductServiceClient client = RestClientBuilder
            .newBuilder()
            .baseUri(URI.create(serviceUrl))
            .connectTimeout(5, TimeUnit.SECONDS)
            .readTimeout(10, TimeUnit.SECONDS)
            .register(ProductServiceExceptionMapper.class)
            .build(ProductServiceClient.class);

        return client.getById(productId);
    }
}

Multipart & File Upload

import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.RestForm;
import jakarta.ws.rs.core.MediaType;

// Client interface
@RegisterRestClient(configKey = "storage-service")
@Path("/api/v1/files")
public interface StorageServiceClient {

    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    FileUploadResult upload(
        @RestForm("file") java.io.File file,
        @RestForm("folder") String folder);
}

Async/Reactive REST Client (Mutiny)

Quarkus supports Reactive REST Client with Mutiny — non-blocking calls suitable for high-concurrency (does not block thread while waiting for response):

Reactive Interface

@RegisterRestClient(configKey = "product-service")
@Path("/api/v1/products")
@Produces(MediaType.APPLICATION_JSON)
public interface ProductServiceReactiveClient {

    // Trả về Uni<T> — single async result
    @GET @Path("/{id}")
    Uni<ProductInfo> getById(@PathParam("id") Long id);

    // Trả về Multi<T> — stream of results
    @GET
    Multi<ProductInfo> listAll();

    // Uni<Response> — khi cần check status code
    @POST @Path("/{id}/reserve-stock")
    Uni<Response> reserveStock(
        @PathParam("id") Long id,
        @QueryParam("quantity") int quantity);
}

Use Reactive Client

@ApplicationScoped
public class OrderService {

    @Inject @RestClient
    ProductServiceReactiveClient productClient;

    // Non-blocking: gọi nhiều services song song
    public Uni<OrderDTO> createOrderReactive(
            String customerId, CreateOrderRequest request) {

        // Gọi song song: check stock cho tất cả items
        List<Uni<ProductInfo>> productCalls = request.items()
            .stream()
            .map(item -> productClient.getById(item.productId()))
            .toList();

        return Uni.combine().all().unis(productCalls)
            .with(results -> {
                // Tất cả products đã load xong
                @SuppressWarnings("unchecked")
                List<ProductInfo> products =
                    (List<ProductInfo>) (List<?>) results;

                Order order = new Order();
                order.customerId = customerId;

                for (int i = 0; i < products.size(); i++) {
                    ProductInfo product = products.get(i);
                    int qty = request.items().get(i).quantity();
                    order.addItem(product.id(), product.name(),
                        Money.vnd(product.price()), qty);
                }

                order.persist();
                return OrderDTO.from(order);
            });
    }

    // Chain reactive calls
    public Uni<OrderDTO> processOrder(Long orderId) {
        return Order.<Order>findById(orderId)
            .onItem().ifNull()
                .failWith(new ResourceNotFoundException(
                    "Order", orderId))
            .flatMap(order ->
                // Reserve stock cho từng item
                reserveAllStock(order)
                    .replaceWith(order))
            .map(OrderDTO::from);
    }

    private Uni<Void> reserveAllStock(Order order) {
        List<Uni<Response>> reservations = order.items.stream()
            .map(item -> productClient.reserveStock(
                item.productId, item.quantity))
            .toList();

        return Uni.combine().all().unis(reservations)
            .discardItems();
    }
}

When to use Reactive? When the service needs to call multiple downstream services in parallel or needs high throughput. For a simple request-response, blocking client is good enough.

Service Discovery with Stork

Instead of hardcoding the URL, use Stork for service discovery + client-side load balancing:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-stork</artifactId>
</dependency>
<!-- Consul backend -->
<dependency>
    <groupId>io.smallrye.stork</groupId>
    <artifactId>stork-service-discovery-consul</artifactId>
</dependency>
# application.properties
# Stork service discovery
quarkus.stork.product-service.service-discovery.type=consul
quarkus.stork.product-service.service-discovery.consul-host=localhost
quarkus.stork.product-service.service-discovery.consul-port=8500

# Load balancing strategy
quarkus.stork.product-service.load-balancer.type=round-robin

# REST Client dùng stork:// scheme
quarkus.rest-client.product-service.url=stork://product-service
// Client interface — không thay đổi gì!
@RegisterRestClient(configKey = "product-service")
@Path("/api/v1/products")
public interface ProductServiceClient {
    @GET @Path("/{id}")
    ProductInfo getById(@PathParam("id") Long id);
}
// Stork tự động resolve URL từ Consul

Kubernetes DNS (no Consul needed)

# Kubernetes service discovery
quarkus.stork.product-service.service-discovery.type=kubernetes
quarkus.stork.product-service.service-discovery.k8s-namespace=ecommerce

Testing REST Client with WireMock

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
    <scope>test</scope>
</dependency>

Mock REST Client with @InjectMock

@QuarkusTest
class OrderServiceTest {

    @InjectMock
    @RestClient
    ProductServiceClient productClient;

    @Inject
    OrderService orderService;

    @Test
    void testCreateOrder() {
        // Mock product response
        when(productClient.getById(1L))
            .thenReturn(new ProductInfo(
                1L, "Laptop", 25_000_000L, "VND",
                "ACTIVE", new StockInfo(50, 0)));

        when(productClient.getById(2L))
            .thenReturn(new ProductInfo(
                2L, "Mouse", 500_000L, "VND",
                "ACTIVE", new StockInfo(100, 0)));

        CreateOrderRequest request = new CreateOrderRequest(
            List.of(
                new OrderItemRequest(1L, 1),
                new OrderItemRequest(2L, 2)));

        OrderDTO result = orderService.createOrder(
            "user-123", request);

        assertEquals(26_000_000L, result.totalAmount());
        assertEquals(2, result.items().size());

        verify(productClient).getById(1L);
        verify(productClient).getById(2L);
    }

    @Test
    void testProductNotFound() {
        when(productClient.getById(999L))
            .thenThrow(new ResourceNotFoundException(
                "Product", 999L));

        assertThrows(BusinessException.class,
            () -> orderService.createOrder("user-123",
                new CreateOrderRequest(List.of(
                    new OrderItemRequest(999L, 1)))));
    }

    @Test
    void testProductServiceDown() {
        when(productClient.getById(anyLong()))
            .thenThrow(new ProcessingException(
                "Connection refused"));

        assertThrows(ProcessingException.class,
            () -> orderService.createOrder("user-123",
                new CreateOrderRequest(List.of(
                    new OrderItemRequest(1L, 1)))));
    }
}

## Bài tập

1. Tạo `ProductServiceClient` interface với REST Client annotations
2. Thêm `ResponseExceptionMapper` xử lý 404, 400, 503
3. Implement logging filter cho REST Client calls
4. Thêm `X-Correlation-ID` header propagation
5. Gọi Product Service từ Order Service khi tạo order
6. Tạo Reactive REST Client — gọi song song check stock cho nhiều products
7. Tạo WireMock test cho: success, product not found, service down
8. Implement Programmatic Client (`RestClientBuilder`) cho dynamic URL
9. (Nâng cao) Tích hợp Stork service discovery với Consul/Kubernetes

## Tổng kết

- **MicroProfile REST Client** — type-safe, declarative HTTP calls giữa services
- **`@RegisterRestClient`** + `@RestClient` inject — giống CDI injection
- **`ResponseExceptionMapper`** chuyển HTTP errors thành meaningful exceptions
- **Client Filters** cho logging, header propagation, authentication
- **Timeout configuration** trong `application.properties`
- **Async/Reactive** (`Uni<T>`, `Multi<T>`) — non-blocking, gọi nhiều services song song
- **Stork** — service discovery + client-side load balancing (Consul, Kubernetes DNS)
- **Programmatic client** (`RestClientBuilder`) cho dynamic URLs
- **Testing** — `@InjectMock @RestClient` with Mockito for unit tests

Next article: gRPC — High performance communication.