@Component marks a class as a candidate for component scanning — it tells Spring "
create and manage a bean from this class." It's the generic stereotype that all the others
build on.
@Component // discovered by @ComponentScan → becomes a bean
class EmailValidator {
boolean isValid(String email) { return email.contains("@"); }
}
During startup, @ComponentScan (bundled into @SpringBootApplication) walks the package
tree, finds every @Component-annotated class, and registers a bean definition for each.
You then inject it anywhere. @Component is the foundation — @Service, @Repository, and
@Controller are all specializations meta-annotated with it.
Rule of thumb: @Component = "scan me into a bean." Use it for generic helper beans;
prefer a more specific stereotype when one fits the class's role.
All four register a bean — they are functionally near-identical — but they differ in semantic intent and a couple gain extra behavior. They make the class's layer obvious.
@Component class GenericHelper { } // generic, no specific layer
@Service class OrderService { } // business-logic layer
@Repository class OrderRepository { } // persistence layer (+ exception translation)
@Controller class OrderController { } // web layer (returns view names)
@Service and @Controller are mostly documentation — they read better and let tools/
aspects target a layer. @Repository is special: it enables persistence exception
translation, converting vendor-specific exceptions into Spring's DataAccessException
hierarchy. @Controller integrates with Spring MVC's request mapping.
Rule of thumb: Pick the stereotype that names the layer — @Service for logic,
@Repository for data access (it adds exception translation), @Controller for web,
@Component for everything else.
@RestController is a convenience annotation that combines **@Controller + @ResponseBody
**. It signals that every handler method returns data serialized into the response body
(typically JSON), not a view name.
@Controller
class PageController {
@GetMapping("/home")
String home() { return "home"; } // "home" = a VIEW NAME to render
}
@RestController // = @Controller + @ResponseBody
class ApiController {
@GetMapping("/users")
List<User> users() { return userService.all(); } // serialized to JSON
}
With plain @Controller you'd annotate each method with @ResponseBody to return data
instead of a view. @RestController applies @ResponseBody to all methods at once, which
is exactly what you want for REST APIs. Use @Controller for server-rendered pages,
@RestController for JSON/XML APIs.
Rule of thumb: Building a JSON API → @RestController. Rendering HTML views (Thymeleaf,
JSP) → @Controller. The difference is "return data" vs "return a view name."
@Autowired tells Spring to inject a matching bean at the annotated point — constructor,
setter, or field. Spring resolves a bean by type, finds the right one in the container,
and supplies it.
@Service
class OrderService {
private final PaymentGateway gateway;
@Autowired // inject a PaymentGateway bean here
OrderService(PaymentGateway gateway) {
this.gateway = gateway;
}
}
Since Spring 4.3, @Autowired is optional on a class's single constructor — Spring
injects its parameters automatically. By default a @Autowired dependency is required;
if no matching bean exists, startup fails. Set @Autowired(required = false) (or use
Optional/@Nullable) to make it optional.
Rule of thumb: @Autowired = "inject the matching bean by type here." With a single
constructor you can omit it entirely — Spring still autowires the parameters.
Spring supports constructor, setter, and field injection. They differ in where the dependency is supplied and in what guarantees you get.
@Service
class A {
// 1. Constructor injection (preferred)
private final Repo repo;
A(Repo repo) { this.repo = repo; }
}
@Service
class B {
// 2. Setter injection
private Repo repo;
@Autowired void setRepo(Repo repo) { this.repo = repo; }
}
@Service
class C {
// 3. Field injection (discouraged)
@Autowired private Repo repo;
}
Constructor injection lets you use final fields (immutability), guarantees the object
is never in a half-built state, and makes dependencies explicit and testable. Setter
injection suits optional/reconfigurable dependencies. Field injection is the most
concise but can't be final, hides dependencies, and needs reflection to test.
Rule of thumb: Default to constructor injection; use setter injection for optional dependencies; avoid field injection except in throwaway/test code.
Constructor injection is the Spring team's recommended style because it produces immutable, fully-initialized, testable objects — none of which field injection gives you.
// Constructor injection — immutable, explicit, testable:
@Service
class OrderService {
private final PaymentGateway gateway; // can be final
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 used half-initialized; (3) makes dependencies visible in
the signature — a constructor with too many params is a smell pointing at a class doing too
much; (4) needs no Spring or reflection to test. Field injection hides dependencies,
forbids final, and silently allows an ever-growing dependency list.
Rule of thumb: Prefer constructor injection — final fields, no half-built objects,
dependencies you can see, and tests that just call new.
Component scanning is the process by which Spring discovers stereotype-annotated classes
and registers them as beans. @ComponentScan (inside @SpringBootApplication) defines
where scanning starts.
@SpringBootApplication // @ComponentScan defaults to THIS class's package
class App { }
// → scans com.example and all sub-packages for @Component/@Service/etc.
// Override the base packages explicitly if needed:
@ComponentScan(basePackages = { "com.example.web", "com.shared.util" })
class Config { }
Scanning starts at the annotated class's package and descends into all sub-packages — which
is exactly why your @SpringBootApplication class must live in a root package above the
rest of your code. Beans in packages outside that tree won't be found unless you add their
package to basePackages. Under the hood Spring uses ASM to read class metadata without
loading every class.
Rule of thumb: Put the main application class in the top-level package so its
@ComponentScan covers everything below; add basePackages only to reach code outside that
tree.
@ComponentScan accepts includeFilters and excludeFilters to narrow or widen
what becomes a bean — by annotation, type, regex, or a custom filter.
@ComponentScan(
basePackages = "com.example",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ANNOTATION, classes = Deprecated.class), // skip @Deprecated
includeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE, classes = SpecialBean.class))
class Config { }
Filter types: ANNOTATION (by annotation present), ASSIGNABLE_TYPE (by class/interface),
REGEX and ASPECTJ (by name pattern), and CUSTOM (your own TypeFilter). This is how
Spring Boot's test slices (@WebMvcTest, @DataJpaTest) limit which beans load — they use
filters to include only the relevant layer.
Rule of thumb: Use excludeFilters to keep unwanted classes out of the context (faster,
cleaner tests); the same mechanism powers Spring Boot's sliced test annotations.
Both are scanned into beans, but @Configuration is specifically for classes that host
@Bean factory methods, and it adds CGLIB proxying so inter-bean method calls
return the shared singleton. @Component with @Bean methods does not get that
guarantee.
@Configuration // proxied: a() called twice returns the SAME bean
class GoodConfig {
@Bean A a() { return new A(); }
@Bean B b() { return new B(a()); } // a() returns the singleton A
}
@Component // "lite" mode: a() here builds a NEW A each call!
class RiskyConfig {
@Bean A a() { return new A(); }
@Bean B b() { return new B(a()); } // a() bypasses the container → duplicate A
}
In @Configuration ("full" mode) Spring intercepts @Bean method calls so they return the
managed singleton. In @Component ("lite" mode) there's no proxy, so calling another
@Bean method directly runs the raw Java and creates a second instance. Use
@Configuration whenever @Bean methods reference each other.
Rule of thumb: Define @Bean methods in @Configuration classes (full mode), not
@Component, so cross-references return the singleton instead of accidental duplicates.
Use @Component when you own the class and can annotate it; use @Bean when you
can't annotate the class — third-party types — or need programmatic construction
logic.
// @Component: your own class, annotate it directly
@Service
class OrderService { }
// @Bean: a third-party class you can't annotate, or custom build logic
@Configuration
class Config {
@Bean
RestClient restClient() { // library class, no @Component possible
return RestClient.builder()
.baseUrl("https://api.example.com")
.build();
}
}
@Component is declarative and concise but only works on classes you can edit.
@Bean gives you a method body — perfect for builders, conditional construction, or
configuring a DataSource/ObjectMapper/RestClient from a library. They're often used
together: stereotypes for your code, @Bean methods for external types.
Rule of thumb: Your class, simple wiring → @Component. Third-party class or
construction that needs real code → @Bean in a @Configuration.
Inject a List or Map of a type and Spring supplies every bean of that type —
a clean way to implement the Strategy pattern or a plugin registry.
interface Notifier { void send(String msg); }
@Component class EmailNotifier implements Notifier { /* ... */ }
@Component class SmsNotifier implements Notifier { /* ... */ }
@Service
class NotificationService {
private final List<Notifier> notifiers; // ALL Notifier beans
NotificationService(List<Notifier> notifiers) { this.notifiers = notifiers; }
void broadcast(String m) { notifiers.forEach(n -> n.send(m)); }
}
Injecting List<Notifier> collects every implementation; injecting Map<String, Notifier>
keys them by bean name, letting you look one up dynamically. Control list ordering with
@Order or Ordered. Adding a new strategy is then just adding a new @Component — no
change to the consuming code.
Rule of thumb: Inject List<T> or Map<String,T> to gather all implementations of an
interface — the open/closed way to wire pluggable strategies.
A @Autowired dependency is required by default, so a missing bean fails startup. Make
it optional with Optional<T>, @Autowired(required = false), @Nullable, or
ObjectProvider<T>.
@Service
class MetricsService {
private final Optional<MetricsExporter> exporter; // empty if no bean exists
MetricsService(Optional<MetricsExporter> exporter) { this.exporter = exporter; }
void record() { exporter.ifPresent(e -> e.export()); }
}
// Alternatives:
@Autowired(required = false) private Cache cache; // left null if absent
MetricsService(@Nullable MetricsExporter e) { } // null if absent
MetricsService(ObjectProvider<MetricsExporter> p) { p.ifAvailable(...); } // lazy + safe
Optional<T> is the cleanest — it documents optionality in the type and avoids null checks.
ObjectProvider<T> additionally handles "zero, one, or many" beans gracefully and resolves
lazily. Plain required = false works but leaves a raw null you must remember to guard.
Rule of thumb: Prefer Optional<T> or ObjectProvider<T> for optional dependencies —
they make "this might be absent" explicit instead of leaving a surprise null.
@DependsOn forces Spring to initialize named beans first, establishing an ordering
that isn't expressed through a direct injected dependency. It's for implicit ordering
requirements.
@Component("schemaInitializer")
class SchemaInitializer { /* creates DB tables on startup */ }
@Component
@DependsOn("schemaInitializer") // ensure schema exists before this bean starts
class DataLoader { /* inserts seed rows, needs tables to exist */ }
Normally Spring infers init order from injected dependencies. But sometimes bean A must run
before bean B even though B doesn't inject A — e.g. one bean has a side effect (registering
a driver, creating a schema, setting a system property) the other relies on. @DependsOn
makes that hidden edge explicit so the container orders them correctly.
Rule of thumb: Use @DependsOn only for side-effect ordering that isn't captured by
injection; if B actually uses A, just inject A and let Spring order them for you.
When two or more beans match an injection point by type, Spring can't choose and throws
NoUniqueBeanDefinitionException. @Primary marks one bean as the default winner
for unqualified injections.
interface PaymentGateway { }
@Component @Primary // chosen when no qualifier is given
class StripeGateway implements PaymentGateway { }
@Component
class PaypalGateway implements PaymentGateway { }
@Service
class Checkout {
Checkout(PaymentGateway gateway) { } // gets StripeGateway (the @Primary)
}
@Primary sets the fallback default while still letting specific injection points opt into
another bean with @Qualifier. It's ideal when there's one "normal" implementation and
others are exceptions. If you need per-injection precision rather than a global default,
use @Qualifier instead.
Rule of thumb: @Primary picks the default among several candidates of one type;
override that default at specific injection points with @Qualifier.
@Profile makes a bean conditional on which profiles are active, so the same codebase
wires different beans per environment (dev, test, prod) without code changes.
@Component
@Profile("dev") // only a bean when the "dev" profile is active
class InMemoryMailSender implements MailSender { }
@Component
@Profile("prod") // only a bean in production
class SmtpMailSender implements MailSender { }
A class (or @Bean method, or whole @Configuration) annotated with @Profile is only
registered when its profile is in spring.profiles.active. You can negate
(@Profile("!prod")) and combine (@Profile({"dev","test"})). This is how you swap a stub
service for a real one between environments while keeping a single MailSender injection
point.
Rule of thumb: Annotate environment-specific beans with @Profile so the active profile
decides which implementation gets wired — no if (env) logic in your code.
Field injection (@Autowired on a private field) is convenient but undermines two things
interviewers probe: testability and immutability. Spring sets the field by
reflection, so nothing outside the container can.
@Service
class ReportService {
@Autowired private Repo repo; // private, non-final, set via reflection
// - Can't be 'final' → mutable, not thread-safe by construction
// - In a plain unit test, 'repo' is null; you must use reflection or
// Spring just to populate it
// - Dependencies are hidden — the class can accumulate many unnoticed
}
Because there's no constructor exposing the dependency, a unit test can't simply pass a
mock — it must use ReflectionTestUtils, Mockito's @InjectMocks, or spin up a Spring
context. The field can't be final, so the object is mutable. And nothing forces you to
notice when the dependency list grows to ten. Constructor injection fixes all three.
Rule of thumb: Field injection trades a few keystrokes for untestable, mutable, opaque
classes — switch to constructor injection so tests are plain new calls and fields are
final.
More Dependency Injection interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.