IoC container & beans
Mid / SeniorDependency 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
01Inversion of Control and Dependency Injection
#c-inversion-of-control-and-dependency-injectionInversion 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.02BeanFactory vs ApplicationContext
#c-beanfactory-vs-applicationcontextBeanFactory— the core interface: creates beans, wires dependencies, returns them by name/type. Lazy by default.ApplicationContext— extendsBeanFactoryand adds what real applications need: eager instantiation of singletons at startup (so wiring errors fail fast), automatic registration ofBeanPostProcessors /BeanFactoryPostProcessors, event publishing (ApplicationEventPublisher), i18n (MessageSource), resource loading, and theEnvironment(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.”03Stereotype annotations — @Component vs @Service vs @Repository vs @Controller
#c-stereotype-annotations-component-vs-service-vs-repository-vsInterview answer: “For component scanning there’s no difference —
@Service,@Repositoryand@Controllerare all meta-annotated with@Component, so they’re registered as beans identically.@Serviceadds no behaviour; it’s purely semantic.@Repositorydoes add behaviour: persistence exception translation into Spring’sDataAccessExceptionhierarchy.@Controllermakes 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,@Repositoryand@Controllerare themselves meta-annotated with@Component, so the scanner registers all of them as beans the same way. The differences:Annotation Extra behaviour @ComponentNone — generic bean. @ServiceNone today. Purely semantic: marks the business/service layer. Useful as a pointcut target and for readability. @RepositoryException translation. PersistenceExceptionTranslationPostProcessorwraps@Repositorybeans in a proxy that converts persistence-specific exceptions (Hibernate’s, JPA’s, JDBC’s) into Spring’s uncheckedDataAccessExceptionhierarchy (DataIntegrityViolationException,OptimisticLockingFailureException, …). Boot registers that post-processor automatically.@ControllerDetected by Spring MVC’s RequestMappingHandlerMappingas a web handler, so its@RequestMapping/@GetMappingmethods become endpoints.@RestController@Controller+@ResponseBody— return values are serialized (JSON) into the response body instead of being resolved as view names.The point of
@Repositorytranslation: your service layer catches one technology-neutral exception type, and swapping JDBC for JPA doesn’t change yourcatchblocks.Where
@Repositoryactually earns its keep:- Spring Data repository interfaces (
JpaRepositoryand friends) already get exception translation automatically, so putting@Repositoryon them is redundant. JdbcTemplate-based DAOs don’t strictly need it either —JdbcTemplatealready translatesSQLExceptions intoDataAccessExceptions itself.- Your own DAO classes that use
EntityManageror Hibernate’sSessiondirectly are where it matters: without@Repository, their raw JPA/Hibernate exceptions leak to the service layer untranslated.
- Spring Data repository interfaces (
04@Component on an interface vs on a class
#c-component-on-an-interface-vs-on-a-classAnnotate the implementation class. Scanning registers concrete classes; an annotated interface is ignored (Spring Data repository interfaces are the exception — they’re picked up by
@EnableJpaRepositoriesscanning, not component scanning, and no annotation is needed).05@Bean vs @Component
#c-bean-vs-componentBoth 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.@Beangoes on a method inside a@Configurationclass. 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
@Beanwhen 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@Componentfor your own application classes.06@Configuration full mode vs lite mode
#c-configuration-full-mode-vs-lite-modeA
@Configurationclass is subclassed with CGLIB at startup. The proxy intercepts calls between@Beanmethods so that calling another@Beanmethod 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 —
@Beanmethods inside a plain@Component, or@Configuration(proxyBeanMethods = false)— there is no interception, sodataSource()above would create a secondHikariDataSource. Spring Boot’s own auto-configuration classes useproxyBeanMethods = falsefor faster startup and inject dependencies as method parameters instead (jdbcTemplate(DataSource ds)), which is the safe style in either mode.07Constructor vs field vs setter injection
#c-constructor-vs-field-vs-setter-injectionInterview answer: “Constructor injection is the recommended default: dependencies are
finaland explicit, the object can’t exist half-initialised, and a unit test just callsnewwith 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 justnew 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 withnullfields 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,
@RequiredArgsConstructoron the class generates the constructor for allfinalfields — constructor injection without the boilerplate.- Constructor injection: dependencies can be
08@Autowired — how it works and how to use it properly
#c-autowired-how-it-works-and-how-to-use-it-properly@Autowiredtells Spring “inject a bean here”. It’s processed byAutowiredAnnotationBeanPostProcessorwhile the bean is being created. It can go on a constructor, a setter (or any method), or a field.Resolution rules:
- Find beans by type that are assignable to the declared type (interfaces are matched by their implementations).
- Exactly one → inject it.
- Several → narrow by
@Qualifier, then@Primary, then by matching the parameter/field name to a bean name; still ambiguous →NoUniqueBeanDefinitionException. - None →
NoSuchBeanDefinitionExceptionat startup (becauserequired = trueby default).
When you need
@Autowiredand 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
@Autowiredon 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.
@Beanmethod 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 calledObjectProvider<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.@Autowiredvs@Injectvs@Resource:Source Resolves by Notes @AutowiredSpring type, then qualifier/name has required@InjectJakarta (JSR-330) type same behaviour, no required; useOptional/Provider@ResourceJakarta (JSR-250) name first, then type fields and setters only, not constructors Where
@Autowireddoesn’t work:- Objects you create with
new— Spring didn’t create them, so nothing is injected; the field staysnull. The most common cause of aNullPointerExceptionin a “Spring bean”. staticfields — 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
nullinside the constructor; use constructor parameters, or@PostConstructfor initialisation logic. - Classes not scanned — outside the
@SpringBootApplicationpackage tree, or missing a stereotype annotation →NoSuchBeanDefinitionExceptionfor 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”).
09Good DI design practices
#c-good-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
ApplicationContextand callinggetBean()(service locator) hides dependencies and couples the class to Spring. - Inject configuration as typed objects (
@ConfigurationPropertiesrecords) rather than scattered@Valuestrings. - Make time and randomness injectable — inject a
java.time.Clockbean instead of callingLocalDateTime.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
@Lazyworkarounds. - Prefer
final+ constructor; treat a growing constructor as a signal to split the class. - Unit-test without Spring (
new+ Mockito mocks); reserve@SpringBootTestfor integration tests — fast, focused tests are a direct payoff of constructor injection.
10Resolving multiple candidates — @Primary, @Qualifier, and collections
#c-resolving-multiple-candidates-primary-qualifier-and-collectiInjection is by type first. If several beans match:
@Qualifier("name")at the injection point picks one explicitly.- Otherwise a bean marked
@Primarywins. - Otherwise Spring falls back to matching the parameter/field name against bean names.
- Otherwise:
NoUniqueBeanDefinitionExceptionat startup.
Injecting
List<PaymentProvider>orMap<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 aswitch.11The bean lifecycle — from definition to destruction
#c-the-bean-lifecycle-from-definition-to-destruction- Bean definitions loaded — from scanning,
@Beanmethods, auto-configuration. BeanFactoryPostProcessors run — they modify definitions before any bean exists (e.g.PropertySourcesPlaceholderConfigurerresolves${...}placeholders).- Instantiation — constructor called (constructor injection happens here).
- Populate properties — field and setter injection.
*Awarecallbacks —BeanNameAware,ApplicationContextAware, …BeanPostProcessor.postProcessBeforeInitialization.- Initialization —
@PostConstruct, thenInitializingBean.afterPropertiesSet(), then a custominitMethod. 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.- Bean in use.
- Destruction on context close (singletons only) —
@PreDestroy, thenDisposableBean.destroy(), then a customdestroyMethod.
Practical consequence: in
@PostConstructdependencies are injected, but calling your own@Transactionalmethod from there won’t be transactional — you’re on the raw object and the proxy may not exist yet. Use anApplicationReadyEventlistener for startup work that needs the full machinery.- Bean definitions loaded — from scanning,
12BeanPostProcessor vs BeanFactoryPostProcessor
#c-beanpostprocessor-vs-beanfactorypostprocessorBeanFactoryPostProcessorworks 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.BeanPostProcessorworks 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 allBeanPostProcessors.
Mnemonic: factory post-processor = blueprints; bean post-processor = finished objects.
13Bean scopes and the prototype-in-singleton problem
#c-bean-scopes-and-the-prototype-in-singleton-problemScope Instances singleton(default)One per container. Not a JVM singleton — one per ApplicationContext.prototypeNew instance every time it’s requested. Spring doesn’t manage its destruction ( @PreDestroyisn’t called).requestOne per HTTP request (web only). sessionOne per HTTP session. applicationOne per ServletContext.Problem: injecting a
prototype(orrequest) bean into a singleton injects it once, at singleton creation — you get the same instance forever. Fixes:- inject
ObjectProvider<MyPrototype>and callgetObject()each time; @Lookupmethod 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).
- inject
14Spring singleton scope vs the GoF Singleton pattern
#c-spring-singleton-scope-vs-the-gof-singleton-patternThey 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.15Singleton beans and thread safety
#c-singleton-beans-and-thread-safetyA singleton bean is shared by every request thread. Spring provides no synchronization. Singletons are safe when they are stateless (only
finalreferences to other stateless beans/config). Mutable instance fields — a counter, a “current user” field, a non-thread-safeSimpleDateFormat— are race conditions. Keep per-request state in local variables, method parameters, request-scoped beans, or thread-safe structures (ConcurrentHashMap,AtomicLong).16Circular dependencies
#c-circular-dependenciesAneedsB,BneedsA.- 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), createsBwith that early reference, then finishesA. The early-reference factory is also what lets the cycle work whenAgets 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
@Lazyon one injection point (injects a lazy-resolution proxy).- Constructor injection on both sides cannot work — neither can be constructed first —
and fails at startup with
17@Lazy, @Profile and @Conditional
#c-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=truemakes 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).
18@Value vs @ConfigurationProperties — typed configuration objects
#c-value-vs-configurationproperties-typed-configuration-objectsInterview answer: “
@Valueinjects one property by its string key. For anything more than a value or two I bind a whole prefix to a typed, immutable@ConfigurationPropertiesrecord: 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
@Valuestyle — 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
retriesis positive or thatbase-urlis present until a request fails. There’s no single place to see what thepaymentsmodule can be configured with. A unit test has to fake the property source or pass values one by one.Typed
@ConfigurationPropertiesstyle — one class owns the wholepayments.*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>).@Valuecan convert simple values too, but it can’t bind lists of objects or nested structures. - Fail-fast validation. With
@Validatedand Jakarta constraints (spring-boot-starter-validation), a missingbase-urlorretries: 50stops 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_urland the environment variablePAYMENTS_BASEURLall bind tobaseUrl. That’s how the same class works with YAML locally and env vars in Kubernetes. (@Valuehas 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-processorannotation processor and Boot generatesMETA-INF/spring-configuration-metadata.json, so IntelliJ / VS Code autocompletepayments.*inapplication.ymland flag unknown keys. - Visibility at runtime. Actuator’s
/actuator/configpropsshows 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 giantAppProperties. - Inject the properties object only where it’s needed, or pass just the values a class uses.
- Don’t put
@Componenton a constructor-bound record; register it with@ConfigurationPropertiesScanor@EnableConfigurationProperties. - Keep
@Valuefor a truly one-off value or when you need a SpEL expression (@Value("#{systemProperties['user.region']}")).@ConfigurationPropertiesdoesn’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@ConfigurationPropertiesGranularity 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 - Single source of truth. Keys, types and defaults are declared once. Consumers depend on
19Application events
#c-application-eventsApplicationEventPublisher.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@Asyncto 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.