Skip to content

IoC container & beans

Mid / Senior

Dependency injection, stereotype annotations, @Bean vs @Component, the bean lifecycle, scopes, and circular dependencies.

Created 5 October 2026 · Updated 23 October 2026

19 concepts · detail hidden

  1. 01Inversion of Control and Dependency Injection

    Inversion of Control (IoC) means your classes don’t build or look up their own collaborators — something outside them (the container) does it and hands them in. Dependency Injection (DI) is the specific form of IoC Spring uses: dependencies are passed in through the constructor, a setter, or a field.

    @Service
    public class OrderService {
        private final PaymentClient payments;      // no "new PaymentClient()" here
    
        public OrderService(PaymentClient payments) {   // container supplies it
            this.payments = payments;
        }
    }
    

    Why it matters: classes depend on abstractions, are trivially testable (pass a mock into the constructor), and cross-cutting behaviour (transactions, security, caching) can be added by the container wrapping the object in a proxy — which is only possible because the container, not your code, creates the object.

    An object created and managed by the container is a bean. The container itself is the ApplicationContext.

    #c-inversion-of-control-and-dependency-injection
  2. 02BeanFactory vs ApplicationContext
    • BeanFactory — the core interface: creates beans, wires dependencies, returns them by name/type. Lazy by default.
    • ApplicationContext — extends BeanFactory and adds what real applications need: eager instantiation of singletons at startup (so wiring errors fail fast), automatic registration of BeanPostProcessors / BeanFactoryPostProcessors, event publishing (ApplicationEventPublisher), i18n (MessageSource), resource loading, and the Environment (profiles, properties).

    In practice you always use an ApplicationContext (Spring Boot creates e.g. AnnotationConfigServletWebServerApplicationContext). The interview answer: “BeanFactory is the low-level DI engine; ApplicationContext is the full container built on top of it.”

    #c-beanfactory-vs-applicationcontext
  3. 03Stereotype annotations — @Component vs @Service vs @Repository vs @Controller

    Interview answer: “For component scanning there’s no difference — @Service, @Repository and @Controller are all meta-annotated with @Component, so they’re registered as beans identically. @Service adds no behaviour; it’s purely semantic. @Repository does add behaviour: persistence exception translation into Spring’s DataAccessException hierarchy. @Controller makes Spring MVC treat the class as a web handler.” The trap is stopping at “they’re all the same”.

    All four mark a class for component scanning — @Service, @Repository and @Controller are themselves meta-annotated with @Component, so the scanner registers all of them as beans the same way. The differences:

    Annotation Extra behaviour
    @Component None — generic bean.
    @Service None today. Purely semantic: marks the business/service layer. Useful as a pointcut target and for readability.
    @Repository Exception translation. PersistenceExceptionTranslationPostProcessor wraps @Repository beans in a proxy that converts persistence-specific exceptions (Hibernate’s, JPA’s, JDBC’s) into Spring’s unchecked DataAccessException hierarchy (DataIntegrityViolationException, OptimisticLockingFailureException, …). Boot registers that post-processor automatically.
    @Controller Detected by Spring MVC’s RequestMappingHandlerMapping as a web handler, so its @RequestMapping/@GetMapping methods become endpoints.
    @RestController @Controller + @ResponseBody — return values are serialized (JSON) into the response body instead of being resolved as view names.

    The point of @Repository translation: your service layer catches one technology-neutral exception type, and swapping JDBC for JPA doesn’t change your catch blocks.

    Where @Repository actually earns its keep:

    • Spring Data repository interfaces (JpaRepository and friends) already get exception translation automatically, so putting @Repository on them is redundant.
    • JdbcTemplate-based DAOs don’t strictly need it either — JdbcTemplate already translates SQLExceptions into DataAccessExceptions itself.
    • Your own DAO classes that use EntityManager or Hibernate’s Session directly are where it matters: without @Repository, their raw JPA/Hibernate exceptions leak to the service layer untranslated.
    #c-stereotype-annotations-component-vs-service-vs-repository-vs
  4. 04@Component on an interface vs on a class

    Annotate the implementation class. Scanning registers concrete classes; an annotated interface is ignored (Spring Data repository interfaces are the exception — they’re picked up by @EnableJpaRepositories scanning, not component scanning, and no annotation is needed).

    #c-component-on-an-interface-vs-on-a-class
  5. 05@Bean vs @Component

    Both produce beans; they differ in who writes the creation code.

    • @Component (and stereotypes) goes on your own class. Spring discovers it via classpath scanning and instantiates it by calling a constructor.
    • @Bean goes on a method inside a @Configuration class. You write the code that builds the object, and the method’s return value becomes the bean.
    @Configuration
    public class HttpConfig {
        @Bean
        public RestClient paymentsClient(RestClient.Builder builder) {
            return builder.baseUrl("https://payments.internal").build();
        }
    }
    

    Use @Bean when you can’t annotate the class (third-party library: ObjectMapper, RestClient, DataSource), when construction needs logic or configuration, or when you need several differently configured beans of the same type. Use @Component for your own application classes.

    #c-bean-vs-component
  6. 06@Configuration full mode vs lite mode

    A @Configuration class is subclassed with CGLIB at startup. The proxy intercepts calls between @Bean methods so that calling another @Bean method returns the existing singleton instead of constructing a new object:

    @Configuration
    public class AppConfig {
        @Bean DataSource dataSource() { return new HikariDataSource(); }
    
        @Bean JdbcTemplate jdbcTemplate() {
            return new JdbcTemplate(dataSource());   // same singleton, thanks to CGLIB
        }
    }
    

    In lite mode — @Bean methods inside a plain @Component, or @Configuration(proxyBeanMethods = false) — there is no interception, so dataSource() above would create a second HikariDataSource. Spring Boot’s own auto-configuration classes use proxyBeanMethods = false for faster startup and inject dependencies as method parameters instead (jdbcTemplate(DataSource ds)), which is the safe style in either mode.

    #c-configuration-full-mode-vs-lite-mode
  7. 07Constructor vs field vs setter injection

    Interview answer: “Constructor injection is the recommended default: dependencies are final and explicit, the object can’t exist half-initialised, and a unit test just calls new with mocks. Setter injection is for optional dependencies. Field injection is concise but hides dependencies and makes the class untestable without Spring or reflection.”

    // 1. Constructor injection — recommended
    @Service
    public class OrderService {
        private final OrderRepository orders;
        private final PaymentClient payments;
    
        public OrderService(OrderRepository orders, PaymentClient payments) { // no @Autowired needed
            this.orders = orders;
            this.payments = payments;
        }
    }
    
    // 2. Setter injection — optional / re-configurable dependencies
    @Service
    public class ReportService {
        private MetricsRecorder metrics = MetricsRecorder.NOOP;
    
        @Autowired(required = false)
        public void setMetrics(MetricsRecorder metrics) { this.metrics = metrics; }
    }
    
    // 3. Field injection — avoid in production code
    @Service
    public class LegacyService {
        @Autowired private OrderRepository orders;   // can't be final, invisible from outside
    }
    
    • Constructor injection: dependencies can be final (immutable, safely published to other threads), the object is never half-built, required dependencies are explicit in the signature, and a unit test is just new OrderService(mockRepo, mockClient) — no Spring, no reflection. Missing beans fail at startup. A constructor with 7+ parameters is a useful smell that the class does too much; field injection hides that smell.
    • Setter injection: for genuinely optional dependencies (with a sensible default) or ones that must be re-configurable after construction.
    • Field injection: concise, but hides dependencies, prevents final, needs reflection (ReflectionTestUtils) or a Spring context to test, lets the object exist with null fields when created outside Spring, and allows circular dependencies to go unnoticed. Acceptable in test classes (@Autowired MockMvc mvc;) where the test framework creates the instance.

    With Lombok, @RequiredArgsConstructor on the class generates the constructor for all final fields — constructor injection without the boilerplate.

    #c-constructor-vs-field-vs-setter-injection
  8. 08@Autowired — how it works and how to use it properly

    @Autowired tells Spring “inject a bean here”. It’s processed by AutowiredAnnotationBeanPostProcessor while the bean is being created. It can go on a constructor, a setter (or any method), or a field.

    Resolution rules:

    1. Find beans by type that are assignable to the declared type (interfaces are matched by their implementations).
    2. Exactly one → inject it.
    3. Several → narrow by @Qualifier, then @Primary, then by matching the parameter/field name to a bean name; still ambiguous → NoUniqueBeanDefinitionException.
    4. None → NoSuchBeanDefinitionException at startup (because required = true by default).

    When you need @Autowired and when you don’t:

    • Single constructor — not needed: since Spring 4.3, a class with exactly one constructor has it used for injection automatically, and @Autowired on it adds nothing. Leaving it off keeps the class free of Spring annotations.
    • Several constructors — mark the one Spring should use with @Autowired.
    • Setter / field injection — required.
    • @Bean method parameters — not needed; they’re autowired automatically.

    Optional dependencies:

    public NotificationService(Optional<SmsGateway> sms) { … }          // empty if no bean
    public NotificationService(ObjectProvider<SmsGateway> sms) {        // lazy + optional
        sms.ifAvailable(SmsGateway::warmUp);
    }
    @Autowired(required = false) void setSms(SmsGateway sms) { … }      // setter not called
    

    ObjectProvider<T> is the most flexible: getIfAvailable(), getIfUnique(), lazy lookup, and a fresh instance per call for prototype beans.

    Injecting many beans: List<Validator> / Set<…> gets every bean of that type (ordered by @Order), Map<String, Validator> gets them keyed by bean name — ideal for strategy/plugin patterns.

    @Autowired vs @Inject vs @Resource:

    Source Resolves by Notes
    @Autowired Spring type, then qualifier/name has required
    @Inject Jakarta (JSR-330) type same behaviour, no required; use Optional/Provider
    @Resource Jakarta (JSR-250) name first, then type fields and setters only, not constructors

    Where @Autowired doesn’t work:

    • Objects you create with new — Spring didn’t create them, so nothing is injected; the field stays null. The most common cause of a NullPointerException in a “Spring bean”.
    • static fields — Spring injects into instances, not classes (it logs a warning and skips them). Needing a static is usually a design smell; use an instance bean.
    • Before construction finishes — injected fields are still null inside the constructor; use constructor parameters, or @PostConstruct for initialisation logic.
    • Classes not scanned — outside the @SpringBootApplication package tree, or missing a stereotype annotation → NoSuchBeanDefinitionException for whoever needs them.
    • In BeanPostProcessors / BeanFactoryPostProcessors — they’re created very early, and beans they depend on are created too early to be proxied (Spring logs “is not eligible for getting processed by all BeanPostProcessors”).
    #c-autowired-how-it-works-and-how-to-use-it-properly
  9. 09Good DI design practices
    • Depend on interfaces when there are (or will be) several implementations or when the boundary matters (payment gateway, clock, external client). Don’t create an interface for every single class “just because” — one implementation and no test seam needed means no interface needed.
    • Inject only what the class uses. Injecting ApplicationContext and calling getBean() (service locator) hides dependencies and couples the class to Spring.
    • Inject configuration as typed objects (@ConfigurationProperties records) rather than scattered @Value strings.
    • Make time and randomness injectable — inject a java.time.Clock bean instead of calling LocalDateTime.now(), so tests can fix the time.
    • Keep beans stateless, since singletons are shared across threads.
    • Avoid circular dependencies by design (extract a third bean or use events) rather than @Lazy workarounds.
    • Prefer final + constructor; treat a growing constructor as a signal to split the class.
    • Unit-test without Spring (new + Mockito mocks); reserve @SpringBootTest for integration tests — fast, focused tests are a direct payoff of constructor injection.
    #c-good-di-design-practices
  10. 10Resolving multiple candidates — @Primary, @Qualifier, and collections

    Injection is by type first. If several beans match:

    1. @Qualifier("name") at the injection point picks one explicitly.
    2. Otherwise a bean marked @Primary wins.
    3. Otherwise Spring falls back to matching the parameter/field name against bean names.
    4. Otherwise: NoUniqueBeanDefinitionException at startup.

    Injecting List<PaymentProvider> or Map<String, PaymentProvider> gives you all matching beans (ordered by @Order / Ordered; the map is keyed by bean name) — the idiomatic way to build a strategy pattern without a switch.

    #c-resolving-multiple-candidates-primary-qualifier-and-collecti
  11. 11The bean lifecycle — from definition to destruction
    1. Bean definitions loaded — from scanning, @Bean methods, auto-configuration.
    2. BeanFactoryPostProcessors run — they modify definitions before any bean exists (e.g. PropertySourcesPlaceholderConfigurer resolves ${...} placeholders).
    3. Instantiation — constructor called (constructor injection happens here).
    4. Populate properties — field and setter injection.
    5. *Aware callbacks — BeanNameAware, ApplicationContextAware, …
    6. BeanPostProcessor.postProcessBeforeInitialization.
    7. Initialization — @PostConstruct, then InitializingBean.afterPropertiesSet(), then a custom initMethod.
    8. BeanPostProcessor.postProcessAfterInitialization — this is where proxies are created (AOP, @Transactional, @Async). What gets stored in the container and injected elsewhere may be the proxy, not your object.
    9. Bean in use.
    10. Destruction on context close (singletons only) — @PreDestroy, then DisposableBean.destroy(), then a custom destroyMethod.

    Practical consequence: in @PostConstruct dependencies are injected, but calling your own @Transactional method from there won’t be transactional — you’re on the raw object and the proxy may not exist yet. Use an ApplicationReadyEvent listener for startup work that needs the full machinery.

    #c-the-bean-lifecycle-from-definition-to-destruction
  12. 12BeanPostProcessor vs BeanFactoryPostProcessor
    • BeanFactoryPostProcessor works on bean definitions (metadata) before any bean is instantiated — change property values, scopes, register new definitions (BeanDefinitionRegistryPostProcessor). ConfigurationClassPostProcessor, which processes @Configuration/@ComponentScan/@Import, is one.
    • BeanPostProcessor works on bean instances after creation, around initialization; it can return a different object (a proxy). AutowiredAnnotationBeanPostProcessor (handles @Autowired), CommonAnnotationBeanPostProcessor (@PostConstruct) and the AOP auto-proxy creator are all BeanPostProcessors.

    Mnemonic: factory post-processor = blueprints; bean post-processor = finished objects.

    #c-beanpostprocessor-vs-beanfactorypostprocessor
  13. 13Bean scopes and the prototype-in-singleton problem
    Scope Instances
    singleton (default) One per container. Not a JVM singleton — one per ApplicationContext.
    prototype New instance every time it’s requested. Spring doesn’t manage its destruction (@PreDestroy isn’t called).
    request One per HTTP request (web only).
    session One per HTTP session.
    application One per ServletContext.

    Problem: injecting a prototype (or request) bean into a singleton injects it once, at singleton creation — you get the same instance forever. Fixes:

    • inject ObjectProvider<MyPrototype> and call getObject() each time;
    • @Lookup method that Spring overrides to fetch a fresh instance;
    • a scoped proxy — @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) — which injects a proxy that delegates to the correct current instance on every call (how request-scoped beans are normally injected into singletons).
    #c-bean-scopes-and-the-prototype-in-singleton-problem
  14. 14Spring singleton scope vs the GoF Singleton pattern

    They share a name but not a meaning. The GoF Singleton is one instance per classloader, enforced by the class itself (private constructor, static accessor). A Spring singleton is one instance per container per bean definition — you can define two beans of the same class, and two ApplicationContexts each have their own. The class stays a normal, testable class.

    #c-spring-singleton-scope-vs-the-gof-singleton-pattern
  15. 15Singleton beans and thread safety

    A singleton bean is shared by every request thread. Spring provides no synchronization. Singletons are safe when they are stateless (only final references to other stateless beans/config). Mutable instance fields — a counter, a “current user” field, a non-thread-safe SimpleDateFormat — are race conditions. Keep per-request state in local variables, method parameters, request-scoped beans, or thread-safe structures (ConcurrentHashMap, AtomicLong).

    #c-singleton-beans-and-thread-safety
  16. 16Circular dependencies

    A needs B, B needs A.

    • Constructor injection on both sides cannot work — neither can be constructed first — and fails at startup with BeanCurrentlyInCreationException.
    • Field/setter injection historically worked: Spring instantiates A, exposes an early reference to it (the “three-level cache”: singletonObjects, earlySingletonObjects, singletonFactories), creates B with that early reference, then finishes A. The early-reference factory is also what lets the cycle work when A gets an AOP proxy.
    • Since Spring Boot 2.6, circular references are prohibited by default (spring.main.allow-circular-references=false).

    The right fix is design: extract the shared logic into a third bean, or use events. A quick workaround is @Lazy on one injection point (injects a lazy-resolution proxy).

    #c-circular-dependencies
  17. 17@Lazy, @Profile and @Conditional
    • @Lazy — create the bean on first use instead of at startup (on a class/@Bean), or inject a lazy proxy (on an injection point). Speeds startup, but defers wiring errors to runtime. spring.main.lazy-initialization=true makes everything lazy.
    • @Profile("dev") — register the bean only when that profile is active (spring.profiles.active). Supports expressions: @Profile("!prod").
    • @Conditional — the general mechanism behind profiles and all of Boot’s auto-configuration: @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty, … (see the Spring Boot subtopic).
    #c-lazy-profile-and-conditional
  18. 18@Value vs @ConfigurationProperties — typed configuration objects

    Interview answer: “@Value injects one property by its string key. For anything more than a value or two I bind a whole prefix to a typed, immutable @ConfigurationProperties record: the keys live in one place, values are converted and validated at startup, it’s trivial to unit-test, and the IDE autocompletes the properties.”

    Given this configuration:

    payments:
      base-url: https://payments.internal
      timeout: 5s
      retries: 3
      allowed-currencies: [PLN, EUR, USD]
    

    Scattered @Value style — every class that needs a setting repeats its string key:

    @Service
    public class PaymentClient {
        public PaymentClient(@Value("${payments.base-url}") String baseUrl,
                             @Value("${payments.timeout:5s}") Duration timeout,
                             @Value("${payments.retries:3}") int retries) { … }
    }
    
    @Service
    public class RefundService {
        public RefundService(@Value("${payments.timout:10s}") Duration timeout) { … }
        //                               ^ typo → silently uses the 10s default, forever
    }
    

    Problems: the same keys are copied as strings across classes, and each copy can have a different default (5s here, 10s there). A typo in a key that has a default doesn’t fail, it quietly uses the default. Nothing checks that retries is positive or that base-url is present until a request fails. There’s no single place to see what the payments module can be configured with. A unit test has to fake the property source or pass values one by one.

    Typed @ConfigurationProperties style — one class owns the whole payments.* prefix:

    @Validated
    @ConfigurationProperties(prefix = "payments")
    public record PaymentsProperties(
            @NotNull URI baseUrl,
            @DefaultValue("5s") Duration timeout,
            @DefaultValue("3") @Min(0) @Max(10) int retries,
            @DefaultValue({"PLN", "EUR"}) Set<Currency> allowedCurrencies) {}
    
    @SpringBootApplication
    @ConfigurationPropertiesScan          // or @EnableConfigurationProperties(PaymentsProperties.class)
    public class App { … }
    
    @Service
    public class PaymentClient {
        public PaymentClient(PaymentsProperties props) {   // injected like any other bean
            this.timeout = props.timeout();
        }
    }
    

    What this gives you:

    • Single source of truth. Keys, types and defaults are declared once. Consumers depend on PaymentsProperties, not on string keys, so a rename is one refactor and the compiler finds every use.
    • Type conversion. "5s" → Duration, "10MB" → DataSize, a list → Set<Currency>, nested objects and maps (payments.providers.stripe.api-key → Map<String, Provider>). @Value can convert simple values too, but it can’t bind lists of objects or nested structures.
    • Fail-fast validation. With @Validated and Jakarta constraints (spring-boot-starter-validation), a missing base-url or retries: 50 stops the application at startup with a clear message, not at 3 a.m. on the first payment.
    • Immutability. A record is bound through its constructor, so the values can’t be changed at runtime and the object is safe to share between threads.
    • Relaxed binding. base-url, baseUrl, base_url and the environment variable PAYMENTS_BASEURL all bind to baseUrl. That’s how the same class works with YAML locally and env vars in Kubernetes. (@Value has only limited relaxed binding.)
    • Easy unit tests. new PaymentClient(new PaymentsProperties(URI.create("http://localhost"), Duration.ofSeconds(1), 0, Set.of())), with no Spring context or property files.
    • IDE support and documentation. Add the spring-boot-configuration-processor annotation processor and Boot generates META-INF/spring-configuration-metadata.json, so IntelliJ / VS Code autocomplete payments.* in application.yml and flag unknown keys.
    • Visibility at runtime. Actuator’s /actuator/configprops shows each properties bean with its bound values (secrets masked).

    Rules of thumb:

    • One properties class per module or feature prefix (payments, storage, mail), not one giant AppProperties.
    • Inject the properties object only where it’s needed, or pass just the values a class uses.
    • Don’t put @Component on a constructor-bound record; register it with @ConfigurationPropertiesScan or @EnableConfigurationProperties.
    • Keep @Value for a truly one-off value or when you need a SpEL expression (@Value("#{systemProperties['user.region']}")). @ConfigurationProperties doesn’t evaluate SpEL.
    • Secrets still come from environment variables or a secret store; the properties class only defines the shape, not where the value comes from.
    @Value @ConfigurationProperties
    Granularity one key a whole prefix / tree
    Lists of objects, nested structures, maps no yes
    Relaxed binding limited full
    Validation at startup no yes (@Validated)
    IDE metadata / autocomplete no yes (with the processor)
    SpEL yes no
    #c-value-vs-configurationproperties-typed-configuration-objects
  19. 19Application events

    ApplicationEventPublisher.publishEvent(new OrderPlaced(id)) and @EventListener void on(OrderPlaced e) decouple a producer from its side effects. Listeners run synchronously, in the publisher’s thread (and transaction) by default; add @Async to run them on another thread. For “do this only if the transaction committed” (send an email after an order is saved), use @TransactionalEventListener — see the transactions subtopic.

    #c-application-events