← Vishnu Vardhan

Why @Service on a method won't compile

August 2026 · Spring from first principles, part 1

While wiring a Spring AI controller, I put @Service on a method. Eclipse refused before I could run anything:

The annotation @Service is disallowed for this location

Spring hasn't started at this point, so this is the Java compiler talking. I'd used @Service for years without asking what it actually is. The answer is eight lines long, and it explains most of how Spring finds your classes.

1. An annotation declares where it's allowed

This is Service.java in the Spring Framework source:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {

    @AliasFor(annotation = Component.class)
    String value() default "";
}
@Target(TYPE)Allowed on classes, interfaces, enums and records only. Methods aren't listed, so javac rejects the code. This line is the error.
@Retention(RUNTIME)Kept in the .class file. With SOURCE it would be thrown away during compilation, and Spring would never see it.
@ComponentAn annotation on an annotation, called a meta-annotation. This is the entire mechanism behind Spring stereotypes.

2. What Spring does with it at startup

Take one class and follow it through the application context:

package com.example.advice;

@Service
public class OfferService {
    OfferService(PricingRules rules) { ... }
}
1 · scan
List every .class under com/example/**
2 · read bytes
Read annotation metadata without loading the class
3 · match
Is @Component present, directly or meta?
4 · register
Bean definition "offerService"
5 · instantiate
Constructor called, PricingRules injected
From a class file on disk to a singleton bean
  1. Scan. @SpringBootApplication includes @ComponentScan for its own package and everything below it. A ClassPathBeanDefinitionScanner resolves that package into a list of class files.
  2. Read the bytes, not the class. Each file's annotation metadata is read directly from its bytecode. Loading every class into the JVM just to inspect it would be slow, and it would run static initializers that have no business running at scan time.
  3. Follow the meta-annotations. The filter doesn't look for @Service at all. It asks "is @Component present, on the class or on any of its annotations?" @Service carries @Component, so the answer is yes.
  4. Register a recipe. Spring stores a bean definition: which class to build, its scope and its name. The name is the class name with a lowercase first letter, offerService. No object exists yet.
  5. Instantiate. When the context refreshes, Spring resolves the constructor, finds a PricingRules bean to inject, creates the object and runs the post-processors. Now it's a bean.

So @Service has no behavior of its own. It's @Component with a name that tells you which layer the class is in. And since the scanner works on classes, a method was never something it could pick up.

3. On a method, use @Bean

If a method should produce a bean, that's the job of @Bean, whose @Target includes METHOD:

@Configuration
public class AiConfig {

    @Bean
    ChatClient advisorClient(ChatClient.Builder builder) {
        return builder.defaultSystem("You are a freight advisor.").build();
    }
}

Now the recipe is "call advisorClient()". There's a subtlety: Spring subclasses a @Configuration class at runtime with CGLIB. If one @Bean method calls another, the call is intercepted and returns the existing singleton instead of building a second instance.

So why have stereotypes at all?

If @Service is just @Component, why not use @Component everywhere?

One @Target line revealed the whole model. Annotations are metadata. Retention decides who gets to see them. Spring reads them from bytecode, and a stereotype is a meta-annotated @Component. The next part of this series follows what happens after step 5: post-processors, proxies, and why @Transactional does nothing when a method calls another method on the same object.

Source