Skip to content

Spring Boot · Dependency Injection

Spring Stereotype Annotations and Injection Styles, Explained

5 min read Updated 2026-06-26 Share:

Practice Component Annotations interview questions

The annotations you'll use every single day

Stereotype annotations and @Autowired are the most-typed Spring code there is. They look interchangeable, and interviewers love to probe whether you understand the real differences — which ones add behavior, how scanning finds them, and why the way you inject matters. This article sorts it all out.

@Component is the foundation

@Component marks a class as a candidate for component scanning — "create and manage a bean from this." Every other stereotype is built on it.

@Component
class EmailValidator {
    boolean isValid(String email) { return email.contains("@"); }
}

At startup, @ComponentScan (bundled into @SpringBootApplication) walks the package tree, finds each @Component, and registers a bean definition.

The four stereotypes

@Component, @Service, @Repository, and @Controller all register beans and are functionally near-identical — but they communicate the class's layer, and two add real behavior:

@Component   class GenericHelper { }   // generic
@Service     class OrderService { }    // business logic (documentation)
@Repository  class OrderRepository { } // persistence (+ exception translation)
@Controller  class OrderController { } // web layer (returns view names)

@Service and @Controller are largely semantic — they read better and let aspects/tools target a layer. But @Repository is special: it enables persistence exception translation, converting vendor-specific exceptions into Spring's DataAccessException hierarchy. @Controller plugs into Spring MVC's request mapping.

@RestController = @Controller + @ResponseBody

For JSON APIs you want @RestController, which makes every method return data in the response body instead of a view name:

@Controller
class PageController {
    @GetMapping("/home")
    String home() { return "home"; }          // "home" = a VIEW NAME
}

@RestController                                 // = @Controller + @ResponseBody
class ApiController {
    @GetMapping("/users")
    List<User> users() { return service.all(); } // serialized to JSON
}

Use @Controller for server-rendered HTML, @RestController for REST APIs.

How component scanning finds your beans

@ComponentScan defines where scanning starts — by default, the package of your @SpringBootApplication class and all sub-packages:

@SpringBootApplication   // scans com.example and everything below
class App { }

@ComponentScan(basePackages = { "com.example.web", "com.shared.util" })
class Config { } // override the base packages explicitly

This is exactly why your main class belongs in a root package above the rest of your code — beans in packages outside that tree won't be found. You can narrow or widen scanning with includeFilters/excludeFilters, which is how Spring Boot's test slices (@WebMvcTest, @DataJpaTest) load only the relevant layer.

@Autowired and the three injection styles

@Autowired injects a matching bean by type at a constructor, setter, or field. Spring supports three styles:

@Service
class A {                                  // 1. constructor (preferred)
    private final Repo repo;
    A(Repo repo) { this.repo = repo; }
}
@Service
class B {                                  // 2. setter
    private Repo repo;
    @Autowired void setRepo(Repo repo) { this.repo = repo; }
}
@Service
class C {                                  // 3. field (discouraged)
    @Autowired private Repo repo;
}

Since Spring 4.3, @Autowired is optional on a single constructor — Spring autowires its parameters automatically.

Why constructor injection wins

The Spring team recommends constructor injection, and the reasons are exactly what interviewers want to hear:

@Service
class OrderService {
    private final PaymentGateway gateway;     // can be final → immutable
    private final InventoryService inventory;
    OrderService(PaymentGateway g, InventoryService i) { gateway = g; inventory = i; }
}

// Test with no Spring at all:
var svc = new OrderService(mockGateway, mockInventory);

Constructor injection (1) allows final fields → immutability and thread safety; (2) guarantees the bean is never half-initialized; (3) makes dependencies visible in the signature — a bloated constructor is a visible smell; (4) needs no Spring or reflection to test. Field injection hides dependencies, forbids final, and lets the dependency list grow unnoticed. In a plain unit test, a field-injected dependency is just null.

Useful wiring patterns

A few patterns come up constantly:

// Inject ALL implementations of an interface (Strategy / plugin registry):
@Service
class NotificationService {
    NotificationService(List<Notifier> notifiers) { } // every Notifier bean
}

// Make a dependency optional:
@Service
class MetricsService {
    MetricsService(Optional<MetricsExporter> exporter) { } // empty if no bean
}

// Pick a default among several candidates:
@Component @Primary class StripeGateway implements PaymentGateway { }

// Vary beans by environment:
@Component @Profile("prod") class SmtpMailSender implements MailSender { }

Injecting List<T> or Map<String,T> gathers all implementations — adding a new strategy is just adding a new @Component. @Primary sets a default; @Profile swaps implementations per environment.

@Component vs @Bean

Use @Component when you own the class; use @Bean when you can't annotate it (third-party types) or need construction logic:

@Service class OrderService { }              // your class → stereotype

@Configuration
class Config {
    @Bean RestClient restClient() {          // library class → @Bean method
        return RestClient.builder().baseUrl("https://api.example.com").build();
    }
}

Real apps blend both: stereotypes for your code, @Bean methods for external types and custom wiring.

Recap

@Component is the base stereotype; @Service and @Controller mostly document intent, while @Repository adds exception translation and @RestController bundles @ResponseBody for APIs. Component scanning starts at your @SpringBootApplication package, so keep that class at the root. Inject through constructors for immutable, explicit, testable beans; avoid field injection. And reach for List<T> injection, @Primary, @Profile, and @Bean methods as the situation demands.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel