DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Day 18: Calling another service over HTTP with OpenFeign

Yesterday I split OrderHub's inventory capability into its own Spring Boot app. The two services booted side by side, each on its own port — but they didn't talk. order-service still held an in-process stub of inventory, a leftover from the Day 14/15 resilience work. Today I made the call real: when you place an order, order-service reaches across the network and reserves the stock in inventory-service.

The seam I drew yesterday is now a live wire.

Three ways to call another service

Spring gives you RestTemplate (the classic synchronous template), WebClient (reactive, non-blocking), and OpenFeign (declarative). For a plain "call this endpoint and map the result" — exactly my case — Feign is the least code and the most readable. With RestTemplate you build the URL, headers, body and status handling by hand for every call. With Feign you describe the remote API as a Java interface and Spring generates the client.

The difference at the call site:

// RestTemplate — you build everything
StockView s = rest.postForObject(base + "/api/inventory/{sku}/reserve",
        new ReserveRequest(qty), StockView.class, sku);

// Feign — you declare the call
StockView s = inventory.reserve(sku, new ReserveRequest(qty));
Enter fullscreen mode Exit fullscreen mode

An interface becomes an HTTP call

Here's the whole idea. You write an interface with no implementation, annotated with the same Spring MVC annotations the target controller uses to serve the endpoint:

@FeignClient(name = "inventory-service",
             url  = "${inventory.service.url:http://localhost:8081}")
public interface InventoryServiceClient {

    @GetMapping("/api/inventory/{sku}")
    StockView getStock(@PathVariable("sku") String sku);

    @PostMapping("/api/inventory/{sku}/reserve")
    StockView reserve(@PathVariable("sku") String sku, @RequestBody ReserveRequest request);
}
Enter fullscreen mode Exit fullscreen mode

@EnableFeignClients on the app scans for these interfaces and registers a proxy bean for each. Feign reads the annotations, turns every method invocation into the matching HTTP request, and decodes the JSON response onto the return type. No implements, no new — I just inject InventoryServiceClient into OrderService and call it.

One detail I like: order-service and inventory-service share no code. They only share the JSON shape. So order-service keeps its own small StockView and ReserveRequest records that mirror the fields by name. The coupling is the contract, not the classpath — the consumer's copy can evolve independently of the producer's.

Wiring it into the order flow

placeOrder now reserves stock before it commits the order, so we never accept an order we can't fulfil:

private void reserveStock(String sku, int quantity) {
    try {
        StockView stock = inventory.reserve(sku, new ReserveRequest(quantity));
        log.info("Reserved {} of {}; {} left", quantity, sku, stock.available());
    } catch (FeignException ex) {
        throw new InventoryReservationException(sku, quantity, ex); // -> 409
    }
}
Enter fullscreen mode Exit fullscreen mode

This is where the sharp edge shows up. order-service is now coupled to inventory-service being up and answering. A synchronous call binds two services at runtime: their latencies add, and their failures propagate.

Bound the call, translate its failures

So two things are non-negotiable. First, timeouts — without them a hung dependency ties up an order thread forever:

spring.cloud.openfeign.client.config.inventory-service:
  connect-timeout: 2000
  read-timeout:    3000
Enter fullscreen mode Exit fullscreen mode

Second, error handling. Feign's default ErrorDecoder turns any non-2xx (a 404 unknown SKU, a 409 insufficient stock, a 5xx) or a transport failure into a FeignException. I catch it at the seam and translate it into a domain exception my @RestControllerAdvice maps to a clean 409 — never a leaked stack trace.

Testing without the other service running

You can't require a live inventory-service for order-service's tests. I used two techniques. A focused test points the Feign client at an OkHttp MockWebServer — a real socket with canned responses — and asserts it sends the right method, path and body, and that a 409 surfaces as a FeignException. For the full-stack order integration tests, I replaced the Feign proxy with a Mockito @MockBean so placeOrder's reservation is a no-op. The order flow runs; the network doesn't. The whole reactor stays green: 42 unit tests plus 16 integration tests.

The tradeoff

Feign makes the call cheap to write, which hides an expensive truth: synchronous calls couple you and add latency. Days 14–15 soften that with retries, a circuit breaker and a fallback. But the real fix is to stop calling synchronously when you don't need an answer right now — which is exactly where Phase 4 (Kafka events) is heading. Sync when you must; async when you can.

Next up: Eureka, so services find each other by name instead of a hardcoded URL.

Try it: https://dev48v.infy.uk/orderhub.php
Code: https://github.com/dev48v/order-hub-from-zero

Top comments (0)