跪拜 Guibai
← Back to the summary

Banning Feign: A Unified RPC Client That Fixes Context, Timeout, and Fallback by Default

Ban Feign! Why We Built Our Own InternalServiceClient

In microservice cross-service calls, 90% of people use Feign — but we chose not to.


1. Why Not Use Feign for Microservice Cross-Service Calls?

In China, when building Spring Cloud microservices, cross-service calls almost always have one answer: Feign.

OpenFeign is indeed a classic solution: declarative interfaces, automatic serialization, integrated Ribbon load balancing. But after using it for a while, you find that while it solves some problems, it introduces even more new ones.

During the design of MetaLite, we made a decision: Ban Feign and build our own InternalServiceClient.

This decision sparked a lot of debate within the team. After all, Feign is the standard for Spring Cloud; not using it meant writing our own calling framework. But after stepping on landmines across several projects, we are convinced it was the right choice.

In this article, I'll lay out Feign's pain points, the design thinking behind InternalServiceClient, and a side-by-side code comparison. After reading, you can judge for yourself.


2. Feign's Pain Points

2.1 Difficult Context Propagation

In a microservice call chain, a lot of contextual information needs to be passed between services:

With Feign, this information must be manually injected into HTTP Headers via RequestInterceptor:

@Component
public class FeignHeaderInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        template.header("traceId", ThreadContext.getTraceId());
        template.header("userId", ThreadContext.getLoginUserId());
        template.header("appId", ThreadContext.getAppId());
        template.header("X-Seata-XID", ThreadContext.getSeataXid());
    }
}

It looks fine, but there are two hidden dangers:

2.2 Inflexible Timeout Control

Feign's timeout configuration granularity is coarse. It's usually set globally in application.yml:

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 10000

The problem is: Different calls have vastly different timeout requirements.

With Feign, you either change it globally (affecting other calls) or configure a separate config class for each Client (code bloat).

Even worse, in a microservice call chain, if A → B → C each uses a 10-second timeout, the end user might wait 30 seconds to get a response. This is the timeout accumulation effect — the longer the call chain, the slower the final response.

2.3 Complex Fallback Handling

Feign's fallback relies on Hystrix or Resilience4j:

@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {
    @GetMapping("/api/user/info")
    Resp<UserDto> getUserInfo(@RequestParam String userId);
}

@Component
public class UserClientFallback implements UserClient {
    @Override
    public Resp<UserDto> getUserInfo(String userId) {
        return Resp.error("User service temporarily unavailable");
    }
}

Every interface needs a Fallback implementation class. The more services, the more Fallback classes, and the code volume skyrockets.

Moreover, Fallback is at the interface level, not the call level. For the same interface, some callers need fallback, others don't, but Feign can only configure one global strategy.

2.4 Decoupling of Interface Definition and Implementation

Feign requires the caller to define an interface:

@FeignClient(name = "user-service")
public interface UserClient {
    @PostMapping("/api/admin/sysUser/getInfo")
    Resp<UserDto> getUserInfo(@RequestBody InternalReq req);
}

But the actual implementation of the interface is the callee's Controller. When the callee changes the interface path, parameter types, or return format, the caller's Feign interface gets no compile-time warning — the problem is only discovered when the call fails at runtime.

For projects with many services and frequent iterations, this decoupling is extremely painful.


3. InternalServiceClient's Design

3.1 Core Design Philosophy

MetaLite's InternalServiceClient design philosophy is clear: A unified calling entry point that automatically handles all cross-cutting concerns.

public class InternalServiceClient {

    public void callOneInstance(RpcRequest rpcRequest) { }

    public <T> Resp<T> callOneInstanceRtnData(RpcRequest rpcRequest, Class<T> respDataType) { }

    public <T> Resp<List<T>> callOneInstanceRtnListData(RpcRequest rpcRequest, Class<T> respDataType) { }

    public <E> Resp<PageResultDto<E>> callOneInstanceRtnPageData(RpcRequest rpcRequest, Class<E> respDataType) { }
}

Only 4 core methods, covering all calling scenarios:

Method Purpose
callOneInstance Call a single instance, no return value
callOneInstanceRtnData Call a single instance, return a single object
callOneInstanceRtnListData Call a single instance, return a collection
callOneInstanceRtnPageData Call a single instance, return paginated data

3.2 Automatic Context Propagation

This is one of InternalServiceClient's most core capabilities. In the checkAndFillRpcRequest method, all context is automatically injected:

private RpcRequest checkAndFillRpcRequest(RpcRequest rpcRequest) {
    // Build internal request headers, automatically propagate full-link context
    Map<String, String> headers = new HashMap<>();
    headers.put(TRACE_ID, ThreadContext.getTraceId());
    headers.put(LOGIN_USER_ID, ThreadContext.getLoginUserId());
    headers.put(APP_ID, ThreadContext.getAppId());
    headers.put(SEATA_XID, ThreadContext.getSeataXid());

    if (rpcRequest.getHeaders() == null) {
        rpcRequest.setHeaders(headers);
    } else {
        rpcRequest.getHeaders().putAll(headers);
    }
    return rpcRequest;
}

Developers don't need to worry about how Headers are passed, how TraceId is carried over, how Seata XID propagates across services — as long as you use InternalServiceClient, all of this is handled automatically.

3.3 Layer-by-Layer Timeout Decrement

This is the key design that distinguishes InternalServiceClient from Feign. In checkAndFillRpcRequest:

// Timeout decrements layer by layer
int timeoutMillis = rpcRequest.getTimeoutMillis();
timeoutMillis = timeoutMillis <= 0 ? API_REQUEST_TIMEOUT_MILLIS : timeoutMillis - API_REQUEST_TIMEOUT_MILLIS_MINUS;
rpcRequest.setTimeoutMillis(timeoutMillis);

What does this mean?

A → B → C → D

A initiates call with timeout = 5000ms
B receives call with timeout = 5000 - 500 = 4500ms
C receives call with timeout = 4500 - 500 = 4000ms
D receives call with timeout = 4000 - 500 = 3500ms

With each layer passed, the timeout automatically decrements. This ensures:

3.4 Based on Nacos Service Discovery

InternalServiceClient uses Nacos for service discovery underneath; just specify the service name when calling:

RpcRequest rpcRequest = RpcRequest.builder()
        .provider("user-service")          // Service name, Nacos auto-discovery
        .endpoint("/api/admin/sysUser/getInfo")
        .param(internalReq)
        .rpcMode(RpcModeEnum.HTTP)
        .build();

Resp<UserDto> resp = internalServiceClient.callOneInstanceRtnData(rpcRequest, UserDto.class);

No need for Feign's @FeignClient(name = "user-service") annotation to bind the service name. The service name is explicitly specified on each call, clear and controllable.

3.5 Response Type Safety

Feign returns the type defined by the interface, but type conversion is only validated at runtime. InternalServiceClient requires the response data type to be explicitly specified at call time:

// Single object
Resp<UserDto> resp = internalServiceClient.callOneInstanceRtnData(rpcRequest, UserDto.class);

// Collection
Resp<List<OrderDto>> resp = internalServiceClient.callOneInstanceRtnListData(rpcRequest, OrderDto.class);

// Paginated
Resp<PageResultDto<UserDto>> resp = internalServiceClient.callOneInstanceRtnPageData(rpcRequest, UserDto.class);

Internally, types are strictly validated:

paramCheck(!Collection.class.isAssignableFrom(respDataType), "respDataType", "Cannot be a collection type");

If the type doesn't match, it errors immediately on the caller side, rather than silently failing in the downstream service.

3.6 Aspect Chain Enhancement

All methods of InternalServiceClient are intercepted by ApiCallAspect:

@Around("execution(public * com.metalite.rpc.InternalServiceClient.call*(..))")
private Object aroundBaseInternalServiceClient(ProceedingJoinPoint pjp) throws Throwable {
    // Unified logging, monitoring, exception handling
}

This means every RPC call automatically goes through:

Developers don't need to manually add try-catch, write logs, or set up monitoring — it's all done automatically.


4. Code Comparison

4.1 Feign Approach

// 1. Define Feign interface
@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {
    @PostMapping("/api/admin/sysUser/getInfo")
    Resp<UserDto> getUserInfo(@RequestBody InternalReq req);
}

// 2. Write Fallback implementation
@Component
public class UserClientFallback implements UserClient {
    @Override
    public Resp<UserDto> getUserInfo(InternalReq req) {
        return Resp.error("User service temporarily unavailable");
    }
}

// 3. Write RequestInterceptor to propagate context
@Component
public class FeignHeaderInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        template.header("traceId", ThreadContext.getTraceId());
        template.header("userId", ThreadContext.getLoginUserId());
        // ... Every Header must be added manually
    }
}

// 4. Caller usage
@Resource
private UserClient userClient;

public Resp<UserDto> getUser(String userId) {
    InternalReq req = new InternalReq();
    req.putParam("userId", userId);
    return userClient.getUserInfo(req);  // No timeout decrement
}

4.2 InternalServiceClient Approach

@Resource
private InternalServiceClient internalServiceClient;

public Resp<UserDto> getUser(String userId) {
    InternalReq req = new InternalReq();
    req.putParam("userId", userId);

    RpcRequest rpcRequest = RpcRequest.builder()
            .provider("user-service")
            .endpoint("/api/admin/sysUser/getInfo")
            .param(req)
            .rpcMode(RpcModeEnum.HTTP)
            .build();

    // One line call: context auto-propagated, timeout auto-decremented, response auto-converted
    return internalServiceClient.callOneInstanceRtnData(rpcRequest, UserDto.class);
}

Comparison:

Dimension Feign InternalServiceClient
Interface Definition Need to define Feign interface + Fallback class Not needed, directly construct RpcRequest
Context Propagation Need to manually write RequestInterceptor Auto-injected
Timeout Control Global config or per-Client config Layer-by-layer auto-decrement
Fallback Handling Write Fallback implementation per interface Unified response body handling
Type Safety Runtime validation Explicitly specified at call time
Monitoring/Logging Requires extra configuration Aspect auto-handled

5. Summary of Design Philosophy

Banning Feign isn't because we think Feign is bad, but because Feign solves the problem of "can you call", while microservices need to solve the problem of "calling well".

InternalServiceClient's design philosophy:

This is not a denial of Feign, but another understanding of the essence of microservice calls.

InternalServiceClient's design philosophy runs through the entire MetaLite framework — convention over configuration, explicit over implicit, unified entry over scattered definitions.


Framework Introduction: MetaLite — Next-generation enterprise-grade Java microservice technology foundation

Author Introduction: Based on 15 years of enterprise development experience in the Spring ecosystem, focused on building the next generation of Java microservice technology foundation through engineering thinking and architectural ideas validated in enterprise production environments.

Complete Documentation and Source Code: Search MetaLite on Gitee (https://gitee.com/MetaLite)

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

元界metalite

Backend demo address: http://admin.metalite.top User: guess Password: admin@2026