Top 50 Spring Boot Interview Questions for 2026 (With Detailed Answers)
These are the questions that actually come up in Spring Boot interviews — at startups, scale-ups, and large enterprises. Each answer explains the concept clearly, with the level of depth an interviewer expects from a mid-level or senior developer.
Questions are grouped by topic. For junior roles, focus on sections 1–3. For senior roles, everything here is fair game.
Section 1: Core Fundamentals
Q1. What is the difference between Spring and Spring Boot?
Spring Framework is a dependency injection and application framework — it provides the building blocks (IoC container, AOP, data access, web layer) but requires significant manual configuration. You have to wire beans together, configure component scanning, set up DispatcherServlet, and manage dozens of XML or Java config files.
Spring Boot is an opinionated layer on top of Spring that provides auto-configuration, starter dependencies, and an embedded server. It follows convention-over-configuration — sensible defaults are provided automatically, and you override only what you need to change.
The practical difference: a Spring Boot application starts with @SpringBootApplication and a main() method. A plain Spring web app requires a web.xml, DispatcherServlet registration, component scan config, and more.
Q2. What does @SpringBootApplication do?
It is a meta-annotation that combines three annotations:
@SpringBootApplication
// is equivalent to:
@Configuration // marks this class as a bean definition source
@EnableAutoConfiguration // enables Spring Boot auto-configuration
@ComponentScan // scans the current package and subpackages for beans
@EnableAutoConfiguration is the key one — it triggers the auto-configuration mechanism that reads META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports and conditionally activates configurations based on what is on the classpath.
Q3. How does Spring Boot auto-configuration work?
At startup, Spring Boot scans the AutoConfiguration.imports file (or spring.factories in older versions) in every JAR on the classpath. Each entry is a configuration class annotated with conditions:
@AutoConfiguration
@ConditionalOnClass(DataSource.class) // only if DataSource is on classpath
@ConditionalOnMissingBean(DataSource.class) // only if you haven't defined your own
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.datasource.url")
public DataSource dataSource() {
// creates a HikariCP datasource from application.properties
}
}
If you define your own DataSource bean, @ConditionalOnMissingBean causes the auto-config to back off entirely. This is the non-invasive principle — your explicit configuration always wins.
To see which auto-configurations are active (and which are not, and why), start your app with --debug:
java -jar app.jar --debug
# Prints a CONDITIONS EVALUATION REPORT showing every auto-config decision
Q4. What are Spring Boot starters?
Starters are curated sets of dependencies packaged as a single Maven/Gradle coordinate. When you add spring-boot-starter-web, you get Spring MVC, Jackson, an embedded Tomcat, Hibernate Validator, and the relevant auto-configuration — all in one dependency.
Without starters, you would manually add each of these libraries and figure out compatible versions. Starters solve the dependency management problem.
Common starters:
| Starter | What you get |
|---|---|
spring-boot-starter-web | Spring MVC, Tomcat, Jackson |
spring-boot-starter-data-jpa | Spring Data JPA, Hibernate, HikariCP |
spring-boot-starter-security | Spring Security |
spring-boot-starter-test | JUnit 5, Mockito, MockMvc, AssertJ |
spring-boot-starter-actuator | Health checks, metrics, management endpoints |
spring-boot-starter-cache | Spring Cache abstraction |
Q5. What is Spring Boot Actuator and which endpoints are important in production?
Actuator exposes HTTP endpoints for monitoring and managing a running Spring Boot application.
Key endpoints:
| Endpoint | Purpose |
|---|---|
/actuator/health | Application health status (UP/DOWN/OUT_OF_SERVICE) |
/actuator/health/liveness | Kubernetes liveness probe |
/actuator/health/readiness | Kubernetes readiness probe |
/actuator/metrics | Micrometer metrics |
/actuator/prometheus | Metrics in Prometheus format |
/actuator/env | Resolved application properties |
/actuator/info | Application metadata |
/actuator/loggers | View/change log levels at runtime |
Security best practice: expose only the endpoints you need, and protect sensitive ones:
management:
endpoints:
web:
exposure:
include: health, info, prometheus # never expose * in production
server:
port: 8081 # separate port so firewall can block it from public traffic
Q6. What is the configuration property precedence order in Spring Boot?
Spring Boot resolves properties in a specific priority order — higher in the list wins:
- Command-line arguments (
--server.port=9090) SPRING_APPLICATION_JSONenvironment variable- Servlet init parameters
- OS environment variables (
SERVER_PORT=9090) application-{profile}.propertiesinside JARsapplication.propertiesinside JARs@PropertySourceannotations- Default application properties
In practice: Use application.yml for defaults in the JAR. Override with environment variables in Docker/Kubernetes (12-factor app). Use command-line args for one-off overrides. Never use @PropertySource for secrets.
Section 2: Dependency Injection
Q7. Constructor injection vs field injection — which should you use and why?
Constructor injection (preferred):
@Service
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
// Spring injects these via constructor
public OrderService(PaymentService paymentService, InventoryService inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
}
Field injection (avoid):
@Service
public class OrderService {
@Autowired
private PaymentService paymentService; // avoid this
}
Reasons to prefer constructor injection:
- Dependencies are explicit — visible in the constructor signature
- Fields can be
final— ensures the bean is never in a partially-initialised state - Easier to test — just instantiate with mock dependencies, no reflection required
- Circular dependency detected at startup, not at runtime
Q8. How do you resolve a circular dependency in Spring?
A circular dependency (A depends on B, B depends on A) is usually a design problem. The right fix is to refactor — extract a third service C that both A and B depend on, breaking the cycle.
If you cannot refactor immediately:
@Service
public class ServiceA {
private ServiceB serviceB;
// Lazy injection — ServiceB not created until first call
@Autowired
public void setServiceB(@Lazy ServiceB serviceB) {
this.serviceB = serviceB;
}
}
Or use @Lazy on the constructor parameter:
public ServiceA(@Lazy ServiceB serviceB) {
this.serviceB = serviceB;
}
Spring Boot 2.6+ disallows circular dependencies by default. To disable the check (not recommended):
spring:
main:
allow-circular-references: true
Q9. What is the difference between @Primary and @Qualifier?
When multiple beans of the same type exist, Spring needs to know which to inject.
@Primary marks one bean as the default choice:
@Bean
@Primary
public DataSource primaryDataSource() { ... } // used unless explicitly overridden
@Bean
public DataSource readReplicaDataSource() { ... }
@Qualifier selects a specific bean by name at the injection point:
@Service
public class ReportService {
@Autowired
@Qualifier("readReplicaDataSource")
private DataSource dataSource; // explicitly uses the replica
}
@Primary is a default; @Qualifier is an explicit override. Use @Primary for the “normal” case; use @Qualifier when a specific injection point needs the non-default bean.
Section 3: Data Access
Q10. What is the N+1 problem in JPA and how do you fix it?
The N+1 problem occurs when loading a collection triggers one query for the parent and N additional queries for each child:
// This looks innocent but causes N+1
List<Order> orders = orderRepository.findAll(); // 1 query for orders
for (Order order : orders) {
order.getItems().size(); // N queries — one per order to load items
}
// Result: 1 + N queries for N orders
Solutions:
- JOIN FETCH in JPQL:
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
List<Order> findByStatusWithItems(@Param("status") OrderStatus status);
@EntityGraph:
@EntityGraph(attributePaths = {"items", "items.product"})
List<Order> findByCustomerId(Long customerId);
- Batch fetching (applies globally):
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 10
With batch fetching, Hibernate issues one IN (id1, id2, ..., id10) query instead of 10 individual queries.
Q11. What is @Transactional and how does propagation work?
@Transactional declares that a method should run within a transaction. If no transaction exists, one is started; if one already exists, the behaviour depends on the propagation setting.
Default propagation is REQUIRED:
@Service
public class UserService {
@Transactional // REQUIRED by default
public void registerUser(User user) {
userRepository.save(user);
emailService.sendWelcomeEmail(user); // if this has @Transactional, it joins this transaction
}
}
Key propagation values:
| Propagation | Behaviour |
|---|---|
REQUIRED (default) | Join existing transaction, or create new one |
REQUIRES_NEW | Always create a new transaction; suspend the existing one |
MANDATORY | Must join existing transaction; throws if none exists |
NOT_SUPPORTED | Suspend transaction and run without one |
NEVER | Must NOT have a transaction; throws if one exists |
NESTED | Create a savepoint within existing transaction |
Common mistake: @Transactional on a private method or calling a @Transactional method from within the same class bypasses the AOP proxy — the transaction does not start. Always call @Transactional methods from a different bean.
Q12. What causes LazyInitializationException and how do you fix it?
LazyInitializationException is thrown when you access a lazily-loaded relationship outside of an active transaction (i.e., after the JPA session has closed).
@Entity
public class Order {
@OneToMany(fetch = FetchType.LAZY) // loaded lazily by default
private List<OrderItem> items;
}
// In a controller (outside transaction):
Order order = orderService.findById(1L); // transaction opens and closes here
order.getItems().size(); // LazyInitializationException — session is closed
Fixes:
- Fetch eagerly in the query (preferred for specific use cases):
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Order findByIdWithItems(@Param("id") Long id);
- Use a DTO projection — only load what you need:
public interface OrderSummary {
Long getId();
String getStatus();
int getItemCount();
}
List<OrderSummary> findAllProjectedBy();
@Transactionalon the service method — extends the session to cover the lazy load (less preferred; can cause long transactions).
Section 4: Spring Security
Q13. Explain the Spring Security filter chain.
Spring Security works as a chain of servlet filters that sit in front of your application. Every HTTP request passes through the chain in order.
flowchart LR
Req[HTTP Request] --> CSF[CorsFilter]
CSF --> CSRF[CsrfFilter]
CSRF --> Auth[UsernamePasswordAuthenticationFilter\nor BearerTokenAuthenticationFilter]
Auth --> Authz[AuthorizationFilter]
Authz --> App[Your Controller]
Key filters:
UsernamePasswordAuthenticationFilter— processes form loginBearerTokenAuthenticationFilter— processes JWT Bearer tokensCsrfFilter— validates CSRF token on POST/PUT/DELETEAuthorizationFilter— checksauthorizeHttpRequestsrules
Each filter can short-circuit the chain (e.g., return 401) or pass to the next filter. The order of filters matters and is fixed by Spring Security.
Q14. How do you implement JWT authentication in Spring Boot?
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(jwtDecoder()))
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.csrf(csrf -> csrf.disable()); // safe for stateless JWT APIs
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// Validate with a symmetric key (HMAC)
SecretKeySpec key = new SecretKeySpec(
"my-secret-key-must-be-at-least-256-bits".getBytes(), "HmacSHA256"
);
return NimbusJwtDecoder.withSecretKey(key).build();
// Or validate with an OIDC provider's public key:
// return JwtDecoders.fromIssuerLocation("https://your-auth-server/.well-known/openid-configuration");
}
}
// Access claims in controllers
@GetMapping("/profile")
public UserProfile getProfile(@AuthenticationPrincipal Jwt jwt) {
String userId = jwt.getSubject();
List<String> roles = jwt.getClaimAsStringList("roles");
return userService.getProfile(userId);
}
Q15. What is method-level security and when do you use it?
Method-level security uses annotations to enforce access control at the service layer, not just the HTTP layer.
@Configuration
@EnableMethodSecurity // required to enable method security
public class SecurityConfig { ... }
@Service
public class ReportService {
@PreAuthorize("hasRole('ADMIN')")
public List<Report> getAllReports() { ... }
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.subject")
public Report getUserReport(Long userId) { ... }
@PostAuthorize("returnObject.ownerId == authentication.principal.subject")
public Document getDocument(Long id) { ... }
}
Use it when: the same service method is called from both web controllers and internal services (e.g., batch jobs), and you need the access check to apply regardless of the caller. HTTP-layer security alone does not protect against internal callers bypassing the web layer.
Section 5: Testing
Q16. What is the difference between @SpringBootTest, @WebMvcTest, and @DataJpaTest?
| Annotation | Context loaded | Speed | Use for |
|---|---|---|---|
@SpringBootTest | Full application context | Slow (5–15s) | Integration tests, end-to-end flows |
@WebMvcTest | Web layer only (controllers, filters) | Fast (1–2s) | Controller unit tests |
@DataJpaTest | JPA layer only (repositories, entities) | Medium (2–5s) | Repository query tests |
@RestClientTest | HTTP client layer only | Fast | Testing RestTemplate/RestClient |
// @WebMvcTest — tests only the controller, mocks the service
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean // replaces UserService with a Mockito mock
private UserService userService;
@Test
void shouldReturnUser() throws Exception {
given(userService.findById(1L)).willReturn(new User(1L, "John"));
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"));
}
}
// @DataJpaTest — tests only the repository layer with an embedded DB
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void shouldFindByEmail() {
userRepository.save(new User("john@example.com"));
Optional<User> found = userRepository.findByEmail("john@example.com");
assertThat(found).isPresent();
}
}
Q17. How do you use Testcontainers in Spring Boot?
Testcontainers spins up real Docker containers for your tests, replacing in-memory databases (H2) that can behave differently from production databases.
@SpringBootTest
@Testcontainers
class UserServiceIntegrationTest {
@Container
@ServiceConnection // Spring Boot 3.1+ — auto-wires connection properties
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired
private UserService userService;
@Test
void shouldPersistAndRetrieveUser() {
userService.create("john@example.com");
User user = userService.findByEmail("john@example.com");
assertThat(user.getEmail()).isEqualTo("john@example.com");
}
}
Performance tip: do not put @Container on instance fields — that creates a new container per test class. Put it on static fields so the container is shared across all tests in the class (or even across the entire test suite using a shared TestcontainersConfiguration).
Section 6: Microservices
Q18. What is a circuit breaker and how does Resilience4j implement it?
A circuit breaker is a pattern that stops calling a failing service to prevent cascading failures. It has three states:
stateDiagram-v2
[*] --> Closed
Closed --> Open : Failure rate exceeds threshold
Open --> HalfOpen : Wait duration elapsed
HalfOpen --> Closed : Test calls succeed
HalfOpen --> Open : Test calls fail
- Closed: calls pass through; failures are counted
- Open: calls fail immediately (no actual call made); gives the failing service time to recover
- Half-Open: a limited number of test calls are allowed; if they succeed, switches back to Closed
@Service
public class PaymentClient {
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
@Retry(name = "paymentService")
public PaymentResult processPayment(PaymentRequest request) {
return externalPaymentApi.process(request);
}
public PaymentResult paymentFallback(PaymentRequest request, Exception e) {
// Queue for later processing or return a graceful degraded response
return PaymentResult.queued("Payment queued for retry");
}
}
resilience4j:
circuitbreaker:
instances:
paymentService:
sliding-window-size: 10
failure-rate-threshold: 50 # open when 50% of last 10 calls fail
wait-duration-in-open-state: 10s
permitted-number-of-calls-in-half-open-state: 3
retry:
instances:
paymentService:
max-attempts: 3
wait-duration: 500ms
exponential-backoff-multiplier: 2
Key rule: always pair Retry with a Circuit Breaker. Retry without a circuit breaker can make a failing service worse — it multiplies the number of calls while it is already struggling.
Q19. What is the role of API Gateway in microservices?
An API Gateway is the single entry point for all client requests. It handles:
- Routing: forward
/users/**to the user service,/orders/**to the order service - Authentication: validate JWT before routing to downstream services
- Rate limiting: cap requests per client per second
- Load balancing: distribute traffic across multiple service instances
- Circuit breaking: stop routing to unavailable services
// Spring Cloud Gateway route configuration
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r
.path("/users/**")
.filters(f -> f
.requestRateLimiter(c -> c
.setRateLimiter(redisRateLimiter())
.setKeyResolver(userKeyResolver())
)
.circuitBreaker(c -> c.setName("user-cb").setFallbackUri("/fallback"))
)
.uri("lb://user-service") // lb:// = load-balanced via Eureka
)
.build();
}
}
Section 7: Performance
Q20. What are virtual threads in Spring Boot and how do you enable them?
Virtual threads (Project Loom, stable in Java 21) are lightweight threads managed by the JVM, not the OS. Traditional platform threads map 1:1 to OS threads — you can have a few thousand before memory becomes a problem. Virtual threads are cheap enough to have millions.
In Spring Boot, the practical effect: instead of a fixed thread pool handling requests (blocking on I/O while occupying a thread), each request gets its own virtual thread that is parked (not blocking an OS thread) during I/O.
Enable in Spring Boot 3.2+:
spring:
threads:
virtual:
enabled: true
That is the entire change. Tomcat switches to a virtual-thread-per-request executor automatically.
When it helps: I/O-bound services — REST APIs that call databases, external HTTP services, message queues. Under load with high I/O latency, virtual threads give you similar throughput to WebFlux with standard imperative code.
When it does not help: CPU-bound work (image processing, cryptography, heavy computation). Virtual threads do not make CPU operations faster.
Q21. What is a GraalVM native image and why does it matter for Spring Boot?
A GraalVM native image is an executable compiled ahead-of-time (AOT) from your Spring Boot application. It contains only the code your application actually uses — no JVM startup, no JIT compilation at runtime.
Results on a typical Spring Boot 3/4 application:
- JVM startup: ~1.9 seconds → Native startup: ~45 milliseconds
- Memory at rest: ~250 MB → Native memory: ~60 MB
Build a native image:
./mvnw spring-boot:build-image -Pnative
# Produces an OCI container image that starts in under 100ms
Or as an executable:
./mvnw native:compile -Pnative
./target/my-application # starts directly, no java command needed
Spring Boot 4’s modularisation reduces native image build times significantly because the AOT processor only handles the auto-configuration modules your app uses.
Trade-off: Build time increases from seconds to 10–15 minutes. Profile-guided optimisation is not available in the same way as JVM JIT. Use native images for workloads where fast startup matters: serverless functions, Kubernetes pods that scale to zero, CLI tools.
Section 8: Spring Boot 4 Specific
Q22. What is the biggest change in Spring Boot 4.0?
Modularisation. The single spring-boot-autoconfigure JAR has been split into 70+ granular modules. Each starter now pulls in only the auto-configuration modules it needs.
Impact: smaller JARs, faster startup, and significantly faster GraalVM native image builds.
Q23. What is JSpecify and what did Spring Boot 4 do with it?
JSpecify is a specification for null-safety annotations (@NonNull, @Nullable). Spring Framework 7 annotated the entire codebase with JSpecify annotations. IDEs and static analysis tools can now warn at development time when you pass a potentially null value to a method that does not accept null.
It does not change runtime behaviour — it improves the developer experience by surfacing potential NullPointerException risks at compile time.
Q24. What changed in Spring Security 7 that breaks existing applications?
Three things:
authorizeRequests()is removed — useauthorizeHttpRequests()- CSRF is enabled by default for REST APIs — explicitly disable with
csrf -> csrf.disable()for JWT-based stateless APIs antMatchers()is removed — userequestMatchers()
Q25. What is @HttpServiceClient in Spring Framework 7?
A declarative HTTP client annotation. You write an interface with @GetExchange, @PostExchange, etc. annotations; Spring generates the implementation at startup.
@HttpServiceClient(url = "${payments.api.url}")
public interface PaymentsClient {
@PostExchange("/charges")
Charge createCharge(@RequestBody ChargeRequest request);
}
No WebClient.Builder configuration, no boilerplate request code — inject the interface and call it directly.
Quick-Reference: What an Interviewer Expects
| Topic | Key concepts to know |
|---|---|
| Core | @SpringBootApplication, auto-configuration conditions, starters, configuration precedence |
| DI | Constructor vs field injection, circular deps, @Primary vs @Qualifier |
| Data | N+1 problem, @Transactional propagation, LazyInitializationException |
| Security | Filter chain, JWT resource server, method security, CSRF |
| Testing | @SpringBootTest vs slices, Testcontainers @ServiceConnection, MockMvc |
| Microservices | Circuit breaker states, API Gateway role, Resilience4j |
| Performance | Virtual threads (when to use), GraalVM (startup vs memory trade-off) |
| Boot 4 | Modularisation, JSpecify, @HttpServiceClient, Security 7 changes |
For senior roles, expect follow-up questions like: “Walk me through what happens when a Spring Boot application starts” and “How would you debug a slow query in a JPA application?” Understand the mechanisms, not just the annotations.
