Spring @Autowired Field Is Null: Why Injection Didn't Happen
Why is an @Autowired or @Value field null in Spring Boot? Objects built with new, static fields and constructor timing, and how to fix them.
The Java backend framework interviewers ask about most: dependency injection, auto-configuration, REST APIs, Spring Data JPA, transactions, security and testing.
Official reference: Spring Boot reference
Why is an @Autowired or @Value field null in Spring Boot? Objects built with new, static fields and constructor timing, and how to fix them.
Fix 'the dependencies of some of the beans in the application context form a cycle' in Spring Boot 3 by redesigning beans, not flags.
Fix 'blocked by CORS policy' for a Spring Boot 3 API: global CORS config, Spring Security preflight handling, credentials and exposed headers.
Fix 'Failed to configure a DataSource: url attribute is not specified' in Spring Boot 3 by setting the URL, adding the driver or a profile.
Fix 'Could not write JSON: Infinite recursion (StackOverflowError)' for bidirectional JPA entities in Spring Boot 3 with DTOs or annotations.
Fix 'required a bean of type that could not be found' and 'No qualifying bean' in Spring Boot 3: scanning, conditions, qualifiers, tests.
Fix 'Web server failed to start. Port 8080 was already in use' in Spring Boot: find the process on the port, stop it or change server.port.
Seeing the Whitelabel Error Page with status 404 in Spring Boot 3? Check the mapping, component scan, @RestController and context path.
Which Spring Boot Actuator endpoints to expose, how exposure differs from authorization, and how to lock down env, heapdump and health details.
How Spring Boot auto-configuration works: AutoConfiguration.imports, @ConditionalOnMissingBean, the conditions report, and safe overrides.
Spring bean scopes explained: why singletons must be stateless, injecting prototype or request beans into singletons, and scoped proxies.
Bind Spring Boot settings to typed @ConfigurationProperties records, validate them at startup, and know when @Value is still the right tool.
Why Spring recommends constructor injection over @Autowired fields: final dependencies, plain unit tests, fail-fast startup and visible cycles.
When Spring Security CSRF protection is needed: cookie sessions vs bearer tokens, SameSite, CookieCsrfTokenRepository and SPA setups in Boot 3.
Map Spring Boot exceptions to stable HTTP errors with @RestControllerAdvice and ProblemDetail, without leaking stack traces or internals.
Set connect and read timeouts on Spring RestClient and WebClient, size them from a latency budget, and stop slow calls exhausting threads.
Why Hibernate throws LazyInitializationException in Spring Boot, how open-in-view hides it, and how fetch joins, entity graphs and DTOs fix it.
Detect and fix JPA N+1 selects in Spring Boot with fetch joins, @EntityGraph, batch fetching and DTO queries, then prove it with a test.
Why unbounded Micrometer tags like user IDs or raw URLs blow up Prometheus, how Spring Boot templates URIs, and how to cap tag cardinality.
How JPA @Version prevents lost updates in Spring Boot, what ObjectOptimisticLockingFailureException means, and when to retry or return 409.
Use Spring Boot profiles for environment differences without hiding behaviour: profile files, @Profile beans, groups, and config precedence.
What @DataJpaTest loads, why its rollback and embedded database can hide bugs, and how to test Spring Data repositories with Testcontainers.
Validate Spring Boot request bodies and parameters with @Valid and Jakarta constraints, return clear 400 errors, and avoid silent gaps.
Why Spring @Transactional commits on checked exceptions, how rollbackFor and noRollbackFor work, and why swallowed exceptions commit.
How Spring Security 6 SecurityFilterChain beans work: lambda DSL, securityMatcher, @Order, rule ordering and why the first matching chain wins.
How Spring Boot liveness and readiness probes work on Kubernetes, what belongs in each health group, and how to warm up and shut down gracefully.
Why long @Transactional methods exhaust the Hikari pool and hold locks, and how to keep remote calls, files and email outside the transaction.
Why @Transactional is ignored on self-invocation and private methods in Spring Boot, how the proxy works, and four ways to fix it.
Spring Boot is an opinionated layer on top of the Spring Framework for building stand-alone, production-ready applications quickly. Spring itself gives you the IoC container, MVC, transactions and data access, but you wire and configure most of it yourself. Boot adds:
DataSource when a JDBC driver is presentspring-boot-starter-web, with versions managed by Boot's dependency BOMjava -jar app.jarapplication.properties or YAML, profiles, environment variablesBoot doesn't replace Spring; it's still Spring underneath, with the boilerplate removed and every default overridable. Spring Boot 3 requires Java 17 and moved from javax.* to the Jakarta EE jakarta.* packages.
java -jarLikely follow-up: What changed in Spring Boot 3 compared with 2.x?
Inversion of Control means your objects don't create or look up their own dependencies; the framework does. In Spring, the container (the ApplicationContext) reads configuration metadata such as annotations and @Bean methods, creates the objects, called beans, wires them together and manages their lifecycle.
Dependency injection is how Spring implements IoC: a class declares what it needs through constructor parameters, setters or fields, and the container supplies matching beans.
Why it matters:
BeanFactory is the basic container; ApplicationContext extends it with events, i18n, resource loading and eager creation of singletons.
ApplicationContext extends BeanFactoryLikely follow-up: What is the difference between BeanFactory and ApplicationContext?
I default to constructor injection, which is also what the Spring team recommends. Dependencies are passed in when the object is created, so:
final, making the bean immutable and safe to share between threadsnull dependencynew OrderService(mockRepo), no reflection, no SpringSince Spring 4.3, a class with a single constructor doesn't even need @Autowired.
Setter injection is for genuinely optional dependencies that have a sensible default.
Field injection (@Autowired on a private field) is concise but hides dependencies, rules out final, and forces tests to use reflection or a Spring context. I avoid it outside test classes. A side effect of constructor injection is that circular dependencies fail fast at startup, which is usually what you want.
final fields and immutabilityLikely follow-up: Is @Autowired required on a constructor? · What happens with a circular dependency and constructor injection?
@SpringBootApplication do, and how does component scanning decide what to pick up?easyIt's a convenience annotation that combines three:
@SpringBootConfiguration: a specialized @Configuration, so the class can declare @Bean methods@EnableAutoConfiguration: turns on Boot's auto-configuration@ComponentScan: scans for @Component and its stereotypesComponent scanning starts from the package of the annotated class and covers all its sub-packages. That's why the main class belongs in a root package like com.acme.shop. A controller in a sibling package such as com.acme.web is silently not found, and you get a 404 or a missing-bean error.
You can widen it with scanBasePackages, but restructuring packages is usually cleaner. To turn off one auto-configuration, use exclude = DataSourceAutoConfiguration.class or the spring.autoconfigure.exclude property.
SpringApplication.run(...) then creates the ApplicationContext, applies auto-configuration, starts the embedded server and calls any runners.
// Same as @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan
@SpringBootApplication
public class ShopApplication {
public static void main(String[] args) {
SpringApplication.run(ShopApplication.class, args);
}
}exclude or a propertyLikely follow-up: How would you exclude a single auto-configuration class?
@EnableAutoConfiguration loads the candidate auto-configuration classes listed in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports in every jar on the classpath. (Older Boot 2 versions used spring.factories for this.) Each one is an ordinary configuration class, annotated @AutoConfiguration, guarded by conditions:
@ConditionalOnClass: only if a class, for example a JDBC driver or DataSource, is on the classpath@ConditionalOnMissingBean: only if you haven't defined that bean yourself@ConditionalOnProperty: only if a property has a given valueAuto-configurations are processed after your own bean definitions, so @ConditionalOnMissingBean lets them back off: define your own DataSource and Boot's default disappears. That's the core idea: sensible defaults that yield to explicit configuration.
To see what was applied and why, start the app with --debug to print the condition evaluation report, or look at the Actuator conditions endpoint.
AutoConfiguration.imports@Conditional... annotations@ConditionalOnMissingBean--debug prints the condition evaluation reportLikely follow-up: How would you write your own starter? · How do you disable one auto-configuration?
@Component, @Service, @Repository and @Controller?easyAll four mark a class as a bean for component scanning; @Service, @Repository and @Controller are themselves meta-annotated with @Component. The differences are mostly semantic, with a little behavior:
@Component: a generic bean@Service: the business-logic layer. It adds no behavior today, but it documents intent and is a handy target for aspects@Repository: the data-access layer. It enables exception translation, so technology-specific persistence exceptions are converted into Spring's unchecked DataAccessException hierarchy@Controller: the web layer. Spring MVC detects it as a request handler; its methods return view names unless they're @ResponseBodyUsing the right stereotype makes the layering readable and lets aspects or test slices target a layer. Note that Spring Data repository interfaces don't need @Repository: Spring Data creates and registers their implementations for you.
@Component@Repository adds exception translation@Controller is detected as an MVC handler@Service documents intent, no extra behaviorLikely follow-up: What is DataAccessException and why is it unchecked?
@RestController and @Controller?easy@RestController is @Controller plus @ResponseBody applied to every handler method.
With plain @Controller, a method's return value is treated as a view name: return "orders" and a view resolver renders a template, say Thymeleaf's orders.html, with the model. That's for server-side rendered pages.
With @RestController, the return value is written straight to the response body. An HttpMessageConverter serializes it, typically to JSON with Jackson, based on content negotiation with the request's Accept header. That's what you use for REST APIs.
You can mix the two: a @Controller can annotate individual methods with @ResponseBody, or return a ResponseEntity, which is also written to the body. In practice I use @RestController for APIs and @Controller only when rendering HTML.
@RestController = @Controller + @ResponseBody@Controller returns view names by defaultAccept headerThe default is singleton: one instance per bean definition per container. It isn't the GoF singleton; you can define two beans of the same class, and each context gets its own instances.
@PreDestroy are not called.ServletContext) and websocket.Because a singleton is shared by all request threads, it should be stateless or hold only thread-safe state; a mutable field on a service is a classic concurrency bug.
A common trap is injecting a prototype or request-scoped bean into a singleton: injection happens once, so you keep reusing the same instance. Fix it with ObjectProvider<T>, @Lookup method injection, or a scoped proxy.
Likely follow-up: What happens when you inject a prototype bean into a singleton?
@Transactional work, and what are its default settings?mid@Transactional is declarative transaction management built on AOP proxies. Spring wraps the bean in a proxy; a call through it asks the PlatformTransactionManager (for JPA, JpaTransactionManager) to start or join a transaction, invokes your method, then commits, or rolls back if the method throws.
Defaults:
REQUIRED: join the current transaction or start a new oneDEFAULT: the database's own default levelRuntimeException and Error only: a checked exception commits unless you add rollbackFor = Exception.classI put it on service methods, which define the unit of work, rather than on controllers or repositories. Boot auto-configures transaction management, so @EnableTransactionManagement isn't needed. And because it's proxy-based, only calls that come in through the proxy are transactional.
@Transactional(rollbackFor = Exception.class)
public void transfer(long from, long to, BigDecimal amount)
throws InsufficientFundsException {
accounts.debit(from, amount); // both run in one transaction
accounts.credit(to, amount);
}rollbackFor is setLikely follow-up: Why might @Transactional silently not work? · What does readOnly = true actually do?
@Transactional sometimes have no effect? Explain the self-invocation problem in this code.hardSpring applies @Transactional through a proxy in front of the bean, and only calls that come through the proxy are intercepted. When placeAll calls this.place(...), it's an ordinary Java call on the target object, so no transaction is started even though place is annotated.
Other reasons it silently does nothing:
private or final, which a class-based proxy can't override (since Spring 6, protected and package-private methods work with class-based proxies)new@PostConstruct, before the proxy is fully usableFixes: move the transactional method to another bean (cleanest), inject the bean's own proxy lazily, use TransactionTemplate for programmatic control, or switch to AspectJ weaving. The same pitfall applies to @Async, @Cacheable and method security.
@Service
public class OrderService {
public void placeAll(List<Order> orders) {
orders.forEach(this::place); // plain call on "this"
}
@Transactional
public void place(Order order) { /* ... */ }
}this.method() bypasses the proxy entirelyTransactionTemplate@Async and @CacheableLikely follow-up: How would you get a transaction per order here?
A starter is a dependency descriptor: a mostly empty jar whose POM pulls in everything needed for one capability. spring-boot-starter-web brings Spring MVC, Jackson and embedded Tomcat; spring-boot-starter-data-jpa brings Spring Data JPA, Hibernate and the HikariCP pool; spring-boot-starter-test brings JUnit Jupiter, Mockito, AssertJ and Spring's test support. Bean Validation is its own starter, spring-boot-starter-validation.
Versions come from Boot's dependency management: the spring-boot-starter-parent POM or the spring-boot-dependencies BOM in Maven, or the Boot Gradle plugin. You declare starters without versions and get a set of library versions that were tested together; you can still override one through its version property.
Starters and auto-configuration are separate ideas: the starter puts libraries on the classpath, and auto-configuration reacts to what it finds. Official starters are named spring-boot-starter-*; a third-party one should be acme-spring-boot-starter. Boot 4 splits some starters more finely, for example spring-boot-starter-webmvc.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- no version: managed by Spring Boot -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>acme-spring-boot-starter@Configuration with @Bean instead of @Component, and what does proxyBeanMethods change?mid@Component is for your own classes: annotate the class and scanning finds it. @Bean methods are for when you build the object yourself: third-party classes you can't annotate, objects that need construction logic, or several differently configured beans of the same type. The method's return value becomes a bean, named after the method by default.
In a @Configuration class, Spring by default subclasses the class with CGLIB (full mode, proxyBeanMethods = true). If one @Bean method calls another, the call is intercepted and returns the existing singleton instead of creating a second instance.
With proxyBeanMethods = false, or with @Bean methods in a plain @Component (lite mode), there's no interception: calling another @Bean method is a normal Java call that creates a new object. Lite mode avoids the CGLIB subclass, which is why Boot's own auto-configurations use it. The safe habit in any mode is to declare dependencies as method parameters rather than calling other bean methods.
@Configuration
public class AppConfig {
@Bean
public Clock clock() {
return Clock.systemUTC();
}
@Bean
public InvoiceService invoiceService(Clock clock) { // injected, not called
return new InvoiceService(clock);
}
}@Component for your own classes, found by scanning@Bean for third-party or custom-built objectsproxyBeanMethods = false makes them plain Java calls@Bean methods@PathVariable and @RequestParam, and when do you use each?easy@PathVariable binds a segment of the URI template: /orders/{id} called as /orders/42 gives id = 42. @RequestParam binds a query parameter or form field: /orders?status=PAID&page=2.
The REST convention: path variables identify a resource (/users/7/orders/42); request params filter, sort or paginate a collection and are often optional.
Details worth knowing:
@RequestParam is required by default; a missing one gives a 400. Use required = false, Optional<T>, or defaultValue, which makes it optional implicitly./orders/abc for a Long, also gives a 400.@RequestBody is the third option: it deserializes the JSON body into an object for POST or PUT payloads.-parameters; Boot's Maven parent and Gradle plugin enable that, otherwise write @PathVariable("id").@GetMapping("/users/{userId}/orders")
public List<OrderDto> list(@PathVariable Long userId,
@RequestParam(defaultValue = "ALL") String status,
@RequestParam(required = false) Integer limit) {
return orderService.find(userId, status, limit);
}@RequestParam is required by defaultdefaultValue makes a param optional@RequestBody binds the JSON payloadI use a @RestControllerAdvice class (@ControllerAdvice plus @ResponseBody) with @ExceptionHandler methods. Spring MVC routes an exception thrown by any controller to the most specific matching handler, and I turn it into a consistent error response with the right status: 404 for a not-found domain exception, 400 for validation failures, 409 for conflicts.
For the body, Spring 6 supports ProblemDetail, the RFC 9457 "problem details" format, served as application/problem+json with type, title, status, detail and instance, plus any custom properties.
Two useful pieces:
ResponseEntityExceptionHandler to get ProblemDetail responses for Spring MVC's own exceptions, or in Boot simply set spring.mvc.problemdetails.enabled=true@ExceptionHandler inside a controller applies only to that controller and takes precedence over the global adviceI also keep a catch-all handler that logs the error and returns a generic 500 without leaking stack traces.
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ProblemDetail notFound(OrderNotFoundException ex) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND, ex.getMessage());
pd.setTitle("Order not found");
pd.setProperty("orderId", ex.getOrderId());
return pd;
}
}@RestControllerAdvice with @ExceptionHandler methodsProblemDetail implements RFC 9457ResponseEntityExceptionHandler covers MVC exceptionsLikely follow-up: How would you return all field errors from a failed @Valid?
You run one query to load N parent rows, then JPA fires one extra query per parent as soon as you touch a lazy association. Here, 100 orders means 101 round trips. The code looks innocent, which is why it's so common; the SQL log (spring.jpa.show-sql or Hibernate's SQL logger) gives it away.
Fixes, in the order I reach for them:
select o from Order o join fetch o.items loads orders and items in one query@EntityGraph(attributePaths = "items") on a repository method, the same effect without writing JPQL@BatchSize or hibernate.default_batch_fetch_size loads the collections for many parents with one IN query, so N+1 becomes 1 + N/batch sizeSwitching the association to EAGER is not a fix: it still issues extra queries and loads data you often don't need. Also be careful combining a collection fetch join with pagination, because Hibernate then paginates in memory.
List<Order> orders = orderRepository.findAll(); // 1 query
for (Order o : orders) {
System.out.println(o.getItems().size()); // +1 query per order
}join fetch or @EntityGraphLikely follow-up: Why is a collection fetch join a problem with pagination?
For a singleton, the container roughly does this:
BeanNameAware and ApplicationContextAwareBeanPostProcessor.postProcessBeforeInitialization from every post-processor@PostConstruct, then InitializingBean.afterPropertiesSet(), then a custom init methodpostProcessAfterInitialization, which is typically where AOP proxies are created, so other beans may receive a proxy rather than the raw object@PreDestroy, then DisposableBean.destroy(), then a custom destroy methodPrototype beans are initialized but never get destruction callbacks.
In practice I use @PostConstruct and @PreDestroy (from jakarta.annotation in Boot 3), which the Spring docs recommend over the Spring-specific interfaces. I don't rely on transactional behavior inside @PostConstruct; an ApplicationRunner or an ApplicationReadyEvent listener is safer for startup work.
@PostConstruct, then afterPropertiesSet, then init method@PreDestroy on shutdown, never for prototypesA profile is a named set of configuration and beans that's active only in some environments, like dev, test or prod.
application-prod.yml is loaded on top of application.yml when prod is active, overriding matching keys. In one multi-document YAML file, each document can use spring.config.activate.on-profile.@Profile("dev") on a component or @Bean method registers it only for that profile; @Profile("!prod") negates.SPRING_PROFILES_ACTIVE=prod as an environment variable, --spring.profiles.active=prod on the command line, or the property itself. With nothing active, the default profile applies.spring.profiles.group.prod=proddb,prodmq activates several profiles at once.You can't set spring.profiles.active inside a profile-specific document. My rule of thumb: use profiles for structural differences, like a stub client versus a real one, and inject real secrets and URLs from the environment rather than committed profile files.
spring:
datasource:
url: jdbc:postgresql://localhost:5432/shop
---
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:postgresql://db.internal:5432/shopapplication-{profile}.yml overrides the base file@Profile registers beans conditionallySPRING_PROFILES_ACTIVE or a command-line argdefault profile@Value and @ConfigurationProperties, and which do you prefer?mid@Value("${app.timeout}") injects a single property into a field or parameter. It supports defaults like ${app.timeout:5s} and SpEL expressions, but keys become strings scattered through the code, there's no real validation, and relaxed binding is limited.
@ConfigurationProperties(prefix = "app.payments") binds a whole tree of properties to a type-safe object, ideally an immutable record:
app.payments.retry-count, retryCount and the env var APP_PAYMENTS_RETRYCOUNT all bind to the same fieldDuration, DataSize, lists, maps and nested objects@Validated and Bean Validation constraints, failing fast at startupspring-boot-configuration-processorRegister it with @EnableConfigurationProperties or @ConfigurationPropertiesScan; a record with a single constructor uses constructor binding automatically.
I use @ConfigurationProperties for any group of related settings and keep @Value for a one-off value or when I genuinely need SpEL.
@ConfigurationProperties(prefix = "app.payments")
@Validated
public record PaymentProperties(
@NotBlank String baseUrl,
Duration timeout,
@Min(0) int retryCount) {}@Value injects one property and supports SpEL@ConfigurationProperties binds a typed tree@Validated fails fast at startupREQUIRED, REQUIRES_NEW and NESTED differ?hardPropagation decides what a transactional method does when a transaction may already be running.
REQUIRED (default): join the existing transaction, or start one. All participants share one physical transaction. If an inner method throws and marks it rollback-only, everything rolls back; if the outer method catches that exception and tries to commit anyway, Spring throws UnexpectedRollbackException.REQUIRES_NEW: suspend the outer transaction and run in an independent one that commits or rolls back on its own. Good for an audit record that must survive a business rollback. It holds a second connection while the outer one waits, so it can exhaust the pool under load.NESTED: one physical transaction with a savepoint; the inner part can roll back to the savepoint while the outer continues. It's meant for JDBC's DataSourceTransactionManager; JPA has no nested transactions, and JpaTransactionManager disallows them by default.The rest: SUPPORTS joins if present, NOT_SUPPORTED suspends, MANDATORY requires an existing one and NEVER forbids one. And propagation only applies to calls through the proxy, so the inner method must live on another bean.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordAudit(AuditEvent event) {
auditRepository.save(event); // commits even if the caller rolls back
}UnexpectedRollbackExceptionSpring Security is built on servlet filters, so it runs before a request ever reaches the DispatcherServlet.
DelegatingFilterProxy, which bridges into the Spring context and delegates to the FilterChainProxy bean.FilterChainProxy holds one or more SecurityFilterChains, each with a request matcher. Only the first matching chain handles the request, so specific chains like /api/** must come before general ones.ExceptionTranslationFilter and AuthorizationFilter near the end.Authentication and store it in the SecurityContextHolder, which is thread-local by default.ExceptionTranslationFilter sends unauthenticated users to the AuthenticationEntryPoint (a 401 or login redirect) and authenticated ones to the AccessDeniedHandler (a 403).In Spring Security 6 you configure all this by declaring SecurityFilterChain beans; WebSecurityConfigurerAdapter has been removed.
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}DelegatingFilterProxy delegates to FilterChainProxySecurityFilterChain winsSecurityContextHolderYou declare an interface such as OrderRepository extends JpaRepository<Order, Long> and Spring Data generates the implementation at runtime: a proxy backed by SimpleJpaRepository. You get save, findById, findAll, deleteById, count, paging and sorting for free.
Derived queries are parsed from the method name: findByCustomerEmailAndStatus(String email, OrderStatus status) becomes a JPQL query that joins through customer. Keywords like GreaterThan, Between, Like, In, OrderBy, First or Top3, plus existsBy, countBy and deleteBy, are supported. The name is checked when the repository is created, usually at startup, so a typo in a property name fails fast.
The hierarchy: CrudRepository (returns Iterable), ListCrudRepository (returns List), PagingAndSortingRepository (adds Sort and Pageable), and JpaRepository, which combines them with JPA extras like flush, saveAllAndFlush and getReferenceById. Since Spring Data 3, PagingAndSortingRepository no longer extends CrudRepository.
When a method name gets long and unreadable, I switch to @Query.
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByCustomerEmailAndStatus(String email, OrderStatus status);
Optional<Order> findFirstByCustomerIdOrderByCreatedAtDesc(Long customerId);
boolean existsByReference(String reference);
long countByStatus(OrderStatus status);
}JpaRepository adds JPA extras to CRUD and paging@Query when names get unwieldyjavax.persistence, javax.servlet and javax.validation become jakarta.*. This is the biggest migration effort, because third-party libraries also need Jakarta-compatible versionsWebSecurityConfigurerAdapter is gone; you declare SecurityFilterChain beans and use authorizeHttpRequests with requestMatchersProblemDetail error responses and declarative HTTP interface clients (@HttpExchange)AutoConfiguration.imports instead of spring.factories/orders/ no longer matches /ordersLater 3.x releases added @ServiceConnection for Testcontainers (3.1), virtual threads and RestClient (3.2), and structured logging plus graceful shutdown by default (3.4). For migrating, spring-boot-properties-migrator reports renamed properties at startup, and OpenRewrite recipes automate much of the javax to jakarta change.
javax.* moved to jakarta.*SecurityFilterChain beans onlyLikely follow-up: How would you plan a migration from Boot 2.7 to 3.x?
With Bean Validation (Jakarta Validation, implemented by Hibernate Validator), added through spring-boot-starter-validation.
@NotBlank, @Email, @Size, @Positive, @Past, @Pattern, and @Valid on nested objects you want validated too.@Valid (or Spring's @Validated, which also supports validation groups). On failure, Spring throws MethodArgumentNotValidException, which becomes a 400.@RequestParam or @PathVariable parameters, like @Min(1) int page, trigger Spring MVC's built-in method validation (Spring 6.1+), which raises HandlerMethodValidationException.@RestControllerAdvice to return field-level errors, ideally as a ProblemDetail.For rules that span several fields, I write a custom constraint with @Constraint(validatedBy = ...). Rules that need the database, like "email already registered", belong in the service layer: DTO validation checks shape, the domain checks business rules.
public record CreateUserRequest(
@NotBlank @Size(max = 50) String name,
@NotBlank @Email String email,
@Min(18) int age) {}
@PostMapping("/users")
public UserDto create(@Valid @RequestBody CreateUserRequest req) {
return userService.create(req);
}spring-boot-starter-validation brings Hibernate Validator@Valid on the parameterMethodArgumentNotValidException: a 400@ConstraintLazyInitializationException?midEager loads an association together with the entity; lazy loads it on first access, through a proxy or a persistent collection wrapper. The JPA defaults: @ManyToOne and @OneToOne are EAGER, @OneToMany and @ManyToMany are LAZY.
My rule is to make everything lazy, including @ManyToOne(fetch = FetchType.LAZY), and fetch what each use case needs explicitly with a fetch join, an entity graph or a DTO projection. An eager mapping can't be switched off per query, so it loads data you don't need and causes extra queries when you load lists.
LazyInitializationException happens when you touch a lazy association after the persistence context has closed, typically outside the transaction; a classic case is Jackson serializing an entity in the controller. The fixes are to load what you need inside the transactional service method, map to a DTO there, or use a fetch join. Making things EAGER or relying on open-session-in-view just hides the problem.
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY) // default would be EAGER
private Customer customer;
@OneToMany(mappedBy = "order") // LAZY by default
private List<OrderItem> items = new ArrayList<>();
}Actuator adds production-ready endpoints under /actuator: health, info, metrics, env, loggers, threaddump, prometheus (with the Micrometer Prometheus registry) and more. By default only health is exposed over HTTP; you opt in with management.endpoints.web.exposure.include.
/actuator/health aggregates the HealthIndicators Boot auto-configures for what you use, such as the database, disk space or Redis, into a status like UP or DOWN. Details are hidden by default (show-details is never); when-authorized is a sensible production setting.
For Kubernetes there are liveness and readiness groups at /actuator/health/liveness and /actuator/health/readiness. Boot 3 enables them automatically when it detects Kubernetes, or anywhere with management.endpoint.health.probes.enabled=true. Keep external systems like the database out of liveness, or a database outage restarts every pod.
You can add a custom HealthIndicator bean. Endpoints like env and heapdump are sensitive, so I expose them sparingly, secure them, or move management to its own port with management.server.port.
/actuatorhealth is exposed over HTTP by defaultHealthIndicatorsDispatcherServlet, Spring MVC's front controller.HandlerMapping (RequestMappingHandlerMapping for annotated controllers) finds the controller method matching the path, HTTP method, headers and media types, along with any HandlerInterceptors, whose preHandle runs.HandlerAdapter invokes the method. Argument resolvers fill in @PathVariable, @RequestParam and @RequestBody, the last through an HttpMessageConverter such as Jackson, and validation runs.@ResponseBody or ResponseEntity, a message converter writes the body; for a view name, a ViewResolver renders a template.postHandle and afterCompletion run.HandlerExceptionResolvers take over, which is how @ExceptionHandler and @ControllerAdvice get invoked.The whole request is handled on one container thread, unless you use async request processing.
@ResponseBody results@ExceptionHandlerLikely follow-up: What is the difference between a servlet filter and a HandlerInterceptor?
AOP applies cross-cutting concerns, like transactions, security, caching, logging or metrics, without putting that code in every method. An aspect combines advice, the code to run (@Before, @AfterReturning, @AfterThrowing, @After or @Around), with a pointcut saying where, such as execution(* com.acme.service..*(..)) or @annotation(com.acme.Timed).
Spring AOP is proxy-based and works at runtime. When a bean matches a pointcut, the container hands out a proxy instead of the raw object:
spring.aop.proxy-target-class=true)The consequences: only method executions on Spring beans can be advised; calls must come through the proxy, so self-invocation isn't intercepted; and CGLIB can't override final or private methods or subclass final classes. For anything beyond that, such as advising constructors or non-Spring objects, you need full AspectJ weaving.
@Aspect
@Component
public class TimingAspect {
@Around("@annotation(com.acme.Timed)")
public Object time(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed();
} finally {
log.info("{} took {} ns", pjp.getSignature(), System.nanoTime() - start);
}
}
}@SpringBootTest, @WebMvcTest and @DataJpaTest?mid@SpringBootTest loads the full application context: all your beans plus auto-configuration. By default it uses a mock web environment with no real server; webEnvironment = RANDOM_PORT starts the embedded server for true end-to-end HTTP tests. The most realistic and the slowest.@WebMvcTest(OrderController.class) is a slice: only the MVC layer, meaning controllers, @ControllerAdvice, filters, converters and JSON support, with MockMvc auto-configured. Services and repositories aren't loaded, so you replace them with @MockitoBean. Ideal for routing, validation, status codes and JSON shape.@DataJpaTest loads only JPA: entities, repositories and a TestEntityManager. Each test is transactional and rolled back, and it swaps in an embedded database by default; @AutoConfigureTestDatabase(replace = Replace.NONE) keeps your real one, for example a Testcontainers database.Slices keep tests fast and focused. I write many unit and slice tests and fewer @SpringBootTest integration tests. Spring caches contexts between test classes with the same configuration, so needlessly varying mocks and properties slows the suite down.
@SpringBootTest loads the full context@WebMvcTest loads only the MVC slice, with MockMvc@DataJpaTest loads JPA only and rolls back each test@MockitoBeanIn a stateless API every request carries a bearer token in the Authorization header and the server keeps no session. Instead of hand-writing a JWT filter, I use Spring Security's OAuth2 Resource Server support:
spring-boot-starter-oauth2-resource-server and set spring.security.oauth2.resourceserver.jwt.issuer-uri, so Spring discovers the issuer's public keys (the JWK set)oauth2ResourceServer(o -> o.jwt(...)): each request's token is checked for a valid signature, exp and nbf, and the issuer, and scope claims become SCOPE_ authoritiesSessionCreationPolicy.STATELESS and disable CSRF, since no cookies carry credentialsrequestMatchers or @PreAuthorize("hasAuthority('SCOPE_orders:write')")Design points: keep access tokens short-lived with refresh tokens, because a JWT can't easily be revoked before it expires. Don't put secrets in the payload; it's encoded, not encrypted. Prefer asymmetric signatures so services only hold the public key. And let a dedicated authorization server, such as Keycloak or Spring Authorization Server, issue tokens.
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(csrf -> csrf.disable())
.build();
}If a dependency matches more than one bean and Spring can't narrow it down, startup fails with NoUniqueBeanDefinitionException. Ways to resolve it:
@Primary on one bean makes it the default whenever there's ambiguity@Qualifier("stripe") at the injection point picks a specific bean by qualifier or bean name, and it takes precedence over @PrimaryList<PaymentGateway>, or Map<String, PaymentGateway> keyed by bean name, for strategy-style code that chooses at runtimeI use @Primary for "the normal implementation plus a special case", @Qualifier (often via a custom qualifier annotation) when callers must choose explicitly, and a Map of strategies when the choice depends on request data.
@Component("stripe")
class StripeGateway implements PaymentGateway { }
@Primary
@Component("paypal")
class PaypalGateway implements PaymentGateway { }
@Service
class CheckoutService {
CheckoutService(@Qualifier("stripe") PaymentGateway gateway) { }
}NoUniqueBeanDefinitionException@Primary marks the default candidate@Qualifier picks explicitly and beats @PrimaryList or Map of all implementationsResponseEntity, and when would you return it instead of the body object?easyResponseEntity<T> represents the whole HTTP response: status code, headers and body. Returning a plain object from a @RestController always gives a 200 with that body (unless @ResponseStatus says otherwise); ResponseEntity lets you decide per request.
Typical uses:
Location header after a POST: ResponseEntity.created(uri).body(dto)ResponseEntity.noContent().build()ResponseEntity.notFound().build(), or ResponseEntity.of(optional)ETag, Cache-Control or Content-Disposition for downloadsFor errors I usually prefer throwing a domain exception and mapping it centrally in a @ControllerAdvice rather than building error responses in every controller. So my controllers return ResponseEntity for successful responses that aren't a plain 200, and bare DTOs otherwise.
created(uri) for a 201 with LocationnoContent() and notFound() buildersapplication.properties and application.yml?easyBoth are Boot's default configuration files, loaded from the classpath root (and config/ locations) into the same Environment; the difference is syntax.
.properties: flat key=value lines that repeat the prefix: spring.datasource.url=..., spring.datasource.username=.... Lists use indexes like app.hosts[0]=.....yml: hierarchical, which is more readable for nested settings and lists, and one file can hold several documents separated by ---, for example one per profile.Trade-offs: YAML is whitespace-sensitive, so a bad indent can silently move a key under the wrong parent, and YAML files can't be loaded with @PropertySource. Properties files are harder to get wrong. If both formats exist in the same location, .properties takes precedence.
Either way, environment variables and command-line arguments override the same keys, which is how you change settings per deployment without rebuilding.
.properties wins if both exist in one location@Query in Spring Data JPA, and what does @Modifying do?mid@Query lets you write the query yourself when a derived method name would be unreadable, or when you need joins, aggregates, a fetch join or a DTO projection. It takes JPQL by default, written against entity and field names rather than tables, or native SQL with nativeQuery = true for database-specific features. JPQL here is checked at startup; native SQL isn't.
Bind values with named parameters (:status plus @Param) or positional ones (?1); never concatenate user input into the string.
For bulk updates and deletes add @Modifying. Two things to know:
@Transactional on the method, because declared queries get no transaction configuration of their own@Modifying(clearAutomatically = true) clears the context afterwards, and flushAutomatically = true flushes pending changes first.public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("select o from Order o join fetch o.items where o.customer.id = :customerId")
List<Order> findWithItems(@Param("customerId") Long customerId);
@Modifying(clearAutomatically = true)
@Query("update Order o set o.status = :status where o.createdAt < :before")
int expireOlderThan(@Param("status") OrderStatus status, @Param("before") Instant before);
}nativeQuery = true for SQL@Modifying for bulk update and deletePage and Slice?midAdd a Pageable parameter to a repository method, like Page<Order> findByStatus(OrderStatus status, Pageable pageable), and Spring Data applies the limit, offset and sort. Build one with PageRequest.of(0, 20, Sort.by("createdAt").descending()). In a controller, Spring Data's web support resolves a Pageable parameter straight from ?page=0&size=20&sort=createdAt,desc; page numbers are zero-based.
Page<T> runs an extra count query, so it knows the total number of elements and pages. Useful for "page 3 of 12" UIs, but the count can be expensive on large tables.Slice<T> skips the count; it fetches one extra row just to know whether there's a next slice. Good for "load more" and infinite scroll.List<T> return type with a Pageable just applies the limit, with no metadata.Offset pagination slows down on deep pages, because the database still walks the skipped rows, and it can skip or repeat rows when data changes. For large or fast-moving tables I prefer keyset pagination, where id > :lastId order by id, which recent Spring Data versions support through the Window and ScrollPosition scrolling API.
Pageable parameter adds limit, offset and sortPage runs an extra count querySlice only knows if a next slice existsReturning entities couples your API contract to your database schema, and causes practical problems:
LazyInitializationException or fires surprise queriesrole or idSo I use DTOs, usually Java records, with separate request and response types per use case and validation annotations on the request types. Mapping happens in the service layer, inside the transaction, by hand for small cases or with a mapper like MapStruct for larger ones. For read-heavy endpoints, Spring Data projections (interface projections, or records populated by a JPQL constructor expression) fetch only the columns you need.
The cost is some mapping code; the payoff is an API that can evolve independently of the schema.
It's when bean A needs B and B needs A, directly or through a longer chain. With constructor injection neither can be created first, so startup fails with BeanCurrentlyInCreationException, and Boot's failure analyzer prints the cycle.
Spring can technically resolve a cycle between singletons that use setter or field injection, by handing one bean an early reference to the other before it's fully initialized. But since Spring Boot 2.6, circular references are prohibited by default, so those fail too unless you set spring.main.allow-circular-references=true, which should be a last resort.
A cycle usually signals a design problem, so the real fixes are:
ApplicationEventPublisher plus @EventListener)As a stopgap, @Lazy on one injection point injects a lazy proxy, which breaks the cycle at startup without changing the design.
BeanCurrentlyInCreationException@Lazy is a stopgap, not a fixFilter and a Spring HandlerInterceptor?midA servlet Filter belongs to the Servlet API and wraps the whole request before it reaches the DispatcherServlet. It sees every request, including static resources and error dispatches, can wrap or replace the request and response, and knows nothing about which controller will run. Spring Security, CORS, request logging and compression live here. In Boot, a Filter bean is registered automatically; use FilterRegistrationBean to control URL patterns and order, and extend OncePerRequestFilter so it runs once per request.
A HandlerInterceptor is Spring MVC's hook and runs inside the DispatcherServlet, after the handler has been chosen:
preHandle: before the controller; returning false stops processingpostHandle: after the controller, before view rendering; of limited use with @ResponseBody, since the response is already written by thenafterCompletion: after everything, even on exceptions; good for cleanup and timingBecause it knows the HandlerMethod, an interceptor can read the controller's annotations. Register it through WebMvcConfigurer.addInterceptors. Rule of thumb: generic HTTP concerns go in filters, MVC-aware ones in interceptors.
postHandle is of limited use with @ResponseBodyWebMvcConfigurer.addInterceptorsWith the web starter, Boot puts Tomcat on the classpath and starts it programmatically inside your application, instead of you deploying a WAR into an external server. The Boot build plugin packages an executable "fat" jar containing your code, its dependencies and the server, so you run it with java -jar app.jar, which suits containers well.
server.port (0 picks a random free port), server.servlet.context-path, server.ssl.*, compression, and Tomcat thread and connection limits under server.tomcat.*.spring-boot-starter-tomcat and add spring-boot-starter-jetty. WebFlux apps use Reactor Netty by default. Undertow was an option in Boot 3 but was dropped in Boot 4.WebServerFactoryCustomizer bean.SpringBootServletInitializer.On Java 21 with Boot 3.2 or later, spring.threads.virtual.enabled=true makes Tomcat handle requests on virtual threads.
java -jarserver.* propertiesNever in plain text, and never with a fast hash like MD5 or plain SHA-256. Use an adaptive, salted one-way function through a PasswordEncoder: bcrypt, scrypt, Argon2 or PBKDF2. They're deliberately slow and tunable, and each hash includes a random salt, so identical passwords produce different hashes.
The usual setup is the DelegatingPasswordEncoder from PasswordEncoderFactories.createDelegatingPasswordEncoder(). It stores hashes with an id prefix, like {bcrypt}$2a$10$..., encodes new passwords with bcrypt, and can still verify the other supported formats. That makes migrating algorithms possible: verify with the old one, then re-encode with the new one on the next successful login.
Rules: call encode when a user registers, matches(raw, stored) when checking, and never compare hashes yourself. Tune the work factor so a verification takes a noticeable amount of time on your hardware (Spring's docs suggest around one second), balanced against login throughput. NoOpPasswordEncoder and {noop} are for demos only.
DelegatingPasswordEncoder with {id} prefixesmatches, never compare hashes manuallyRestTemplate: the classic synchronous, template-style client. Still common, but in maintenance mode; RestClient is its modern replacement.WebClient: the reactive, non-blocking client from WebFlux, returning Mono and Flux. The right choice in a WebFlux app or for streaming. It works from MVC too, but calling .block() everywhere defeats the point.RestClient (Spring Framework 6.1, Boot 3.2+): a synchronous client with a fluent API similar to WebClient, built on the same infrastructure as RestTemplate. My default in a blocking MVC service.@GetExchange or @PostExchange methods, and Spring generates the implementation on top of RestClient or WebClient, a built-in alternative to OpenFeign.Whichever I use, I build it from the Boot-provided builder, like the auto-configured RestClient.Builder, so message converters and observability are applied. And I always set connect and read timeouts, because an unbounded call to a slow dependency is how thread pools get exhausted.
RestTemplate: classic, in maintenance modeWebClient: reactive, for WebFlux and streamingRestClient: modern fluent synchronous clientCross-Site Request Forgery tricks a logged-in user's browser into sending a state-changing request to your site from a malicious page. The browser attaches the session cookie automatically, so without protection the server can't tell the forged request from a real one.
Spring Security enables CSRF protection by default for unsafe methods (POST, PUT, PATCH, DELETE). It requires a secret CSRF token, which a cross-site page can't read, in a form field or header, and compares it with the copy stored server-side, in the HTTP session by default. Since Spring Security 6 the token is randomized on each request to mitigate BREACH attacks. For single-page apps, CookieCsrfTokenRepository puts the token in a cookie the frontend reads and echoes back as a header.
It's reasonable to disable it for a stateless API authenticated only by a bearer token in the Authorization header, because browsers don't attach that automatically. If the API authenticates with cookies, including a JWT stored in a cookie, keep CSRF on. SameSite cookies help, but as defense in depth, not a replacement.
@Cacheable work in Spring Boot, and what pitfalls should you watch for?midEnable it with @EnableCaching on a configuration class. Boot then auto-configures a CacheManager for the provider it finds, such as Caffeine, Redis, Hazelcast or JCache, falling back to a simple ConcurrentHashMap that isn't meant for production.
@Cacheable: on a hit, returns the cached value without calling the method; on a miss, calls it and stores the result@CachePut: always runs the method and updates the entry@CacheEvict: removes an entry, or everything with allEntries = trueThe default key comes from the parameters: none gives SimpleKey.EMPTY, one gives that value, several give a SimpleKey of all of them. Customize with SpEL (key = "#id"), and use condition or unless to control what's cached.
Pitfalls:
equals and hashCodesync = true lets only one thread compute the value@Cacheable(cacheNames = "products", key = "#id", unless = "#result == null")
public Product findById(long id) {
return productRepository.findById(id).orElse(null);
}
@CacheEvict(cacheNames = "products", key = "#product.id")
public Product update(Product product) {
return productRepository.save(product);
}@EnableCaching plus an auto-configured CacheManager@Async work in Spring, and what are its common pitfalls?mid@Async makes a method run on another thread: the caller returns immediately and the work is submitted to a TaskExecutor. Enable it with @EnableAsync. Boot auto-configures an executor named applicationTaskExecutor, a ThreadPoolTaskExecutor by default or a virtual-thread executor when spring.threads.virtual.enabled=true; tune the pool with spring.task.execution.pool.*.
Return void for fire-and-forget, or CompletableFuture<T> so callers can compose or wait on the result.
Pitfalls:
@Async method simply runs synchronouslyvoid methods never reach the caller; they go to an AsyncUncaughtExceptionHandler, which by default just logs themSecurityContext or logging MDC isn't propagated unless you configure it, for example with a TaskDecoratorFor background work that must survive a restart, I'd use a message queue instead.
@EnableAsyncvoid or CompletableFuture@Scheduled?easyAdd @EnableScheduling to a configuration class, then annotate a no-argument bean method:
fixedRate: run every N milliseconds, measured from the start of the previous runfixedDelay: wait N milliseconds after the previous run finishesinitialDelay: wait before the first runcron: Spring's cron format has six fields, starting with seconds, so 0 0 2 * * * means 2 AM every day; add zone to pin the time zoneValues can come from properties, for example cron = "${jobs.cleanup.cron}".
Gotchas: Boot's default scheduler has a single thread, so one slow job delays the others; raise spring.task.scheduling.pool.size if needed. An exception in a repeating task is logged and the schedule carries on. Most importantly, in a multi-instance deployment every instance runs the job. For exactly-one-node semantics, use a distributed lock (ShedLock is a popular library), a Kubernetes CronJob, or a clustered scheduler like Quartz.
@Component
public class CleanupJob {
@Scheduled(cron = "0 0 2 * * *", zone = "UTC")
public void purgeExpiredSessions() { /* daily at 02:00 UTC */ }
@Scheduled(fixedDelayString = "PT30S", initialDelay = 10_000)
public void pollOutbox() { /* 30 s after the previous run ends */ }
}@EnableScheduling plus @Scheduled methodsSpring MVC is built on the Servlet API with a thread-per-request model: each request holds a pool thread for its whole duration, including while it blocks on the database or a remote call. It's simple to write, debug and profile, and it works with the whole blocking ecosystem: JDBC, JPA and most client libraries.
Spring WebFlux is non-blocking and built on Project Reactor. A small, fixed number of event-loop threads (on Reactor Netty by default) serve many concurrent requests, because no thread waits on I/O. You return Mono<T> and Flux<T>, and you get back-pressure and streaming. The catch: every layer must be non-blocking, such as R2DBC instead of JDBC and WebClient instead of blocking clients. One blocking call on an event-loop thread stalls every request that thread serves.
I'd pick WebFlux for high-concurrency, I/O-bound services like gateways, streaming, or fan-out to many slow backends. For a typical CRUD service on a relational database, MVC is the better fit, and on Java 21 virtual threads give MVC much of the scalability with a blocking style.
Mono and FluxLikely follow-up: What happens if both the MVC and WebFlux starters are on the classpath?
With @WebMvcTest(OrderController.class), Spring loads only the web layer and auto-configures MockMvc, which drives the real Spring MVC pipeline (routing, argument binding, validation, message conversion, exception handlers) without starting a server. Anything the controller depends on must be supplied, usually as a mock declared with @MockitoBean, which replaces or adds that bean in the test context.
@MockitoBean and @MockitoSpyBean come from Spring Framework 6.2. Boot's older @MockBean and @SpyBean were deprecated in Boot 3.4 and removed in Boot 4.
Then you stub with Mockito, perform a request, and assert on the status, headers and JSON with jsonPath. With a full @SpringBootTest you add @AutoConfigureMockMvc to get a MockMvc; for a real HTTP round trip, use RANDOM_PORT with an HTTP client instead. Recent versions also offer MockMvcTester for AssertJ-style assertions.
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired MockMvc mvc;
@MockitoBean OrderService orderService;
@Test
void returnsOrder() throws Exception {
when(orderService.find(42L)).thenReturn(new OrderDto(42L, "PAID"));
mvc.perform(get("/api/orders/42"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("PAID"));
}
}@WebMvcTest auto-configures MockMvc@MockitoBean replaces collaborators with mocks@MockBean deprecated in 3.4, removed in Boot 4jsonPathTestcontainers starts real services, like PostgreSQL, Kafka or Redis, in throwaway Docker containers from your JUnit tests, so you test against the same engine as production instead of H2, which differs in SQL dialect, types and locking behavior.
The setup:
@Testcontainers on the class and a static @Container field, so the container starts once for the class@ServiceConnection (Boot 3.1+) on that field: Boot takes the host, port and credentials from the running container and creates the connection details, so the DataSource points at it with no properties at all@DynamicPropertySource registers properties such as spring.datasource.url from the container@DataJpaTest, add @AutoConfigureTestDatabase(replace = Replace.NONE) so Boot doesn't swap in an embedded databaseTo keep the suite fast, share containers across test classes, for example through a common base class or @ImportTestcontainers, so Spring's context cache keeps working. Boot can also reuse the same container definitions at development time.
@SpringBootTest
@Testcontainers
class OrderRepositoryIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired OrderRepository orders;
@Test
void savesAndLoads() { /* runs against a real PostgreSQL */ }
}@ServiceConnection wires connection details automatically@DynamicPropertySource for manual propertiespersist and merge?hardAn entity is in one of four states relative to a persistence context:
new, unknown to JPA, no row yetpersist, find or a query. Changes are tracked by dirty checking and flushed automatically at commit, with no save call neededremove, deleted at flushpersist(entity) makes a new instance managed; the object you passed is the managed one. merge(entity) copies the state of a detached or new object onto a managed instance and returns that copy; the argument itself stays detached, which is a classic bug when code keeps using it.
Spring Data's save() calls persist if the entity looks new (a null id or null version, or Persistable.isNew()) and merge otherwise, and returns the managed instance, so always use the return value. Inside a transaction, an already-managed entity doesn't need save() at all.
persist attaches the same instancemerge returns a managed copysave picks persist or merge; use its return valueBoot builds the Environment from many property sources, and later sources override earlier ones. From lowest to highest precedence, the main ones are:
SpringApplication@PropertySource on configuration classesapplication.properties and application.yml-Dkey=value)SPRING_APPLICATION_JSON--server.port=9090@SpringBootTest(properties = ...), @DynamicPropertySource and @TestPropertySource, at the very topAmong config files, profile-specific files beat plain ones, and files outside the jar (the current directory or ./config/) beat those packaged inside. So a config/application-prod.yml next to the jar overrides the packaged defaults.
@PropertySource sits near the bottom and is added too late to affect early settings like logging.*. Environment variables map through relaxed binding: SPRING_DATASOURCE_URL sets spring.datasource.url.
application.yml@PropertySource is near the bottomI'd split it the way Boot does: an autoconfigure module with the code, and a starter module that only aggregates dependencies. I'd name it acme-sms-spring-boot-starter, since the spring-boot- prefix is reserved for official starters.
In the autoconfigure module:
@AutoConfiguration with @Bean methods@ConditionalOnClass so it applies only when the library is present, @ConditionalOnMissingBean on each bean so users can override it, and @ConditionalOnProperty for an on/off switch@ConfigurationProperties class under your own prefix, plus spring-boot-configuration-processor for IDE metadataMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports@AutoConfiguration(after = ...) if it depends on other auto-configured beansI'd test it with ApplicationContextRunner, which asserts which beans exist under different properties and classpaths without booting a full app. And I'd only use @ConditionalOnBean in auto-configuration classes, because it depends on which bean definitions exist at that point.
@AutoConfiguration
@ConditionalOnClass(SmsClient.class)
@EnableConfigurationProperties(SmsProperties.class)
public class SmsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
SmsClient smsClient(SmsProperties props) {
return new SmsClient(props.apiUrl(), props.apiKey());
}
}
// Listed in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports@AutoConfiguration listed in the imports file@ConditionalOnMissingBean so users can override@ConfigurationPropertiesApplicationContextRunnerYou enable it with @EnableMethodSecurity, which turns on @PreAuthorize, @PostAuthorize, @PreFilter and @PostFilter by default; @Secured and JSR-250 annotations like @RolesAllowed need securedEnabled = true or jsr250Enabled = true. It replaces the deprecated @EnableGlobalMethodSecurity, and Boot doesn't enable it for you.
@PreAuthorize("hasRole('ADMIN')") evaluates a SpEL expression before the call and can reference arguments, like #userId == authentication.name@PostAuthorize("returnObject.owner == authentication.name") checks after the call, against the returned value@PreFilter and @PostFilter filter collection arguments or resultsExpressions can call a bean, for example @access.canEdit(#id, authentication), which keeps complex rules in testable code.
It's implemented with Spring AOP, so the proxy rules apply: self-invocation isn't checked, and only Spring beans are protected. A denial throws AccessDeniedException, which becomes a 403. I use URL rules for coarse access and method security for domain rules like ownership.
@EnableMethodSecurity enables pre/post annotations@PreAuthorize evaluates SpEL before the callAccessDeniedException, a 403@Transactional, and which anomalies do they prevent?hardIsolation controls what a transaction can see of concurrent transactions. @Transactional(isolation = ...) offers:
DEFAULT (the default): use the database's own default, e.g. READ COMMITTED on PostgreSQL, Oracle and SQL Server, REPEATABLE READ on MySQL's InnoDBREAD_UNCOMMITTED: allows dirty reads of other transactions' uncommitted dataREAD_COMMITTED: no dirty reads, but reading a row twice can give different values (non-repeatable reads)REPEATABLE_READ: rows you've read stay stable, but the SQL standard still allows new matching rows to appear (phantom reads); some engines prevent those tooSERIALIZABLE: behaves as if transactions ran one at a time; the safest and slowest, and it produces serialization failures you must retryHigher isolation means more locking or more aborted transactions. The level is applied when a transaction starts, so an inner REQUIRED method that joins an existing transaction has its setting ignored by default. In practice I stay on READ COMMITTED and handle specific races with optimistic locking (@Version) or a pessimistic @Lock(LockModeType.PESSIMISTIC_WRITE) query.
DEFAULT uses the database default level@Transactional(readOnly = true) actually do?midIt's an optimization hint, not a guarantee that nothing gets written.
DataSource can use it to send queries to a read replica.What it doesn't do: it doesn't reliably block writes. Some databases reject INSERT or UPDATE inside a read-only transaction, others don't. And if the method joins an existing read-write transaction with the default REQUIRED propagation, its read-only flag is ignored.
Spring Data JPA's SimpleJpaRepository already marks its read methods readOnly = true. In my services I often put @Transactional(readOnly = true) at class level and override it with plain @Transactional on the write methods.
Spring Cloud is a family of projects that solve common distributed-system problems on top of Boot.
spring.config.import=configserver:...; the old bootstrap context is disabled by default. Beans marked @RefreshScope can pick up changes at runtime via /actuator/refresh, or on all instances with Spring Cloud Bus.@FeignClient interfaces. It's now considered feature-complete, and the Spring team suggests Spring's own HTTP interface clients for new code.On Kubernetes, the platform already provides much of this, like discovery, config maps and ingress, so I'd adopt only the pieces that add value.
spring.config.importResilience4j is a lightweight fault-tolerance library, integrated with Boot through resilience4j-spring-boot3 (plus the AOP and Actuator starters) or through the Spring Cloud CircuitBreaker abstraction. Its modules are CircuitBreaker, Retry, RateLimiter, Bulkhead and TimeLimiter.
A circuit breaker is a state machine:
CallNotPermittedException and the fallback runs, so the struggling dependency gets breathing roomRetries handle brief glitches, but only for idempotent calls and with backoff. The time limiter bounds latency, and a bulkhead caps concurrent calls so one slow dependency can't take all your threads. With annotations, Retry wraps the others by default, so it's the outermost layer. I monitor breaker states through Actuator and metrics.
@CircuitBreaker(name = "inventory", fallbackMethod = "stockUnknown")
@Retry(name = "inventory")
public StockLevel stock(String sku) {
return inventoryClient.stock(sku);
}
// Same parameters plus the exception
private StockLevel stockUnknown(String sku, Throwable ex) {
return StockLevel.unknown(sku);
}With the starters, your code logs through SLF4J and Boot uses Logback as the implementation; Boot's own internals use Commons Logging, routed to the same backend. By default it logs to the console at INFO level and writes no file unless you set logging.file.name or logging.file.path.
Configuration:
logging.level.root=warn, logging.level.com.acme=debug, or as environment variables like LOGGING_LEVEL_COM_ACME=DEBUGweb and sql: logging.level.sql=debugloggers endpointlogback-spring.xml rather than logback.xml, because the Spring variant supports <springProfile> and <springProperty>logging.structured.format.console=ecs, handy for log aggregationGood practice: use parameterized messages like log.info("Order {} placed", id), never log secrets or personal data, and carry a correlation id; with Micrometer Tracing, recent Boot versions add trace and span ids to log lines.
logging.level.* per logger, also via env varslogback-spring.xml for profile-aware configOn SIGTERM, Boot closes the application context. With graceful shutdown, the embedded server first stops accepting new requests and lets in-flight requests finish, for up to spring.lifecycle.timeout-per-shutdown-phase, 30 seconds by default. Then beans are destroyed: @PreDestroy methods run, connection pools close, executors shut down.
It's on by default since Boot 3.4; on earlier versions you set server.shutdown=graceful, because the old default was immediate. The readiness state also switches to refusing traffic.
On Kubernetes, removing the pod from load balancers and sending SIGTERM happen concurrently, so some requests can still arrive after shutdown begins. The documented fix is a preStop hook with a short sleep, so traffic stops being routed before the app stops accepting connections. Keep terminationGracePeriodSeconds (30 seconds by default) longer than the sleep plus the shutdown timeout, or the kubelet sends SIGKILL mid-drain.
For your own background work, stop consumers cleanly in @PreDestroy or a SmartLifecycle, and set spring.task.execution.shutdown.await-termination if queued async tasks must finish.
server.shutdown=graceful beforepreStop sleep on KubernetesDependencies are resolved once, when the singleton is created. Spring creates one ReportBuilder for that injection point and the singleton keeps the reference forever, so every call shares the same "prototype", and its state leaks between callers, possibly across threads.
The fixes all ask the container for an instance at call time:
ObjectProvider<ReportBuilder>: inject the provider and call getObject() each time. My usual choice, since it's explicit and easy to test.@Lookup method injection: a method annotated @Lookup that returns ReportBuilder; Spring overrides it with CGLIB so each call fetches a new bean.@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) injects a proxy, but for a prototype every method call on it hits a new instance, which is rarely what you want. Scoped proxies fit request and session scopes better.new inside the method.Remember too that Spring never calls destruction callbacks on prototypes, so any cleanup is yours.
@Component
@Scope("prototype")
class ReportBuilder { /* holds per-report state */ }
@Service
class ReportService {
private final ReportBuilder builder; // injected only once
ReportService(ReportBuilder builder) { this.builder = builder; }
}ObjectProvider.getObject() per use@Lookup method injection also worksspring.jpa.open-in-view is enabled by default in Spring Boot web apps. It registers an OpenEntityManagerInViewInterceptor that opens the JPA EntityManager at the start of each request and keeps it open until the request completes, so lazy associations can still be loaded in the controller or during JSON serialization, after the service transaction has ended. Boot logs a warning at startup if you haven't set the property explicitly.
Why teams set spring.jpa.open-in-view=false:
With it off, lazy access outside a transaction throws LazyInitializationException, which forces each use case to fetch what it needs in the service layer, with fetch joins, entity graphs or DTO projections. I disable it on new projects.
@Version.hardA lost update happens when two users read the same row, both change it, and the second write silently overwrites the first.
With optimistic locking, you add an attribute annotated @Version. JPA adds it to every update, as in update account set balance = ?, version = 4 where id = ? and version = 3, and increments it. If another transaction changed the row first, no row matches, and JPA throws an OptimisticLockException, which Spring translates into ObjectOptimisticLockingFailureException. No locks are held between read and write, so it scales well when conflicts are rare.
Handling the conflict: return a 409 Conflict so the client reloads and retries, or retry the whole transaction automatically when the operation is safe to repeat. Across HTTP requests, expose the version (for example as an ETag) and check the client's version on update, since each request loads a fresh entity.
Pessimistic locking, @Lock(LockModeType.PESSIMISTIC_WRITE) on a repository query, issues a SELECT ... FOR UPDATE and blocks other writers. It suits frequent conflicts or expensive retries, but keep those transactions short to limit contention and deadlocks.
@Entity
public class Account {
@Id @GeneratedValue
private Long id;
@Version
private Long version; // checked and incremented on every update
private BigDecimal balance;
}@Version adds a version check to updatesOptimisticLockExceptionFOR UPDATE for frequent conflictsThe standard options:
CommandLineRunner or ApplicationRunner beans: their run method is called after the context has started, just before the app is considered ready. CommandLineRunner receives the raw String[] arguments; ApplicationRunner gets parsed ApplicationArguments with option and non-option arguments. Order several with @Order.@EventListener(ApplicationReadyEvent.class) method, which fires once the application is ready to serve traffic.@PostConstruct for the initialization of a single bean. It runs during context creation, before transactional proxies are reliably usable, so it's for the bean's own setup rather than application-wide startup work.An exception thrown from a runner fails startup, which is useful for mandatory checks. Long-running work in a runner delays readiness, because Boot only reports ready after all runners finish, so I move lengthy jobs to a background executor.
CommandLineRunner gets the raw String argumentsApplicationRunner gets parsed ApplicationArgumentsApplicationReadyEvent fires when ready for traffic@PostConstruct is only for per-bean setupSpringApplication.run()? Walk through Spring Boot startup.hardRoughly, in order:
ApplicationStartingEventEnvironment: property sources from command-line args, environment variables, system properties and config files, plus active profiles. EnvironmentPostProcessors run here and the logging system is initializedApplicationContext matching the type, for example a servlet web server contextBeanFactoryPostProcessors, register BeanPostProcessors, create the embedded web server, and instantiate all non-lazy singletonsApplicationStartedEventApplicationRunners and CommandLineRunnersApplicationReadyEvent; readiness switches to accepting trafficIf startup fails, FailureAnalyzers turn common exceptions, like a port already in use, into readable messages. For slow startups, the Actuator startup endpoint (with BufferingApplicationStartup) shows where the time goes.
ApplicationReadyEvent firesNo questions match that filter.
Prefer multiple choice? All 21 Spring Boot MCQs with answers →