33992 added support for importing HTTP service proxies into a registry and declaring them as beans for direct autowiring. This can be done centrally through@ImportHttpServices and also through extensions of AbstractHttpServiceRegistrar as described in the docs.

It would be useful to have an alternative, more decentralized option too, e.g. an @HttpServiceClient annotation to put on an HTTP service interface with the option to declare the group it belongs to through an attribute. We can provide this along with dedicated extension of AbstractHttpServiceRegistrar that looks for such annotated interfaces in specified base packages.

Comment From: DanielLiu1123

Friends, I think HttpServiceClient may not be a good idea. The HttpExchange annotation should remain neutral, meaning it can be applied to both the client and the server. In contrast, HttpServiceClient is client-specific, and placing it alongside HttpExchange would make the design look inconsistent.

For example:

// user/api/UserApi.java
@HttpExchange("/users")
public interface UserApi {
    @GetExchange("/{id}")
    User get(@PathVariable String id);
}

// user/server/UserServer.java
@RestController
public class UserServer implements UserApi {
    @Override
    public User get(String id) {
        // ...
    }
}

In this case, UserApi serves as an interface that can be both consumed by a client and implemented by a server. Adding HttpServiceClient to UserApi would be redundant for the server side and would undermine the annotation’s neutrality.

Comment From: quaff

Friends, I think HttpServiceClient may not be a good idea. The HttpExchange annotation should remain neutral, meaning it can be applied to both the client and the server. In contrast, HttpServiceClient is client-specific, and placing it alongside HttpExchange would make the design look inconsistent.

For example:

// user/api/UserApi.java @HttpExchange("/users") public interface UserApi { @GetExchange("/{id}") User get(@PathVariable String id); }

// user/server/UserServer.java @RestController public class UserServer implements UserApi { @Override public User get(String id) { // ... } } In this case, UserApi serves as an interface that can be both consumed by a client and implemented by a server. Adding HttpServiceClient to UserApi would be redundant for the server side and would undermine the annotation’s neutrality.

You can annotate @HttpServiceClient on sub interface:

@HttpServiceClient
public interface UserClient extends UserApi {
}

Comment From: DanielLiu1123

You can annotate @HttpServiceClient on sub interface:

Frankly, this approach just involves repetitive work. Our company is currently doing something similar, except we’re using @RequestMapping and @FeignClient. I must point out that this pattern can be quite frustrating in real-world development.

When the number of APIs is small, it may seem like a clean and elegant solution. But once the number of interfaces exceeds a hundred, you can’t help but question: “Why are we doing this? It’s completely redundant.” We’re currently exploring ways to eliminate the pattern where each client requires a separately defined class that extends the API interface.

If the only goal is to replace @FeignClient with another annotation, I believe it’s entirely unnecessary—because the pattern itself doesn’t need to exist in the first place.

Comment From: bclozel

@DanielLiu1123 @HttpServiceClient is entirely optional and you shouldn't use it if it doesn't fit your case. Lots of developers will only care about writing the interface for their client as the REST service is not implemented by their company, or is implemented with another programming language/technology. While it might not be a "real-world" use case for you, this is a perfectly valid case for many others, don't you think?

Let's take a step back, maybe. Is there anything missing from @ImportHttpServices for organizing your service clients? Have you tried using AbstractClientHttpServiceRegistrar instead? If you can organize clients in your codebase with one of these, you won't need @HttpServiceClient at all.

Comment From: DanielLiu1123

To summarize my position upfront: @HttpServiceClient adds unnecessary complexity and inconsistency to the API design, and it should be removed.

After reviewing the code, I’ve come to understand the role of HttpServiceClient. While it is indeed entirely optional, I still believe it introduces unnecessary complexity and may lead to inconsistencies in usage.

For users who use @HttpExchange to define clients, they must extend AbstractClientHttpServiceRegistrar to configure the packages to scan. This approach is essentially no different from using @HttpExchange with AbstractHttpServiceRegistrar; it simply offers an alternative way to achieve the same goal. As a result, we now have three different ways to register clients:

  1. @HttpExchange + @ImportHttpServices — Declarative approach
  2. @HttpExchange + extending AbstractHttpServiceRegistrar — Programmatic approach
  3. @HttpServiceClient + @HttpExchange + extending AbstractClientHttpServiceRegistrar

The first two approaches already address the needs of both types of users: those who use @HttpExchange as a neutral interface annotation, and those who treat it as client-specific. Given that, do we really need a third method for client registration?

Below are two examples illustrating equivalent use cases:

Using @HttpExchange for client-specific interfaces

@HttpServiceClient("user")
@HttpExchange("/users")
interface UserClient {
    @GetExchange("/{id}")
    User get(@PathVariable String id);
}

class X extends AbstractClientHttpServiceRegistrar {
    @Override
    protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importingClassMetadata) {
        findAndRegisterHttpServiceClients(registry, List.of("user.client"));
    }
}

Using @HttpExchange for neutral API interfaces

@HttpExchange("/users")
interface UserApi {
    @GetExchange("/{id}")
    User get(@PathVariable String id);
}

class Y extends AbstractHttpServiceRegistrar {
    @Override
    protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importingClassMetadata) {
        registry.forGroup("user").detectInBasePackages("user.api");
    }
}

As shown, the latter approach could easily accommodate the former, resulting in a more unified and streamlined solution. From this perspective, @HttpServiceClient doesn’t seem to solve a new or distinct problem.

These are just some of my personal thoughts. As an experienced Spring user, I value clarity and consistency in API design, and I hope this feedback can contribute to further refinement of the overall design.

Comment From: quaff

@HttpServiceClient doesn’t seem to solve a new or distinct problem.

If it's true, @HttpServiceClient will confuse users since there is a declarative annotation @ImportHttpServices.

Comment From: philwebb

The @HttpServiceClient interface is trying to solve a specific use-case that we want to support in Spring Boot. Perhaps there's a better way to do that, but I think it's an important one and probably quite common.

In Spring Boot, applications typically scan for all important classes from the user's application class down. For example, given:

package com.example;

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

}

All classes in com.example and below are scanned.

That means Spring Boot users expect to be able to use @Component, @Service, @Controller (etc) annotations on clases in com.example or below and have automatically get beans.

We also have the same thing with Spring Data Respoistory interfaces. Create an interface that extends Spring Data's Repository interface and we automatically create a bean from it.

So the use-case we really want to support is the following:

I am a Spring Boot user wanting to use an HTTP Service Client interface to connect to a remote service implemented by someone else.

For those folks, we really don't want them to have to use the @ImportHttpServices annotation themselves as it gives a very un-boot like experience.

I'm very curious if folks watching this issue have any other suggestions to solve that use-case that will make things less confusing.

Comment From: quaff

The @HttpServiceClient interface is trying to solve a specific use-case that we want to support in Spring Boot.

Then it should be moved to Spring Boot?

Comment From: bclozel

Then it should be moved to Spring Boot?

It is imported by ˋHttpServiceClientRegistrarSupport` in Framework.

Comment From: quaff

Then it should be moved to Spring Boot?

It is imported by ˋHttpServiceClientRegistrarSupport` in Framework.

I mean it and its related codes.

Comment From: DanielLiu1123

Key Points

  1. We can easily support automatic registration of @HttpExchange clients without requiring @ImportHttpServices or introducing @HttpServiceClient.
  2. Whether implicit auto-registration (without @ImportHttpServices) is a good default that deserves discussion.

1. Auto-registration is possible without @HttpServiceClient

We have a user story:

As a Spring Boot user, I want all @HttpExchange interfaces within the @SpringBootApplication package to be automatically registered, without explicitly declaring @ImportHttpServices, so that the experience aligns with how Spring Data Repository interfaces work.

This can be implemented cleanly without introducing @HttpServiceClient. Here’s an example:

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingHttpServiceProxyBean
@Import(ImportHttpServiceClients.class)
class ImportHttpServiceClientsConfiguration {

    static class ImportHttpServiceClients extends AbstractHttpServiceRegistrar {

        private final BeanFactory beanFactory;

        ImportHttpServiceClients(BeanFactory beanFactory) {
            this.beanFactory = beanFactory;
        }

        @Override
        protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importingClassMetadata) {
            if (AutoConfigurationPackages.has(this.beanFactory)) {
                registry
                    .forGroup("default")
                    .detectInBasePackages(AutoConfigurationPackages.get(this.beanFactory).toArray(String[]::new));
            }
        }
    }
}

The only concern with this approach is backward compatibility. After upgrading to version 4.0.0, interfaces that previously required manual registration might now be auto-registered. To avoid this, we can introduce a compatibility check: if the user has already explicitly registered a @HttpExchange interface, the auto-registration logic should skip it.

This is exactly the approach taken in httpexchange-spring-boot-starter.

2. Should auto-registration be enabled by default?

We can look at two established patterns in the Spring ecosystem:

  • Spring Data-style: interfaces are auto-scanned and registered based on package metadata.
  • Explicit opt-in model: like @EnableAsync or @EnableScheduling, users must explicitly enable the feature.

In my view, if @HttpExchange had been designed with auto-registration from the beginning, the Spring Data approach would be appropriate. But in reality, many users have already:

  • Manually registered numerous clients,
  • Built their own starters, or
  • Adopted third-party starters.

Introducing auto-registration now could break existing setups—or at the very least, create confusion. More importantly, there is currently no way to disable auto-registration, which makes the behavior harder to control.

Therefore, given that this is a new capability being introduced, I believe it’s more reasonable and safer to require users to explicitly use @ImportHttpServices for now.

Comment From: rstoyanchev

A couple of clarifications to insert.

@HttpServiceClient and AbstractClientHttpServiceRegistrar make the annotation approach possible, but it needs to enabled, and that is only done in Boot. The registrar is trivial enough so both could live in Boot, but having them in Framework makes it possible for @ImportHttpServices and the client annotation approach to stay out of each other's way as the former explicitly skips interfaces with the annotation. That helps to keep the two approaches independent from each other, and to keep things simpler in case of overlap where both are used.

The second is that @HttpServiceClient can be seen as a marker annotation. Its presence on an interface is only to indicate the intent to use it as a client proxy. In that sense it is different from a component annotation and has no other implications. It does not prevent server use, and I question the need for a sub-interface only to add the marker annotation.

Comment From: philwebb

More importantly, there is currently no way to disable auto-registration, which makes the behavior harder to control.

This isn't correct. Currently the out-of-the-box experience is to scan for @HttpServiceClient annotated interfaces. Once you declare @ImportHttpServices the auto-configuration backs off and you're in control. This is a standard pattern in Spring Boot.

Comment From: DanielLiu1123

I get what you’re trying to do, but users might not. For example, users might ask questions like: 1. What’s the difference between @HttpExchange and @HttpServiceClient? Why not just add the group property to @HttpExchange? It definitely looks tempting, but we all know this shouldn't happen. 2. Should I extend AbstractHttpServiceRegistrar or AbstractClientHttpServiceRegistrar? What’s the difference? 3. Why didn’t @ImportHttpServices automatically register my @HttpExchange interface? Ohhh… it’s because I added @HttpServiceClient to the interface, so it got skipped. :)

These are totally fair questions. Even after going through the source code, I still think this part feels messy. And all of this could’ve been avoided with a cleaner API design from the start.

Just imagine a world we didn’t have @HttpServiceClient: We wouldn’t need AbstractClientHttpServiceRegistrar anymore. We have simpler and clearer way to register client interfaces:

  • If you like declarative config, use @ImportHttpServices.
  • If you prefer a programmatic approach, extend AbstractHttpServiceRegistrar.

And just like that, all the questions above would be solved.

As for the feature that automatically registers @HttpServiceClient interfaces in the @SpringBootApplication package by default, it’s really not that important. You’re saving the user a single line of code, but it could introduce all the issues mentioned above. This is not a worthwhile trade-off.

Comment From: rstoyanchev

The crux of the matter is a split between those who prefer a centralized config approach vs those who prefer a decentralized approach. It's important to recognize there are plenty who prefer the latter, and I don't think any amount of discussion will bridge this gap because it is subjective and one's opinion will be further shaped by the specific use case at hand.

To put it differently, we can remove the client annotation as you suggest, but that will only shift the argument to the other side where those who prefer the distributed model will argue the opposite.

The decision to add the annotation is in part a recognition of this, so rather than removal, it is more practical to discuss how to manage co-existence of approaches. I will agree with you there is inherent complexity, but I think the questions you've chosen aren't maybe the best examples of the complexity.

  1. It's quite obvious that a group attribute cannot be added on HttpExchange which is for type and method level use, on the client or on the server side.
  2. AbstractClientHttpServiceRegistrar is a specialization of AbstractHttpServiceRegistrar with a documented purpose.
  3. This one touches on the real complexity and requires a bit of elaboration.

Given that both the centralized and the distributed approaches specify a group, if we let the two approaches blend where ImportHttpServices imports all interfaces including those with HttpServiceClient then there is no good way to decide which group to use. The choices are as follows:

  • The one from the import is used as an override
  • The one from the client annotation stands
  • Respect both, i.e. one in each group
  • Treated it as illegal, leading to more questions if it is not a mistake

This is further complicated by the fact that you can have no group at all ("Default" group), and it's hard to judge if that means open to being set externally, or rather deliberately in the "Default" group.

There are legitimate reasons for each of the above, and I see no good way of managing this other than keeping the two as mutually exclusive sets without overlap. That is simple to understand, and stays focused on the goal to provide both approaches, but not try to do anything more.

If you like HttpServiceClient directly on interfaces, use that and what you see on the interface and annotation is what you get. No need to look elsewhere.

If you like the centralized ImportHttpServices, then use that and it applies to every HttpExchange interface except those with HttpServiceClient, in effect recognizing those as using a different approach to providing metadata.

Comment From: rstoyanchev

After extensive discussions, we've decided to drop @HttpServiceClient in 7.0 M9 with #35431.

Comment From: ThomasVitale

Will an alternative option for having a declarative, annotation-based approach be considered in the future? Or does this mean that @ImportHttpServices will be the only way to go? It would have been very convenient. In any case, thanks for working on this feature, which I really liked!