Skip to content

Component Annotations Interview Questions & Answers

16 questions Updated 2026-06-26 Share:

Spring stereotype annotations explained — @Component, @Service, @Repository, @Controller, @RestController, @Autowired, component scanning, and the difference between constructor, setter, and field injection.

Read the in-depth guideSpring Stereotype Annotations and Injection Styles, Explained(opens in new tab)
16 of 16

@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 ways to practice

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

Join our WhatsApp Channel