Top 50 Spring Boot Interview Questions 2026
If you are interviewing for a Java backend role, the Spring Boot round is almost guaranteed. It shows up for junior developers, for senior engineers moving between JVM shops, and for architects who are expected to explain why a service behaves the way it does under load. The questions look simple - "what does @Transactional do?" - and the follow-ups are where the round is decided.
What the interviewer is really testing is whether you understand the machinery under the annotations: the container, the proxies, the auto-configuration conditions, the persistence context. Someone who knows that a transactional method called from the same class runs without a transaction will find the bug in ten minutes. Someone who only knows the annotation will spend a day. The answers below target Spring Boot 3.x and 4.x (Spring Framework 6 and 7, Java 17+, the jakarta.* namespace), and the questions escalate from fundamentals to the debugging scenarios live rounds actually use.
Practice alongside this post. Drill the language underneath in the Java question bank and the persistence side in the databases question bank; keep the REST API design cheat sheet and the concurrency essentials cheat sheet open for Parts 5 and 8. If you want a structured path rather than a list, the backend junior-to-senior track covers the same ground in order. Sibling posts: Top 50 Java interview questions, Top 50 microservices interview questions and Top 50 PostgreSQL interview questions.
Part 1: Core container - IoC and dependency injection (1-8)
1. What is Inversion of Control, and how does Spring implement it?
Inversion of Control means your classes do not create or look up their own collaborators - something outside them does. Spring implements it with a container (the ApplicationContext) that reads bean definitions, instantiates objects, wires their dependencies and manages their lifecycle. Dependency injection is the specific technique: the container passes dependencies in through constructors, setters or fields.
The payoff the interviewer wants you to name is not "less code". It is that wiring is decided in one place, so you can swap an implementation (a real payment gateway for a stub in tests, a different DataSource per environment) without touching the classes that use it.
2. Constructor, setter or field injection - which do you use and why?
Constructor injection, by default. It makes dependencies explicit, lets fields be final, makes the class usable in a plain unit test with new, and fails fast at startup if a dependency is missing. Since Spring 4.3, a class with a single constructor does not need @Autowired on it.
@Service
public class OrderService {
private final OrderRepository orders;
private final PaymentClient payments;
public OrderService(OrderRepository orders, PaymentClient payments) {
this.orders = orders;
this.payments = payments;
}
}
Setter injection is for genuinely optional dependencies. Field injection (@Autowired on a private field) hides dependencies and needs reflection or a Spring context to test. The follow-up: "a constructor with nine parameters" - that is a design smell telling you the class does too much, not a reason to switch to field injection.
3. What is the difference between @Component, @Service, @Repository and @Controller?
All four are stereotype annotations that mark a class for component scanning. @Service, @Repository and @Controller are meta-annotated with @Component. The differences are behavioural only in two cases:
@Repository enables persistence exception translation - vendor exceptions are translated into Spring's DataAccessException hierarchy.
@Controller (and @RestController, which adds @ResponseBody) is picked up by Spring MVC as a request handler.
@Service is purely semantic. Using them correctly still matters because aspects and tooling can target them by stereotype.
4. When do you use @Bean instead of @Component?
@Component goes on your own classes and relies on component scanning. @Bean goes on a method inside a @Configuration class and is used when you cannot annotate the class (a third-party ObjectMapper, an HTTP client, a Clock) or when construction needs logic.
The gotcha: inside a @Configuration class, Spring proxies the class with CGLIB so that calling one @Bean method from another returns the singleton, not a new instance. With @Configuration(proxyBeanMethods = false) - which Spring Boot's own auto-configurations use for faster startup - that inter-method call creates a new object. Inject the dependency as a method parameter instead and both modes behave the same.
5. Two beans implement the same interface. How does Spring decide which one to inject?
If more than one candidate exists, startup fails with NoUniqueBeanDefinitionException unless you disambiguate:
@Primary on one bean makes it the default.
@Qualifier("stripe") at the injection point picks by name.
- Injecting
List<PaymentGateway> or Map<String, PaymentGateway> gives you all of them - the map is keyed by bean name, which is the clean basis for a strategy pattern.
- Spring also falls back to matching the parameter name against bean names.
The senior follow-up is "how would you choose the implementation at runtime per request?" - inject the map and look it up by a key, rather than branching on if statements.
6. What are the bean scopes, and what goes wrong when you inject a prototype into a singleton?
| Scope | One instance per |
|---|
singleton (default) | Container |
prototype | Every injection or lookup |
request | HTTP request |
session | HTTP session |
application | ServletContext |
websocket | WebSocket session |
The trap: a singleton is wired once, so a prototype injected into it is also created once and reused forever - it silently behaves like a singleton. Fixes are ObjectProvider<T> (call getObject() each time you need a fresh one), a @Lookup method, or a scoped proxy (@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)). Request- and session-scoped beans injected into singletons need a scoped proxy for the same reason.
7. Walk through the bean lifecycle.
- Instantiate (constructor).
- Populate properties (setter and field injection).
Aware callbacks (BeanNameAware, ApplicationContextAware and so on).
BeanPostProcessor.postProcessBeforeInitialization.
- Initialization:
@PostConstruct, then InitializingBean.afterPropertiesSet, then a custom initMethod.
BeanPostProcessor.postProcessAfterInitialization - this is where AOP proxies (transactions, caching, security) are created and swapped in.
- Bean in use.
- On shutdown:
@PreDestroy, DisposableBean.destroy, custom destroyMethod.
Two follow-ups. Spring does not call destroy callbacks on prototype beans - you own their cleanup. And because the proxy is created in step 6, calling a @Transactional method from @PostConstruct on the same bean runs on the raw object with no transaction. Use an ApplicationReadyEvent listener for startup work that needs proxies.
8. How does Spring Boot handle circular dependencies?
Since Spring Boot 2.6, circular references are prohibited by default and startup fails with a message showing the cycle. You can re-enable them with spring.main.allow-circular-references=true, and that only ever worked for setter or field injection - two beans that need each other in their constructors can never be built.
The answer interviewers want is that a cycle is a design problem: extract the shared logic into a third bean, publish an event instead of calling back, or inject ObjectProvider to defer the lookup. @Lazy on one injection point breaks the cycle with a proxy but hides the coupling rather than removing it.
Part 2: Spring Boot fundamentals - auto-configuration and config (9-17)
9. What does Spring Boot add on top of the Spring Framework?
Spring Boot is opinionated configuration for the Spring Framework, not a different framework. It adds:
- Auto-configuration - beans configured for you based on what is on the classpath and what properties are set.
- Starters - curated dependency sets (
spring-boot-starter-web, -data-jpa, -security) with versions managed by the Boot BOM.
- Embedded servers - Tomcat by default, so the app runs as an executable jar.
- Externalized configuration with a well-defined precedence order.
- Production features - Actuator health, metrics and info endpoints.
The one-line version: Spring gives you the container, Boot decides sensible defaults for it and gets out of the way when you define your own.
10. What does @SpringBootApplication do?
It combines three annotations:
@SpringBootConfiguration - a specialised @Configuration.
@EnableAutoConfiguration - turns on auto-configuration.
@ComponentScan - scans the package of the annotated class and everything below it.
The classic bug that follows: a @Service in com.acme.shared is not found because the main class lives in com.acme.orders. Component scanning only goes down from the main class's package. Put the main class at the root package, or add scanBasePackages.
11. How does auto-configuration actually work?
At startup, Boot loads the list of auto-configuration classes from META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports in each jar (the older spring.factories mechanism for auto-configuration was removed in Boot 3.0). Each class is a @AutoConfiguration guarded by conditions:
@AutoConfiguration
@ConditionalOnClass(DataSource.class)
@ConditionalOnProperty(prefix = "app.audit", name = "enabled", havingValue = "true")
public class AuditAutoConfiguration {
@Bean
@ConditionalOnMissingBean
AuditWriter auditWriter(DataSource ds) {
return new JdbcAuditWriter(ds);
}
}
@ConditionalOnMissingBean is the key idea: auto-configuration backs off the moment you define your own bean of that type. Auto-configurations are processed after your own bean definitions, which is why that check works.
Run with --debug (or debug=true) to print the condition evaluation report at startup. It lists positive matches, negative matches and the exact condition that failed - "@ConditionalOnClass did not find required class" or "@ConditionalOnMissingBean found beans of type...". With Actuator, the /actuator/conditions endpoint gives the same report on a running app, and /actuator/beans lists every bean and its dependencies.
To switch an auto-configuration off entirely: @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) or the spring.autoconfigure.exclude property.
13. What is a starter, and how would you build your own?
A starter is a dependency-only module that pulls in a library plus its auto-configuration, so a consumer adds one dependency and gets a working default. The convention for third-party starters is acme-spring-boot-starter, never spring-boot-starter-acme (that prefix is reserved for the Spring team).
A custom starter usually has two parts: an autoconfigure module with @AutoConfiguration classes, @ConfigurationProperties and the AutoConfiguration.imports registration, and the starter module that depends on it plus the library. Use @ConditionalOnMissingBean on everything so consuming teams can override any bean. This is a common senior question at companies with an internal platform team.
14. Explain externalized configuration and property precedence.
Boot reads configuration from many sources and the more specific one wins. Simplified, from highest to lowest:
- Command-line arguments (
--server.port=9000).
SPRING_APPLICATION_JSON.
- OS environment variables (
SERVER_PORT maps to server.port via relaxed binding).
- Profile-specific files outside the jar, then inside it (
application-prod.yml).
application.yml / application.properties outside the jar, then inside it.
- Defaults set in code.
Test-specific sources (@TestPropertySource, @DynamicPropertySource) sit above all of these. The practical rule: ship safe defaults in the jar, and let the environment (Kubernetes env vars, a config server, a secret manager) override per deployment. Secrets never live in application.yml in the repo.
15. @Value or @ConfigurationProperties?
@ConfigurationProperties for anything more than one or two values. It binds a whole prefix into a typed object, supports validation, relaxed binding and IDE metadata, and with Java records it is immutable:
@ConfigurationProperties(prefix = "app.payments")
@Validated
public record PaymentProperties(
@NotBlank String apiUrl,
@DurationUnit(ChronoUnit.SECONDS) Duration timeout,
@Min(1) int maxRetries) {}
Register it with @EnableConfigurationProperties(PaymentProperties.class) or @ConfigurationPropertiesScan. @Value("${app.payments.api-url}") is fine for a single value but scatters string keys through the code and does not validate. The gotcha: @Validated makes a bad config fail at startup, which is exactly when you want it to fail.
16. How do profiles work, and what are the common mistakes?
Profiles group beans and configuration per environment. Activate with spring.profiles.active=prod (or SPRING_PROFILES_ACTIVE), and Boot loads application-prod.yml on top of application.yml. Beans can be restricted with @Profile("prod") or @Profile("!prod"). Profile groups (spring.profiles.group.prod=prod-db,prod-mq) activate several at once.
Mistakes interviewers probe: putting business logic behind profiles so the code that runs in prod is never run in tests; a @Profile("dev") bean that silently disappears in prod and nothing else provides the type; and building one artifact per environment instead of one artifact configured per environment.
17. What is the difference between Spring Boot 3 and Spring Boot 4?
Spring Boot 3.0 (late 2022) was the big break: Java 17 baseline, Spring Framework 6, and the move from javax.* to jakarta.* for Servlet, JPA and Validation. It also introduced first-class GraalVM native image support and Micrometer Observation.
Spring Boot 4.0 went GA in November 2025 on Spring Framework 7. It keeps Java 17 as the minimum while supporting newer JDKs, moves to Jakarta EE 11 and Jackson 3 (new tools.jackson packages), splits auto-configuration into smaller modules, and adds framework features such as API versioning and built-in retry. Spring Boot 3.5 is the stepping stone - fix its deprecation warnings first. The interview point is less the feature list than the migration habit: upgrade one major at a time and read the release notes, because removed APIs only fail at compile or startup time.
Part 3: Transactions and the proxy model (18-23)
18. How does @Transactional work under the hood?
Spring wraps the bean in an AOP proxy - a JDK dynamic proxy if it implements an interface and is configured that way, otherwise a CGLIB subclass (Boot defaults to CGLIB). When a caller invokes a transactional method through the proxy, a TransactionInterceptor asks the PlatformTransactionManager to begin or join a transaction, binds the connection to the current thread, calls the real method, then commits or rolls back.
Everything that goes wrong with @Transactional follows from two facts in that description: it only works when the call goes through the proxy, and the transaction is bound to the current thread.
19. Why does @Transactional not work when I call the method from the same class?
Self-invocation. this.saveAudit() calls the method on the target object, not on the proxy, so the interceptor never runs.
@Service
public class OrderService {
public void placeOrder(Order o) {
save(o);
}
@Transactional
public void save(Order o) { ... }
}
Fixes, in order of preference: move the transactional method to another bean; put @Transactional on the outer public method that is actually called from outside; or use TransactionTemplate for programmatic control. Injecting the bean into itself works but is a workaround interviewers will question. Related: with proxy-based AOP, private and final methods cannot be intercepted, so @Transactional on them has no effect - treat public methods on Spring beans as the safe assumption.
20. What are the propagation levels you actually use?
| Propagation | Behaviour |
|---|
REQUIRED (default) | Join the current transaction, or start one |
REQUIRES_NEW | Suspend the current one, start an independent transaction |
NESTED | Savepoint inside the current transaction (JDBC only) |
SUPPORTS | Join if one exists, else run without |
MANDATORY | Fail if there is no transaction |
NOT_SUPPORTED / NEVER | Run without; suspend or fail if one exists |
REQUIRES_NEW is the one with a real use case - an audit or outbox record that must commit even if the business transaction rolls back. The gotcha: it needs a second database connection while the first is suspended, so heavy use under load can exhaust the pool and deadlock the app on itself.
21. Which exceptions trigger a rollback?
By default, only unchecked exceptions (RuntimeException and Error). A checked exception propagating out of a @Transactional method commits the transaction. That surprises people with code like throws IOException or a custom checked PaymentException.
@Transactional(rollbackFor = PaymentException.class)
public void charge(Order o) throws PaymentException { ... }
Second trap: if you catch the exception inside the method and swallow it, there is nothing for the proxy to see, so it commits. Third: an inner REQUIRED method that throws marks the shared transaction rollback-only; if the outer method catches and continues, the commit fails with UnexpectedRollbackException. The fix is to decide where the transaction boundary really is, not to add more catch blocks.
22. What does readOnly = true do?
It is a hint, not a guarantee. With JPA and Hibernate it sets the session flush mode so dirty checking and flushing are skipped, which saves memory and CPU on large reads. The JDBC driver may also be told the connection is read-only, which some databases and routing data sources use to send the query to a replica. It does not stop you from issuing an UPDATE statement on every database. A common pattern is @Transactional(readOnly = true) at the class level of a query service and plain @Transactional on the few write methods.
23. What happens if you call a remote HTTP API inside a transaction?
The database connection stays checked out of the pool, and row locks stay held, for the whole duration of the HTTP call - including timeouts and retries. Under load a slow downstream service exhausts the connection pool (HikariCP defaults to 10 connections) and the whole service stops serving requests that never touch that downstream.
Keep transactions short and database-only. Do the remote call before or after, and if you need the external effect and the database write to be consistent, use the transactional outbox pattern: write the event in the same transaction and publish it asynchronously. The Top 50 Kafka interview questions post covers the publishing side.
Part 4: Data access - JPA and Hibernate (24-30)
24. What is the N+1 query problem and how do you fix it in Spring Data JPA?
You load N parent rows with one query, then touching a lazy association on each one fires another query per row: 1 + N round trips.
List<Order> orders = orderRepository.findAll();
orders.forEach(o -> o.getItems().size());
Fixes:
JOIN FETCH in a JPQL query: select o from Order o join fetch o.items.
@EntityGraph(attributePaths = "items") on the repository method.
- Batch fetching (
spring.jpa.properties.hibernate.default_batch_fetch_size=50) to load associations in chunks with IN clauses.
- A DTO projection that selects only the columns the endpoint needs.
Gotcha: fetch-joining a collection with pagination makes Hibernate paginate in memory (it logs a warning). For paged lists, page the parent IDs first, then fetch the associations for that page.
25. Explain lazy loading and LazyInitializationException.
@OneToMany and @ManyToMany default to LAZY; @ManyToOne and @OneToOne default to EAGER in JPA - and most teams switch those to LAZY explicitly. A lazy association is a proxy that loads on first access, which needs an open persistence context. Access it after the transaction and session are closed - in a controller, in a Jackson serializer - and you get LazyInitializationException.
The right fix is to load what the use case needs inside the transaction (fetch join, entity graph, projection) and return a DTO. The wrong fixes are switching associations to EAGER globally, which creates N+1 everywhere, or relying on open-in-view (next question).
26. What is spring.jpa.open-in-view and should you disable it?
Open Session in View keeps the persistence context open for the entire web request, so lazy loading works in controllers and view rendering. Spring Boot enables it by default and logs a warning at startup saying so.
The cost: the database connection can be held for the whole request, and lazy loads fire from serialization code where nobody sees the queries, so N+1 problems hide until production load. Most teams set spring.jpa.open-in-view=false on new services and fix the resulting LazyInitializationExceptions by fetching explicitly. Being able to name the warning and the trade-off is a strong signal.
27. When would you skip JPA and use JdbcClient or JdbcTemplate?
JPA earns its keep for aggregate-style domain models with modest write paths. Plain SQL wins for reporting queries, bulk updates, database-specific features (window functions, ON CONFLICT, CTEs) and hot paths where you need to see exactly what runs. Spring Framework 6.1 added JdbcClient, a fluent API over JdbcTemplate:
List<OrderSummary> rows = jdbcClient
.sql("select id, total from orders where customer_id = :cid")
.param("cid", customerId)
.query(OrderSummary.class)
.list();
Mixing both in one service is normal - they share the same DataSource and transaction manager. If your loop is heavy on PostgreSQL, the Top 50 PostgreSQL interview questions post covers the query-planning side, and the SQL query tuning cheat sheet is a good companion.
28. What is the persistence context, and what is dirty checking?
The persistence context is the first-level cache attached to an EntityManager: within a transaction, each entity row is represented by exactly one managed object. Dirty checking means Hibernate snapshots managed entities when loaded and, at flush time, issues UPDATEs for anything that changed - you never call save() on a managed entity.
Follow-ups: loading 100,000 entities in one transaction keeps them all in memory with snapshots, so batch jobs need to flush() and clear() periodically or use a stateless approach. And repository.save() on a detached entity calls merge, which returns a different instance - code that keeps using the original object is a classic bug.
29. How do you handle concurrent updates to the same row?
Optimistic locking: add a @Version column. Hibernate includes it in the WHERE clause of every update and increments it; if another transaction got there first, zero rows are updated and you get ObjectOptimisticLockingFailureException. Map that to a 409 Conflict, or retry if the operation is safe to repeat.
Pessimistic locking: @Lock(LockModeType.PESSIMISTIC_WRITE) on a repository method issues SELECT ... FOR UPDATE. Use it when conflicts are frequent and retrying is expensive, and keep the transaction short. Optimistic is the default choice for web apps because most requests do not collide.
30. How do you manage schema changes?
Never spring.jpa.hibernate.ddl-auto=update in production - it cannot rename, cannot migrate data, and does different things on different databases. Use Flyway or Liquibase; Boot auto-configures both when they are on the classpath and runs migrations at startup. Set ddl-auto=validate (or none) so Hibernate checks the mapping against the migrated schema.
The senior follow-up is zero-downtime migrations with rolling deploys: old and new versions run at the same time, so changes must be backward compatible - add the column, deploy code that writes both, backfill, switch reads, then drop the old column in a later release.
Part 5: Web layer - REST, validation and errors (31-36)
31. @Controller vs @RestController, and how does a request reach your method?
@RestController is @Controller plus @ResponseBody, so return values are serialized to the response body by an HttpMessageConverter (Jackson for JSON) instead of being resolved as view names.
The path of a request: the servlet container hands it through the filter chain (including Spring Security's) to DispatcherServlet, which asks the HandlerMapping for the matching @RequestMapping method, runs any HandlerInterceptors, resolves arguments (@PathVariable, @RequestBody, validation), invokes the method, and converts the return value. Exceptions go to HandlerExceptionResolvers, which is where @ControllerAdvice plugs in. Knowing that filters run before DispatcherServlet and interceptors run inside it answers a whole family of follow-ups.
32. How does request validation work?
Add spring-boot-starter-validation - it is not included in the web starter. Put Jakarta Bean Validation constraints on the DTO and @Valid on the parameter:
public record CreateUserRequest(
@NotBlank @Email String email,
@Size(min = 12) String password) {}
@PostMapping("/users")
ResponseEntity<UserDto> create(@Valid @RequestBody CreateUserRequest req) { ... }
A failing body throws MethodArgumentNotValidException, which Boot maps to 400 Bad Request. Constraints directly on method parameters (@PathVariable @Min(1) long id) are handled by Spring Framework 6.1+ built-in method validation and raise HandlerMethodValidationException. Follow-up: validation groups for create-vs-update rules, and custom constraints via ConstraintValidator for cross-field checks.
33. How do you handle exceptions globally, and what is ProblemDetail?
@RestControllerAdvice with @ExceptionHandler methods centralizes error mapping. Spring Framework 6 added ProblemDetail, an implementation of RFC 9457 (which superseded RFC 7807) "problem details for HTTP APIs" with type, title, status, detail and instance fields:
@RestControllerAdvice
class ApiErrors {
@ExceptionHandler(OrderNotFoundException.class)
ProblemDetail notFound(OrderNotFoundException ex) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
pd.setTitle("Order not found");
pd.setProperty("orderId", ex.orderId());
return pd;
}
}
Set spring.mvc.problemdetails.enabled=true to get problem-detail bodies for Spring MVC's built-in exceptions too, or extend ResponseEntityExceptionHandler. The interviewer's follow-up: never leak stack traces or SQL in detail, and log the full exception with a correlation ID instead.
34. Which HTTP client do you use in a modern Spring Boot app?
RestClient (Spring Framework 6.1+) - the synchronous, fluent client and the default choice for blocking apps.
- HTTP interface clients - declare an interface with
@GetExchange / @PostExchange methods and Spring generates the implementation, much like Feign.
WebClient - the reactive client, for WebFlux apps or when you need streaming.
RestTemplate - still works, but is in maintenance mode; new code should use RestClient.
Whatever the client, the senior answer is about configuration: set connect and read timeouts explicitly (no timeout means a slow dependency can hold threads indefinitely), add retries only for idempotent calls, and put a circuit breaker (Resilience4j) in front of flaky dependencies.
35. What is the difference between Spring MVC and Spring WebFlux?
Spring MVC is the Servlet stack: one thread per request, blocking I/O, simple stack traces. WebFlux is the reactive stack on Reactor and non-blocking servers (Netty by default): a small number of event-loop threads handle many concurrent connections, but every call in the path must be non-blocking - one JDBC call on an event loop stalls everything on it.
With virtual threads (Part 8), a lot of the scalability argument for WebFlux on typical CRUD services has weakened, because blocking MVC code can now handle high concurrency cheaply. WebFlux still makes sense for streaming, server-sent events, and apps built around reactive drivers end to end. "We chose MVC plus virtual threads because the team writes imperative code and the database is the bottleneck anyway" is a good answer.
36. How do you version and document a REST API?
Common versioning options are a URL prefix (/v1/orders), a header, or a media type. URL versioning is the most visible and easiest to route and cache. Spring Framework 7 adds first-class API versioning support on request mappings, so in Boot 4 you can declare versions without hand-rolled routing.
For documentation, springdoc-openapi generates an OpenAPI spec and Swagger UI from your controllers and annotations. The follow-up is about compatibility: additive changes (new optional fields) are non-breaking, and clients should ignore unknown fields, so you need a new version only for removals and semantic changes. The REST API design cheat sheet covers status codes and idempotency for this part of the round.
Part 6: Spring Security (37-40)
37. How does the Spring Security filter chain work?
Spring Security plugs a single DelegatingFilterProxy into the servlet container, which delegates to a FilterChainProxy holding one or more SecurityFilterChains. Each chain matches a set of requests and runs an ordered list of filters - CORS, CSRF, authentication (bearer token, form login), exception translation, and finally authorization.
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
WebSecurityConfigurerAdapter was removed in Spring Security 6; configuration is now SecurityFilterChain beans and the lambda DSL. The authenticated user lives in the SecurityContextHolder, which is thread-bound by default - relevant when work moves to another thread.
38. How do you secure an API with JWTs?
Make the service an OAuth2 resource server. Add spring-boot-starter-oauth2-resource-server and point it at the issuer:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com/
Spring discovers the JWKS endpoint, verifies the token signature, and checks expiry and issuer. Map scopes or custom claims to authorities with a JwtAuthenticationConverter, and enforce audience if your identity provider issues tokens for several APIs.
Follow-ups interviewers like: do not build your own token issuer if an identity provider can do it; JWTs cannot be revoked before expiry without extra state, so keep access tokens short-lived; never put sensitive data in the payload, because it is only encoded, not encrypted.
39. When should CSRF protection be on?
CSRF attacks rely on the browser attaching credentials automatically - cookies. If your API authenticates with session cookies, keep CSRF protection on. If it is a stateless API that only accepts bearer tokens in the Authorization header, a cross-site form cannot attach that header, so disabling CSRF for that chain is standard. The mistake to avoid is the copy-pasted csrf.disable() on an app that also uses cookie sessions for a browser front end.
40. How do you do method-level authorization?
Enable it with @EnableMethodSecurity, then use @PreAuthorize with SpEL:
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.name")
public UserProfile getProfile(String userId) { ... }
It is proxy-based, exactly like @Transactional, so self-invocation bypasses it. For object-level rules ("can this user see this order?"), call a bean from the expression (@orderAccess.canRead(#id, authentication)) or check ownership in the query itself. URL rules in the filter chain are coarse-grained; method security is where tenant and ownership checks belong.
Part 7: Production - Actuator, observability and testing (41-45)
41. What does Actuator give you, and how do you expose it safely?
Actuator adds operational endpoints: health, info, metrics, prometheus (with the Micrometer Prometheus registry), loggers (change log levels at runtime), env, beans, conditions, threaddump, heapdump and more. By default only health is exposed over HTTP.
Expose deliberately: management.endpoints.web.exposure.include=health,info,prometheus, run management on a separate port (management.server.port) that is not routed publicly, and secure it. env, heapdump and configprops can leak secrets and should never be reachable from the internet. Kubernetes liveness and readiness groups are available at /actuator/health/liveness and /actuator/health/readiness.
42. What is the difference between liveness and readiness, and what belongs in each?
Readiness answers "should this instance get traffic right now?" - failing it removes the pod from the load balancer. Liveness answers "is this process broken beyond recovery?" - failing it restarts the pod.
The trap is putting the database health check in liveness. When the database has a blip, every pod fails liveness and Kubernetes restarts the whole fleet at once, turning a dependency outage into your outage. Dependencies belong in readiness at most; liveness should reflect only the application's own internal state. Boot's availability state support (LivenessState, ReadinessState) is built for this split.
43. How do you do metrics and tracing in Spring Boot 3 and later?
Micrometer is the metrics facade: Boot auto-configures timers for HTTP requests, JVM, connection pool and more, and you add your own with MeterRegistry or @Timed/@Counted. The Observation API (Micrometer 1.10+) lets one instrumentation produce metrics and trace spans together.
For tracing, Micrometer Tracing replaced Spring Cloud Sleuth in Boot 3, with bridges to OpenTelemetry or Brave and exporters such as OTLP. Trace and span IDs are propagated across HTTP and messaging and can be put into log lines so you can jump from a log to the trace. The interviewer's follow-up: watch tag cardinality - a user ID or raw URL as a metric tag can overwhelm your metrics backend.
44. @SpringBootTest vs test slices - when do you use each?
| Annotation | Loads | Use for |
|---|
@SpringBootTest | Full application context | Integration tests across layers |
@WebMvcTest | MVC layer, one or more controllers | Request mapping, validation, error bodies, security rules |
@DataJpaTest | JPA repositories, EntityManager | Queries, mappings; transactional and rolled back per test |
@JsonTest | Jackson | Serialization contracts |
@RestClientTest | RestClient/RestTemplate + mock server | HTTP client code |
Slices are faster and fail for one reason. Mock collaborators in slices with @MockitoBean - Spring Boot 3.4 deprecated @MockBean in favour of Spring Framework's @MockitoBean. Gotcha: every different combination of mocked beans and properties creates a new cached context, so test suites with many distinct @SpringBootTest configurations get slow; standardize a few base configurations.
45. How do you test against a real database?
Testcontainers. Start the same database you run in production (PostgreSQL, not H2) in a container, and since Boot 3.1 @ServiceConnection wires the connection details automatically:
@SpringBootTest
@Testcontainers
class OrderRepositoryIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Test
void savesAndFindsOrders() { ... }
}
H2 in PostgreSQL mode passes tests that fail in production - different SQL dialect, locking, JSON types and constraint behaviour. If a @DataJpaTest still swaps in an embedded database, add @AutoConfigureTestDatabase(replace = Replace.NONE). Boot also supports running the app locally against the same containers for development. The Top 50 Docker interview questions post covers the container side.
Part 8: Modern runtime - virtual threads and native images (46-47)
46. How do you enable virtual threads, and what should you watch for?
On Java 21+, set spring.threads.virtual.enabled=true (Boot 3.2+). Boot then runs Tomcat request handling, @Async tasks and several other executors on virtual threads. Blocking I/O parks the virtual thread instead of an expensive platform thread, so a plain blocking MVC app can hold far more concurrent in-flight requests.
What to watch for:
- The bottleneck moves. Thousands of concurrent requests still share a connection pool of 10-20 database connections. Without limits you trade thread exhaustion for pool timeouts or a database overload. Bound concurrency to downstream resources explicitly.
- Pinning. On JDK 21-23, blocking inside a
synchronized block pins the virtual thread to its carrier. JDK 24 (JEP 491) removed most of that pinning; on older JDKs prefer ReentrantLock in hot paths.
ThreadLocal-heavy code creates one copy per virtual thread, which adds up at high concurrency.
- They help I/O-bound work, not CPU-bound work.
47. What is a GraalVM native image, and what are the trade-offs?
Spring Boot's AOT processing analyses the application at build time and generates code and configuration so GraalVM can compile it ahead of time into a native executable (./mvnw -Pnative native:compile, or a container image with Buildpacks). Native images start in a fraction of the time and use less memory - useful for scale-to-zero, serverless and CLI tools.
Trade-offs: builds are slow and memory-hungry; it is a closed world, so reflection, proxies and resources not discovered at build time need hints (RuntimeHints, @RegisterReflectionForBinding); the bean graph is fixed at build time, so @Profile and @ConditionalOnProperty choices cannot change at runtime; and peak throughput for long-running services is often better on the JIT. A common middle ground is staying on the JVM with Class Data Sharing or CRaC for faster startup.
Part 9: Debugging and design scenarios (48-50)
These are the questions that live rounds actually run on - a symptom, some code, and "walk me through it."
48. "Startup fails with NoSuchBeanDefinitionException (or UnsatisfiedDependencyException). How do you debug it?"
Read the whole message - Boot's failure analyzer usually names the missing type and the injection point. Then check, in order:
- Is the class scanned? It must be in or below the main class's package, and annotated with a stereotype.
- Is the bean conditional?
@Profile, @ConditionalOnProperty or an excluded auto-configuration. Run with --debug and read the condition report for that type.
- Is the starter or dependency present? A
JdbcTemplate bean will not exist without a DataSource, which will not exist without a driver and URL.
- Is it a test-slice problem?
@WebMvcTest does not load @Service beans - mock them with @MockitoBean.
- Type mismatch? Asking for the concrete class when only an interface-based JDK proxy exists.
Narrating this order is the answer - interviewers want the method, not a lucky guess.
49. "Under load, requests start timing out with HikariPool - Connection is not available, request timed out. Diagnose it."
The pool is exhausted: every connection is checked out and new requests wait past connectionTimeout (30 seconds by default). Likely causes:
- Long transactions - an HTTP call or slow computation inside
@Transactional (question 23).
- Open-in-view holding connections for the whole request, including slow serialization (question 26).
REQUIRES_NEW needing a second connection per request while the first is held (question 20).
- Slow queries - N+1 or missing indexes increasing hold time.
- Leaks - manual
getConnection() without closing.
- Concurrency beyond the pool - for example after enabling virtual threads.
How to confirm: Actuator metrics hikaricp.connections.active and hikaricp.connections.pending, a thread dump to see where request threads are waiting, and spring.datasource.hikari.leak-detection-threshold to log stack traces of connections held too long. The fix is almost never "raise the pool size" - that moves the overload to the database.
50. "A customer was charged but the order was not saved. Here is the code. What is wrong, and how would you redesign it?"
@Service
public class CheckoutService {
public void checkout(Cart cart) {
Order order = buildOrder(cart);
paymentClient.charge(cart.total());
persist(order);
}
@Transactional
public void persist(Order order) {
orderRepository.save(order);
inventoryRepository.reserve(order.items());
}
}
Problems: persist is self-invoked, so there is no transaction and the order save and reservation are not atomic. The charge happens before anything is persisted, so a failure afterwards leaves money taken with no order. If the reservation throws a checked exception, even a working transaction would commit.
Redesign: persist the order as PENDING and reserve inventory in one transaction in a separate bean; charge with an idempotency key derived from the order ID so retries cannot double-charge; then mark the order PAID in a second transaction. If the charge fails, release the reservation. For cross-service steps, publish events through an outbox and use a saga with compensating actions. The Top 50 microservices interview questions post covers sagas and idempotency, and the payments system design walkthrough covers the end-to-end design.
How to practice for a Spring Boot interview
- Build one small service end to end - REST controller, validation,
ProblemDetail errors, JPA with Flyway, Spring Security as a JWT resource server, Actuator, and Testcontainers tests. Every question above maps to a part of it.
- Break it on purpose. Self-invoke a
@Transactional method, throw a checked exception, add an N+1 loop with SQL logging on (spring.jpa.show-sql or Hibernate statistics), shrink the Hikari pool to 2 and load-test it. Seeing each failure once makes the answers stick.
- Read the condition report (
--debug) for your own app once. Auto-configuration stops being magic after you have read why each bean exists.
- Know the language underneath. Many "Spring" questions are really Java questions about proxies, threads and collections - drill them in the Java question bank.
- Narrate debugging out loud. For the scenario questions, say the order you would check things in before you jump to a fix. That method is what is being scored.
- Timebox scenarios to 10-15 minutes, and practise saying "I would confirm it with this metric" when you are not sure.
Keep going
#springboot #java #spring #backend #interviews #microservices #career