Migrating from Feign to @HttpExchange in Spring Boot 4: Why It's Not That Simple

The author shares their experience replacing OpenFeign with Spring's @HttpExchange during a Spring Boot 4 upgrade, demonstrating how interface definitions simplify but cross-cutting concerns like authentication, timeouts, and error handling remain, leading to a pragmatic coexistence of both tools based on project needs.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Migrating from Feign to @HttpExchange in Spring Boot 4: Why It's Not That Simple

Recently upgrading a legacy project to Spring Boot 4, the author decided to refactor remote service calls that had long relied on OpenFeign. Initially Feign provided clean interface-based HTTP clients, but over years each client accumulated configuration: request interceptors for authentication and headers, per-service timeout settings, and custom error decoders to map HTTP status codes to business exceptions. With multiple clients (Inventory, Member, Coupon, Logistics, Payment), this boilerplate grew substantially.

Trying Spring's @HttpExchange

Spring Framework's HTTP Interface (@HttpExchange) had existed but felt like syntactic sugar over RestClient. Spring Boot 4 improved its support, so the author tested it on a simple logistics query client.

Before (Feign)

@FeignClient(name = "logistics-service", url = "${service.logistics.url}")
public interface LogisticsFeignClient {
    @GetMapping("/api/logistics/orders/{orderNo}")
    LogisticsDTO query(@PathVariable("orderNo") String orderNo);
}

After (@HttpExchange)

@HttpExchange
public interface LogisticsClient {
    @GetExchange("/api/logistics/orders/{orderNo}")
    LogisticsDTO query(@PathVariable String orderNo);
}

The interface definition is simpler, but the real difference lies in client registration and configuration.

Group Registration with @ImportHttpServices

Instead of manually building RestClient, Adapter, and ProxyFactory for each client, Spring Boot 4 allows grouping clients by business domain:

@Configuration
@ImportHttpServices(group = "logistics", types = LogisticsClient.class)
@ImportHttpServices(group = "inventory", types = InventoryClient.class)
public class HttpClientConfig {}

Base URLs and timeouts move to configuration files:

spring:
  http:
    serviceclient:
      logistics:
        base-url: http://logistics-service
        connect-timeout: 2s
        read-timeout: 3s
      inventory:
        base-url: http://inventory-service
        connect-timeout: 1s
        read-timeout: 2s

The client interface now contains only the HTTP contract; environment-specific details are externalized.

Handling Authentication per Client Group

Internal services require propagating the user's JWT token and an application name. Adding @RequestHeader to every method would pollute business code. Instead, a RestClientHttpServiceGroupConfigurer bean configures a request interceptor for selected groups:

@Bean
RestClientHttpServiceGroupConfigurer httpClientConfigurer() {
    return groups -> {
        groups.filterByName("inventory")
              .forEachClient((group, builder) -> configureInternalClient(builder));
        groups.filterByName("member")
              .forEachClient((group, builder) -> configureInternalClient(builder));
    };
}

private void configureInternalClient(RestClient.Builder builder) {
    builder.requestInterceptor((request, body, execution) -> {
        String token = UserContextHolder.getToken();
        if (token != null) {
            request.getHeaders().set("Authorization", token);
        }
        request.getHeaders().set("X-App-Name", "order-service");
        return execution.execute(request, body);
    });
}

This keeps authentication logic out of business services and applies it only to internal service groups, avoiding leaking tokens to third-party APIs.

Exception Handling with RestClient Status Handlers

Feign's ErrorDecoder is replaced by defaultStatusHandler on the RestClient builder, again scoped to the inventory group:

private void configureInventoryClient(RestClient.Builder builder) {
    builder.defaultStatusHandler(status -> status.value() == 404,
            (request, response) -> { throw new SkuNotFoundException(); });
    builder.defaultStatusHandler(status -> status.value() == 409,
            (request, response) -> { throw new StockNotEnoughException(); });
    builder.defaultStatusHandler(status -> status.is5xxServerError(),
            (request, response) -> { throw new InventoryServiceException("库存服务异常: " + response.getStatusCode()); });
}

Business code continues to catch typed exceptions:

try {
    return inventoryClient.lock(request);
} catch (StockNotEnoughException e) {
    throw new OrderCreateException("商品库存不足");
} catch (InventoryServiceException e) {
    log.error("库存服务调用失败, skuId={}", request.skuId(), e);
    throw e;
}

Realization: Cross-Cutting Concerns Don't Disappear

The migration revealed that @HttpExchange only simplifies the interface definition. Authentication, timeouts, exception translation, logging, tracing, retries, connection pooling, circuit breaking, service discovery, and load balancing remain necessary engineering tasks. They don't vanish by changing an annotation.

Pragmatic Coexistence

The author did not replace all Feign clients. Stable, complex integrations relying on Nacos service discovery, Spring Cloud LoadBalancer, CircuitBreaker, and custom Feign encoders/decoders were left untouched. Rewriting proven infrastructure for marginal syntactic gain introduces unjustified risk. New, simple HTTP calls (third-party APIs, straightforward internal services) start with @HttpExchange; existing Feign clients continue running.

Extracting Business Logic into a Gateway Layer

During the refactor, the author discovered business logic leaking into Feign interfaces via default methods (e.g., hasStock calling query and comparing quantities). This logic was moved to a dedicated InventoryGateway service:

@Service
@RequiredArgsConstructor
public class InventoryGateway {
    private final InventoryClient client;
    public boolean hasStock(Long skuId, int quantity) {
        InventoryDTO inventory = client.query(skuId);
        return inventory.available() >= quantity;
    }
}

The call chain becomes

OrderService → InventoryGateway → InventoryClient → HTTP

, decoupling business rules from the underlying HTTP client implementation. Future switches (Feign, @HttpExchange, RPC) affect only the client layer.

Performance and Dependency Benefits

No significant performance difference was observed; network latency dominates. The main gains are structural: fewer dependencies (no spring-cloud-starter-openfeign), cleaner separation of concerns, and a lighter starting point for new HTTP integrations.

Conclusion

Feign isn't obsolete; it remains solid for complex, established service meshes. For new Spring Boot 4 projects with ordinary HTTP needs, @HttpExchange is a viable first choice. Tool selection should follow project requirements, not dogma. The author now starts with @HttpExchange and only reaches for Feign when advanced Spring Cloud integration is genuinely needed.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

MigrationMicroservicesAuthenticationError HandlingOpenFeignRestClientHTTP Client@HttpExchangeSpring Boot 4Gateway Pattern
LuTiao Programming
Written by

LuTiao Programming

LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.