Everything else in Spring sits on top of this. The container creates your objects, wires them together, and wraps some of them in proxies — and almost every surprising thing Spring does is explained by one of those three facts.
Dependency injection
What is dependency injection?
An object does not create or look up what it depends on. Something else supplies them. In Spring that something else is the container.
What does it actually buy you?
- You can substitute the dependency. A test passes a mock; production passes the real thing. Without DI the collaborator is hard-coded inside a constructor and there is no seam.
- The class stops knowing how to build its world. It declares what it needs and nothing about where it comes from.
- Wiring lives in one place instead of being smeared across every
new.
Is dependency injection a design pattern?
It is a specific form of inversion of control. IoC is the principle — control over flow and construction is handed to a framework; DI is the mechanism.
Why inject through interfaces?
Because the caller then depends on a contract rather than an implementation, so the
implementation can be swapped, decorated or proxied without the caller changing. That last one is
not theoretical: it is exactly how @Transactional works.
The three ways to inject, and which to use
// Constructor injection - the one to use.
// @RequiredArgsConstructor generates the constructor over the final fields, and since
// Spring 4.3 a single constructor needs no @Autowired at all.
@Service
@RequiredArgsConstructor
public class OrderPlacedListener {
private final CustomerOrderService orderService;
private final MailService mailService;
private final OrderMessagePublisher messagePublisher;
}// Field injection - concise, and worse.
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductDAO productDAO;
}Why is constructor injection preferred?
- The fields can be
final, so the object is immutable and fully formed the moment it exists. - A missing dependency fails at startup, not on the first call.
- The class can be built with
newin a plain unit test, with no Spring at all. - A constructor with eight arguments is visibly wrong. Eight
@Autowiredfields look fine, which is how classes quietly grow into god objects.
Setter injection is the third option, and is only worth reaching for when a dependency is genuinely optional or genuinely needs to change after construction. That is rare.
The ApplicationContext
What is the application context?
Spring's container. It reads your configuration, instantiates the beans, injects their
dependencies, applies post-processing, publishes lifecycle events, and holds the lot until it
closes. ApplicationContext extends BeanFactory and adds the things real
applications need — event publishing, message resolution, resource loading, automatic
BeanPostProcessor registration.
How do you create one?
// In a Spring Boot application you never write this - SpringApplication.run does it,
// and returns the context it created.
ApplicationContext context =
SpringApplication.run(PizzaSpringbootBackendApplication.class, args);
// Plain Spring, for reference:
var ctx = new AnnotationConfigApplicationContext(AppConfig.class);
var ctx2 = new ClassPathXmlApplicationContext("beans.xml"); // legacyWhat is the container's lifecycle?
- Bean definitions are loaded — from component scanning,
@Beanmethods, XML. BeanFactoryPostProcessors run and may edit those definitions.- Beans are instantiated, and their dependencies injected.
BeanPostProcessors run around each bean's initialisation.- The context is ready;
ContextRefreshedEventis published. - On shutdown, destruction callbacks run in reverse order.
How do you close it properly? Does Spring Boot do it for you?
Yes. SpringApplication.run registers a JVM shutdown hook, so a normal exit closes
the context and runs every destroy callback. Written by hand, use try-with-resources —
ConfigurableApplicationContext is AutoCloseable.
The bean lifecycle
Describe the lifecycle of a bean.
- Instantiate — the constructor runs, and constructor injection happens here.
- Populate — fields and setters are injected.
- Aware callbacks —
BeanNameAware,ApplicationContextAwareand friends. BeanPostProcessor.postProcessBeforeInitialization.@PostConstruct.InitializingBean.afterPropertiesSet(), then any custominitMethod.BeanPostProcessor.postProcessAfterInitialization— this is where proxies are created.- Bean is in use.
@PreDestroy, thenDisposableBean.destroy(), then any customdestroyMethod.
Step 7 is the one worth remembering. The object your @PostConstruct ran on is not
necessarily the object other beans get injected with — they get the proxy that wrapped it
afterwards.
@Service
public class JwtService {
private SecretKey key;
// Runs after injection, so properties are available. Failing here fails the
// application at startup rather than on the first login - which is the point.
@PostConstruct
void init() {
byte[] keyBytes = properties.jwt().secret().getBytes(StandardCharsets.UTF_8);
if (keyBytes.length < 32) {
throw new IllegalStateException(
"pizza.jwt.secret must be at least 32 characters for HS256");
}
this.key = Keys.hmacShaKeyFor(keyBytes);
}
}What are the other ways to declare init and destroy methods?
Three, in order of preference: the @PostConstruct / @PreDestroy
annotations (portable, no Spring coupling); initMethod /
destroyMethod attributes on @Bean (the only option for a third-party class
you cannot annotate); and implementing InitializingBean /
DisposableBean (couples your class to Spring — avoid).
Note that destroy callbacks never run for a prototype bean. The container hands it over and forgets it. Anything holding a resource must not be a prototype, or must be closed by whoever asked for it.
Scopes
What scopes exist and what is the default?
| Scope | One instance per |
|---|---|
singleton (default) | container |
prototype | injection or getBean call |
request | HTTP request |
session | HTTP session |
application | ServletContext |
websocket | WebSocket session |
The last four only exist in a web-aware context.
Singleton means one per container, not one per JVM. Two contexts in the same process each get their own. And a singleton is shared across every request thread, so a singleton with mutable state is a data race — which is why the services in the pizza API hold only their injected collaborators and nothing else.
What happens if you inject a prototype into a singleton?
You get exactly one instance, injected once at startup, and it never changes. That is nearly
always a bug. If you need a fresh instance per call, inject an
ObjectProvider<T> and call getObject(), or use a scoped proxy.
Defining beans
@Component versus @Bean — when do you use which?
@Component | @Bean | |
|---|---|---|
| Goes on | your own class | a method in a @Configuration class |
| Found by | component scanning | being declared |
| Use it for | code you own | third-party classes, or anything needing construction logic |
@Configuration
@EnableCaching
public class CacheConfig {
public static final String MENU_CACHE = "menu";
// ConcurrentMapCacheManager is not our class, so it cannot be annotated.
// A @Bean method is the only way to contribute it - and it lets us configure it.
@Bean
public ConcurrentMapCacheManager cacheManager() {
var manager = new ConcurrentMapCacheManager(MENU_CACHE, MENU_BY_TYPE_CACHE);
manager.setAllowNullValues(false);
return manager;
}
}What is the default bean id, and how do you change it?
For @Bean, the method name. For @Component, the class name with a
lower-case first letter. Override with @Bean(name = "...") or
@Component("...").
What does component scanning do, and where does it look?
It walks the classpath under a set of base packages looking for classes annotated with
@Component or a meta-annotation of it — @Service,
@Repository, @Controller, @RestController,
@Configuration — and registers a bean definition for each.
@SpringBootApplication implies @ComponentScan with the annotated class's
own package as the base, which is why that class belongs at the root of your package tree.
The stereotype annotations are not merely decorative. @Repository in particular
enables translation of vendor-specific persistence exceptions into Spring's
DataAccessException hierarchy.
Why would you make a @Bean method static?
Because BeanFactoryPostProcessors must be created very early — before the
enclosing configuration class can be fully initialised. A static method can be called without an
instance, which avoids forcing the configuration class into premature instantiation.
Resolving ambiguity
What happens when two beans match one injection point?
NoUniqueBeanDefinitionException, at startup. Resolve it one of three ways:
@Primaryon the one that should win by default.@Qualifier("name")at the injection point to name the one you want.- Name the field or parameter after the bean — matching by name is the fallback after matching by type.
// Two ThreadPoolTaskExecutor-ish beans exist, so the injection point names one.
@Autowired
@Qualifier(value = "taskExecutor")
private ThreadPoolTaskExecutor taskExecutor;What if a bean might not exist at all?
ObjectProvider<T>. It resolves lazily and tolerates absence, which is how one
security configuration can cover both the profile where OAuth2 is configured and the profile where
it is not:
private final ObjectProvider<ClientRegistrationRepository> clientRegistrationRepository;
// ...
if (clientRegistrationRepository.getIfAvailable() != null) {
http.oauth2Login(oauth2 -> oauth2.successHandler(successHandler));
}Lazy or eager?
Are beans created lazily or eagerly?
Singletons are created eagerly, when the context starts. Prototypes are created
on demand. Add @Lazy to defer a singleton until first use.
Eager is the right default: a misconfigured bean fails the application at startup rather than at
3am on the first request that happens to need it. Reach for @Lazy to break a circular
dependency or to skip building something genuinely expensive and rarely used — and treat the first
of those as a design smell rather than a solution.
Configuration values
How do you inject a scalar value?
@Value("${pizza.oauth2.success-redirect:http://localhost:5173/oauth2/callback}")
private String successRedirect;The part after : is the default, used when the property is absent. Without a
default, a missing property fails the context — which is usually what you want.
What is the Environment abstraction?
The container's unified view of properties and active profiles, whatever their source — command
line, environment variables, application.properties, defaults. Ask it directly when
you need to:
Environment env = ctx.getEnvironment();
System.out.println("Active Profile: " + Arrays.toString(env.getActiveProfiles()));
System.out.println("Port: " + env.getProperty("server.port"));What is a property source, and what does @PropertySource do?
A property source is one named set of key/value pairs. The Environment holds an
ordered list of them and returns the first hit, which is exactly how a command-line argument
overrides a file. @PropertySource adds a file to that list.
PropertySourcesPlaceholderConfigurer is the BeanFactoryPostProcessor that
resolves ${...} placeholders against it — Boot registers one for you.
What is SpEL, and what is the difference between $ and
#?
Spring Expression Language — an expression language for querying and manipulating an object graph at runtime. The two prefixes are completely different things:
${...}is a property placeholder. Resolved from theEnvironment, before SpEL is involved at all.#{...}is a SpEL expression. Evaluated as code — it can reference beans, call methods, do arithmetic.
@Value("${server.port}") // the property named server.port
@Value("#{systemProperties['user.region']}") // a SpEL expression
@Cacheable(value = "menuByType", key = "#type") // SpEL over the method's arguments
@PreAuthorize("hasRole('ADMIN')") // SpEL, over the security contextThe last two are why SpEL is worth knowing even if you never write #{} yourself: the
caching and security annotations are SpEL all the way down.
Prefer @ConfigurationProperties to @Value
This is the modern answer, and it is not a style preference. The pizza API replaced nine scattered
@Value annotations with one record — and two of the nine were the same key
declared in two different classes, with nothing checking they still agreed.
@Validated
@ConfigurationProperties(prefix = "pizza")
public record PizzaProperties(
@Valid Pricing pricing,
@Valid Jwt jwt,
@Valid Cors cors) {
public record Pricing(
@DecimalMin("0.0") @DecimalMax("1.0") BigDecimal taxRate,
@DecimalMin("0.0") BigDecimal deliveryFee) {}
public record Jwt(@NotBlank @Size(min = 32) String secret, @Positive long expirationMinutes) {}
// Binding to List<String> removes the split(",") every @Value version does by hand.
public record Cors(@NotEmpty List<String> allowedOrigins) {}
}Three things you get that @Value cannot give you: types (a
BigDecimal is a BigDecimal, a list is a list), immutability
(a record component is set once by the constructor), and validation at
startup — @Validated checks the constraints while the context is building, so a
missing pizza.jwt.secret fails at boot with a message naming the property instead of
becoming a null field and a NullPointerException on the first login.
Profiles
What are profiles for and how do you configure them?
A profile is a named group of beans and properties that is only active when you say so. Use it when whole components differ per environment — not for a value that differs, which is what properties are for.
@Service
@Profile("search") // no Elasticsearch bean exists unless this profile is on,
public class ProductSearchServiceImpl implements ProductSearchService { ... }spring.profiles.active=local
# and application-local.properties is layered over application.propertiesActivate with spring.profiles.active, the SPRING_PROFILES_ACTIVE
environment variable, or --spring.profiles.active= on the command line. You can have as
many active at once as you like. @Profile("!prod") negates; @Profile works
on @Component and @Bean alike.
Proxies
What is a proxy, and which two types can Spring create?
A proxy is a stand-in object with the same type as the real bean. Callers hold the proxy; it adds
behaviour and then delegates. This is how @Transactional,
@Cacheable, @Async, @PreAuthorize, @Retryable and
every AOP aspect are implemented.
| JDK dynamic proxy | CGLIB | |
|---|---|---|
| Requires | the bean implements an interface | nothing — subclasses the class |
| Proxies | only interface methods | public and protected methods |
| Cannot handle | a class with no interface | final classes or final methods |
Spring Boot defaults to CGLIB (proxyTargetClass=true) so it works either way.
What visibility must a method have to be advised?
public — with CGLIB, protected also works. private,
static and final methods are never advised, and nothing warns you.
⚠️ The rule that catches everyone: self-invocation.
public void a() { b(); } // b() is NOT advised - this call never leaves the object
public void b() { ... } // advised only when ANOTHER bean calls itAdvice runs when a call arrives through the proxy. A call to a sibling method on
this goes straight down the vtable and the proxy never sees it. So a
@Transactional method called from another method of the same class runs with no
transaction at all — silently. If an annotation "isn't working", check for a self-invocation before
anything else.
Post-processors
BeanFactoryPostProcessor versus
BeanPostProcessor?
BeanFactoryPostProcessor | BeanPostProcessor | |
|---|---|---|
| Operates on | bean definitions | bean instances |
| Runs | once, before any bean is created | around every bean's initialisation |
| Example | PropertySourcesPlaceholderConfigurer | the one that wraps beans in AOP proxies |
What to remember
- Constructor injection,
finalfields, no@Autowiredneeded. - Singletons are eager and shared — so they must be stateless.
- Proxies are created in
postProcessAfterInitialization, which is why self-invocation bypasses every annotation built on them. ${}is a property;#{}is SpEL. Different mechanisms entirely.@ConfigurationPropertiesover@Value: typed, immutable, validated at startup.