Migrating from Feign to @HttpExchange in Spring Boot 4: Why a Full Rewrite Isn't Worth It

The author shares practical experience replacing OpenFeign with Spring's @HttpExchange during a Spring Boot 4 upgrade, demonstrating configuration simplification, authentication handling via RestClient interceptors, error mapping with defaultStatusHandler, and the decision to coexist both clients rather than force a full migration.

Java Tech Enthusiast
Java Tech Enthusiast
Java Tech Enthusiast
Migrating from Feign to @HttpExchange in Spring Boot 4: Why a Full Rewrite Isn't Worth It

While upgrading a legacy project to Spring Boot 4, the author evaluated replacing OpenFeign with Spring Framework's built-in HTTP Interface ( @HttpExchange). The project had multiple Feign clients (Inventory, Member, Coupon, Logistics, Payment) each with custom configurations: RequestInterceptor for authentication headers, per-service timeouts in YAML, and ErrorDecoder implementations for mapping HTTP status codes to domain exceptions.

Initial Migration: Logistics Client

The author started with a simple Logistics client. The Feign version used

@FeignClient(name="logistics-service", url="${service.logistics.url}")

with @GetMapping. The @HttpExchange version reduced boilerplate:

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

However, manually creating RestClient, RestClientAdapter, and HttpServiceProxyFactory beans for each client would just swap Feign configuration for another set of bean definitions.

Spring Boot 4 Group Registration

Spring Boot 4 introduces @ImportHttpServices for grouped registration:

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

Base URLs and timeouts move to application.yml under spring.http.serviceclient:

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

This keeps interfaces focused on HTTP contracts while infrastructure handles environment-specific details.

Authentication: Interceptor per Group

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

@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);
    });
}

Third-party clients (e.g., external logistics providers) are excluded from this interceptor to avoid leaking internal tokens.

Error Handling: Status Handlers per Client

Feign's ErrorDecoder mapped 404 → InventoryNotFoundException, 409 → StockConflictException, else InventoryServiceException. With RestClient, the author uses defaultStatusHandler on a per-group basis:

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 services continue to catch typed exceptions without change.

Coexistence Over Full Replacement

The author concludes that @HttpExchange simplifies interface definitions but does not eliminate the need for authentication, timeouts, error translation, retries, connection pooling, tracing, circuit breaking, or service discovery. For stable, complex Feign integrations (Nacos discovery, Spring Cloud LoadBalancer, custom encoders/decoders, circuit breakers), the risk of rewriting outweighs the benefit of swapping @GetMapping for @GetExchange.

The system now runs both: new or simple HTTP calls use @HttpExchange; battle-tested Feign clients remain untouched. Migration is deferred until a module is touched for other reasons.

Architectural Cleanup: Gateway Pattern

During migration, the author discovered business logic leaking into Feign interfaces via default methods (e.g., hasStock calling query and comparing availability). These were extracted into a separate 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;
    }
}

Call chain becomes

OrderService → InventoryGateway → InventoryClient → HTTP

, decoupling business logic from the underlying HTTP client implementation.

Performance and Dependency Impact

No measurable latency difference was observed — network-bound calls dominate. 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. The author now defaults to @HttpExchange for new clients and only reaches for Feign when service discovery, advanced load balancing, or existing Spring Cloud ecosystem integration are required.

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.

ArchitectureMicroservicesMigration StrategyOpenFeignRestClientHTTP Client@HttpExchangeSpring Boot 4
Java Tech Enthusiast
Written by

Java Tech Enthusiast

Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!

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.