How Feign Works Inside

Dynamic Proxies

There is no class implementing ProductClient in your code. At startup Feign creates a JDK dynamic proxy: an object that implements the interface and sends every method call to one handler. The handler reads the annotations, builds the URL, makes the HTTP call and converts the JSON. Here is a tiny "MiniFeign" with a fake HTTP layer:

Example

import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        FakeHttp http = new FakeHttp();
        ProductClient client = MiniFeign.create(ProductClient.class, "http://product-service", http);

        System.out.println("Is a proxy: " + Proxy.isProxyClass(client.getClass()));
        System.out.println(client.getProduct(7));
        System.out.println(client.byCategory("Audio", 0));
    }
}

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Get { String value(); }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) @interface PathVar { String value(); }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) @interface Param { String value(); }

interface ProductClient {
    @Get("/api/products/{id}")
    String getProduct(@PathVar("id") long id);

    @Get("/api/products")
    String byCategory(@Param("category") String category, @Param("page") int page);
}

class MiniFeign {
    @SuppressWarnings("unchecked")
    static <T> T create(Class<T> api, String baseUrl, FakeHttp http) {
        InvocationHandler handler = (proxy, method, args) -> {
            String path = method.getAnnotation(Get.class).value();
            StringBuilder query = new StringBuilder();
            Parameter[] params = method.getParameters();
            for (int i = 0; i < params.length; i++) {
                PathVar pv = params[i].getAnnotation(PathVar.class);
                Param qp = params[i].getAnnotation(Param.class);
                if (pv != null) path = path.replace("{" + pv.value() + "}", String.valueOf(args[i]));
                if (qp != null) query.append(query.isEmpty() ? "?" : "&").append(qp.value()).append('=').append(args[i]);
            }
            String url = baseUrl + path + query;
            System.out.println("  " + method.getName() + "(...) -> GET " + url);
            return http.get(url);
        };
        return (T) Proxy.newProxyInstance(api.getClassLoader(), new Class<?>[] {api}, handler);
    }
}

class FakeHttp {
    private final Map<String, String> responses = Map.of(
        "http://product-service/api/products/7", "{\"id\":7,\"name\":\"boAt Rockerz 450\",\"price\":1499.00}",
        "http://product-service/api/products?category=Audio&page=0", "[{\"id\":7},{\"id\":9}]");

    String get(String url) {
        return responses.getOrDefault(url, "404 Not Found");
    }
}

Output

Is a proxy: true
  getProduct(...) -> GET http://product-service/api/products/7
{"id":7,"name":"boAt Rockerz 450","price":1499.00}
  byCategory(...) -> GET http://product-service/api/products?category=Audio&page=0
[{"id":7},{"id":9}]

What Real Feign Adds

  • Contract: reads Spring MVC annotations (@GetMapping, @RequestParam...).
  • Encoder / Decoder: Jackson converts request bodies and JSON responses into your records.
  • Load balancer: replaces http://product-service with a real instance from Eureka.
  • Interceptors, ErrorDecoder, Retryer, logging and timeouts (next topics).

Tip: In an interview, "How does Feign work without an implementation class?" — answer: JDK dynamic proxy + annotation metadata. Spring registers a FeignClientFactoryBean for each interface that produces that proxy.