36 Spring Boot Interview Questions and Answers for 2026

·31 min read
By ·Updated
spring-bootjavabackendspring-frameworkrest-apiinterview-preparation

Spring Boot reduces setup through dependency management, starters, conditional auto-configuration, executable packaging, and operational integrations. It does not make an application production-ready merely because it starts: security, schema evolution, observability, capacity, failure handling, and deployment remain design work.

That convenience comes with hidden complexity. Spring Boot does so much automatically that many developers don't understand what's happening beneath the surface. Interviewers know this. They'll ask you to explain auto-configuration, describe the bean lifecycle, or troubleshoot why your @Transactional annotation isn't working. Surface-level knowledge gets exposed quickly.

This guide covers Spring Boot 4.1.1 and Spring Framework 7.0.9 as of September 2026, from core container behavior to production operations.

Table of Contents

  1. Spring Core Fundamentals Questions
  2. Auto-Configuration Questions
  3. REST API Questions
  4. Spring Data JPA Questions
  5. Spring Security Questions
  6. Testing Questions
  7. Production Readiness Questions

Spring Core Fundamentals Questions

Before Spring Boot, there was Spring Framework. Understanding core Spring concepts is essential because Spring Boot builds directly on them.

What is Spring Boot and how does it differ from Spring Framework?

Spring Framework provides the container, dependency injection, AOP, transactions, web stacks, data-access abstractions, testing support, and related programming models. It supports annotation, Java, and XML configuration and does not inherently require an external application server.

Spring Boot builds on that foundation with opinionated dependency management, starters, conditional auto-configuration, executable packaging, externalized configuration conventions, and Actuator integration. Boot reduces setup; it does not replace Spring Framework or remove the need to design production behavior.

What is Inversion of Control (IoC) and why does Spring use it?

IoC is the foundational principle of the Spring Framework. Instead of your code controlling object creation, you invert that control to a container. The Spring IoC container creates objects, wires dependencies, and manages lifecycles. This fundamental shift in how objects are created and connected is what makes Spring applications flexible and testable.

The benefits go beyond simple "loose coupling." IoC enables testability because you can inject mock implementations for unit testing. It provides flexibility because you can swap implementations via configuration without code changes. It handles lifecycle management so the container manages creation, initialization, and destruction. And it enables AOP integration because the container can wrap beans with cross-cutting concerns.

// Without IoC: Your code controls dependencies
public class OrderService {
    private PaymentService paymentService;
 
    public OrderService() {
        // You create the dependency - tight coupling
        this.paymentService = new StripePaymentService();
    }
}
 
// With IoC: Container controls dependencies
@Service
public class OrderService {
    private final PaymentService paymentService;
 
    // Container injects the dependency
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

What are the different dependency injection types in Spring?

Spring supports three injection types, and knowing when to use each demonstrates understanding of Spring best practices. Constructor injection is the recommended approach for required dependencies because it makes them explicit and enables immutable objects. Setter injection works for optional dependencies. Field injection, while convenient, should be avoided in production code because it hides dependencies and makes testing difficult.

Constructor injection makes required dependencies explicit, supports final fields, and lets unit tests instantiate the class without a Spring context. It does not detect dependency cycles at compile time; a constructor cycle normally fails when the application context is created, which is still preferable to hiding the cycle behind field injection.

// Constructor injection (RECOMMENDED)
@Service
public class OrderService {
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
 
    // All dependencies declared, immutable, testable
    public OrderService(PaymentService paymentService,
                        InventoryService inventoryService) {
        this.paymentService = paymentService;
        this.inventoryService = inventoryService;
    }
}
 
// Setter injection (optional dependencies)
@Service
public class NotificationService {
    private EmailService emailService;
 
    @Autowired(required = false)
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
}
 
// Field injection (AVOID in production code)
@Service
public class BadExample {
    @Autowired
    private PaymentService paymentService; // Hidden dependency, hard to test
}

What is the Spring bean lifecycle?

Understanding the bean lifecycle helps you debug initialization issues and use lifecycle hooks correctly. Spring manages beans through a well-defined sequence of steps from creation to destruction. Knowing this sequence is essential for properly initializing resources and cleaning them up.

The lifecycle is an extensible pipeline rather than only the callbacks below. After instantiation and property population, relevant Aware callbacks run. BeanPostProcessor implementations surround initialization; one invokes @PostConstruct, followed by InitializingBean.afterPropertiesSet() and a custom init method. Post-processors can return proxies. On context close, eligible managed beans receive @PreDestroy, DisposableBean.destroy(), and a configured destroy method. Prototype destruction is not managed automatically.

Container starts
    ↓
Bean instantiation (constructor called)
    ↓
Dependency injection (setters, fields)
    ↓
Aware callbacks and pre-initialization BeanPostProcessors
    ↓
@PostConstruct method called
    ↓
InitializingBean.afterPropertiesSet() if implemented
    ↓
Custom init-method if specified
    ↓
Post-initialization BeanPostProcessors (may return a proxy)
    ↓
Bean is ready for use
    ↓
... application runs ...
    ↓
@PreDestroy method called
    ↓
DisposableBean.destroy() if implemented
    ↓
Custom destroy-method if specified
    ↓
Container shuts down
@Service
public class CacheService {
    private final CacheClient cacheClient;
 
    public CacheService(CacheClient cacheClient) {
        this.cacheClient = cacheClient;
        // DON'T do heavy initialization here
        // Dependencies might not be fully initialized
    }
 
    @PostConstruct
    public void initialize() {
        // Safe to use injected dependencies
        cacheClient.connect();
        cacheClient.warmUp();
    }
 
    @PreDestroy
    public void cleanup() {
        cacheClient.disconnect();
    }
}

What is the difference between @Component and @Bean?

Both annotations create Spring-managed beans, but they serve different purposes and are used in different contexts. Understanding when to use each shows you understand Spring's component model. @Component goes on your own classes that Spring should discover through component scanning. @Bean goes in configuration classes for creating beans from third-party classes or when you need complex construction logic.

Use @Bean when you can't annotate the class with @Component because it's not your code, when you need conditional bean creation with complex logic, when you need multiple beans of the same type with different configurations, or when beans require programmatic setup that doesn't belong in a constructor.

// @Component: Annotate YOUR classes
@Component
public class MyService {
    // Spring scans and registers this bean
}
 
// @Bean: Create beans from THIRD-PARTY classes or complex construction
@Configuration
public class AppConfig {
 
    @Bean
    public Clock clock() {
        // Third-party/JDK type with an application-specific choice
        return Clock.systemUTC();
    }
 
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
        // Customize Boot's builder without replacing its auto-configured modules
        return builder -> builder.featuresToDisable(
            SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
}

What is the difference between @Component, @Service, @Repository, and @Controller?

These are stereotype annotations that all derive from @Component, but they have semantic and sometimes functional differences. Using the correct annotation indicates the architectural layer and enables layer-specific features. This is a common interview question that tests your understanding of Spring's layered architecture.

@Component is the base stereotype for any Spring-managed bean. @Service marks business logic classes but has no special behavior beyond @Component. @Repository marks data access classes and enables automatic exception translation to Spring's DataAccessException hierarchy. @Controller marks web controllers and enables request mapping annotations. Use the specific annotation for clarity and to enable layer-specific features like exception translation.

@Component    // Generic Spring bean
public class GenericHelper { }
 
@Service      // Business logic layer
public class OrderService { }
 
@Repository   // Data access layer - enables exception translation
public class OrderRepository { }
 
@Controller   // Web layer - enables @RequestMapping
public class OrderController { }

Auto-Configuration Questions

Auto-configuration is a central Spring Boot feature. Understanding its conditions and back-off rules helps diagnose startup behavior without copying arbitrary configuration.

How does Spring Boot auto-configuration work?

At startup, Spring Boot imports auto-configuration candidates, evaluates their conditions, and registers definitions for matching configurations. This is why adding a starter can activate a coherent set of defaults while an application-provided bean can make a default back off.

The process works in three steps. First, Spring Boot scans META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports in all JARs on the classpath. Second, it evaluates @Conditional annotations on each auto-configuration class. Third, it registers beans for configurations where all conditions pass.

// Simplified example of what auto-configuration looks like internally
@AutoConfiguration
@ConditionalOnClass(DataSource.class)  // Only if DataSource is on classpath
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
 
    @Bean
    @ConditionalOnMissingBean  // Only if no DataSource bean exists
    public DataSource dataSource(DataSourceProperties properties) {
        return DataSourceBuilder.create()
            .url(properties.getUrl())
            .username(properties.getUsername())
            .password(properties.getPassword())
            .build();
    }
}

The @Conditional annotations are key to understanding auto-configuration:

AnnotationCondition
@ConditionalOnClassClass exists on classpath
@ConditionalOnMissingClassClass doesn't exist on classpath
@ConditionalOnBeanBean of type exists in context
@ConditionalOnMissingBeanNo bean of type exists
@ConditionalOnPropertyProperty has specific value
@ConditionalOnWebApplicationRunning as web application

What are Spring Boot starter dependencies?

Starters are curated dependency sets that bring in related libraries and trigger their auto-configuration. They're the reason Spring Boot projects are so quick to set up. Instead of manually adding dozens of dependencies and configuring them, you add one starter and Spring Boot handles the rest.

For Spring Boot 4, use spring-boot-starter-webmvc; the older spring-boot-starter-web is deprecated in its favor. The focused starter supplies the Spring MVC stack and default embedded Tomcat integration, and its classpath triggers conditional configuration such as DispatcherServlet, message conversion, error handling, and static resources.

<!-- This single dependency brings in: -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
 
<!--
- spring-webmvc (Spring MVC)
- default embedded Tomcat integration
- JSON/message-conversion support
- Boot core and logging support
 
And auto-configures:
- DispatcherServlet
- Error handling
- HTTP message converters
- Static resource handling
-->

Why might your application fail to start after adding spring-boot-starter-data-jpa?

The JPA starter activates JDBC/JPA auto-configuration. If no connection URL is configured, Boot tries to configure a supported embedded database when one is present. If it cannot determine a suitable driver, URL, or credentials, bean creation fails; this is not simply a failed @ConditionalOnProperty check.

The solution is to either provide the required database connection properties or exclude the DataSource auto-configuration if you're not using a database. Understanding this helps you diagnose similar issues with other auto-configurations.

# Provide required properties
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=user
spring.datasource.password=password

How do you customize Spring Boot auto-configuration?

Three common customization levels are properties, application-provided beans, and explicit exclusions. Properties are usually the narrowest option. A user bean changes behavior only when the relevant auto-configuration has a matching back-off condition; it is not a universal override mechanism. Excluding an auto-configuration removes that configuration and makes the application responsible for the affected infrastructure.

1. Properties (most common)

# application.properties
server.port=8081
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.jpa.hibernate.ddl-auto=validate

2. Define a bean where the auto-configuration documents a back-off condition

@Configuration
public class CustomConfig {
 
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
        // Preserve Boot's modules and customize its builder.
        return builder -> builder.featuresToDisable(
            DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    }
}

3. Exclude auto-configuration entirely

@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class,
    SecurityAutoConfiguration.class
})
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

How do you debug auto-configuration issues?

When auto-configuration doesn't work as expected, you need to understand what conditions passed or failed. Spring Boot provides a debug report that shows exactly what was auto-configured and why. This is an essential troubleshooting skill that interviewers expect you to know.

Enable the debug report temporarily with debug=true or inspect the Actuator conditions endpoint when it is deliberately exposed and access-controlled. The report shows positive matches, negative matches, unconditional classes, and exclusions; do not leave noisy debug logging or sensitive operational endpoints broadly available.

debug=true

REST API Questions

Spring Boot makes REST API development straightforward, but interviews probe deeper than basic CRUD operations.

How do you create REST controllers in Spring Boot?

REST controllers in Spring Boot use @RestController to mark a class as a web controller where every method's return value is serialized to the response body. Understanding the full range of annotations and patterns for request mapping, path variables, query parameters, and response handling demonstrates practical experience.

The key annotations are @GetMapping, @PostMapping, @PutMapping, and @DeleteMapping for HTTP methods. @PathVariable extracts values from the URL path. @RequestParam extracts query parameters. @RequestBody deserializes the request body. @Valid triggers validation on the request.

@RestController
@RequestMapping("/api/users")
public class UserController {
 
    private final UserService userService;
 
    public UserController(UserService userService) {
        this.userService = userService;
    }
 
    @GetMapping
    public List<UserDTO> getAllUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return userService.findAll(PageRequest.of(page, size));
    }
 
    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
 
    @PostMapping
    public ResponseEntity<UserDTO> createUser(
            @Valid @RequestBody CreateUserRequest request) {
        UserDTO created = userService.create(request);
        URI location = URI.create("/api/users/" + created.getId());
        return ResponseEntity.created(location).body(created);
    }
 
    @PutMapping("/{id}")
    public ResponseEntity<UserDTO> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody UpdateUserRequest request) {
        return userService.update(id, request)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
 
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        if (userService.delete(id)) {
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.notFound().build();
    }
}

What is the difference between @Controller and @RestController?

This common interview question tests whether you understand how Spring MVC handles responses. @RestController is simply @Controller combined with @ResponseBody applied to every method. With @RestController, every method's return value is automatically serialized to the response body, typically as JSON.

With plain @Controller, you would need to add @ResponseBody to each method individually, or return ModelAndView for server-side template rendering. @RestController is the standard choice for REST APIs, while @Controller is used for traditional web applications with HTML templates.

How do you validate requests in Spring Boot?

Spring Boot integrates with Jakarta Bean Validation (formerly javax.validation) to validate incoming requests. The @Valid annotation on a controller parameter triggers validation of annotated constraints. If validation fails, Spring throws MethodArgumentNotValidException, which you can handle globally.

Validation annotations go on your DTO fields. Common annotations include @NotBlank for required strings, @Email for email format, @Size for length constraints, and @Pattern for regex matching. You can also create custom validators for complex business rules.

public class CreateUserRequest {
 
    @NotBlank(message = "Email is required")
    @Email(message = "Invalid email format")
    private String email;
 
    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 100, message = "Name must be 2-100 characters")
    private String name;
 
    @NotBlank(message = "Password is required")
    @Size(min = 8, message = "Password must be at least 8 characters")
    private String password;
 
    // getters, setters
}

How do you handle exceptions globally in Spring Boot?

Production APIs benefit from stable error semantics. @ControllerAdvice can apply exception handlers across controllers, while Spring's ProblemDetail, ErrorResponse, and ResponseEntityExceptionHandler support RFC 9457. Content negotiation and errors outside MVC still require separate handling, so no single advice guarantees every failure has the same body.

Create handlers for domain-specific exceptions, map them to HTTP semantics, and avoid leaking exception messages or validation values. Broad Exception handlers can interfere with framework handling; if used, preserve correlation data in controlled logs and return a generic problem detail.

@ControllerAdvice
public class GlobalExceptionHandler {
 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ProblemDetail handleValidationErrors(
            MethodArgumentNotValidException ex) {
 
        List<Map<String, String>> fieldErrors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> Map.of(
                "field", error.getField(),
                "message", Optional.ofNullable(error.getDefaultMessage())
                    .orElse("Invalid value")))
            .toList();
 
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.BAD_REQUEST,
            "Request validation failed");
        problem.setType(URI.create("https://example.com/problems/validation"));
        problem.setProperty("errors", fieldErrors);
        return problem;
    }
 
    @ExceptionHandler(ResourceNotFoundException.class)
    public ProblemDetail handleNotFound(
            ResourceNotFoundException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.NOT_FOUND,
            "The requested resource was not found");
        problem.setType(URI.create("https://example.com/problems/not-found"));
        return problem;
    }
 
    @ExceptionHandler(Exception.class)
    public ProblemDetail handleUnexpected(Exception ex) {
        // Log the full exception for debugging
        log.error("Unexpected error", ex);
 
        // Return generic message to client (don't leak internals)
        return ProblemDetail.forStatusAndDetail(
            HttpStatus.INTERNAL_SERVER_ERROR,
            "An unexpected error occurred");
    }
}

When should you use ResponseEntity?

Knowing when to return ResponseEntity versus a plain object shows you understand HTTP semantics and REST best practices. Return the object directly when you always return 200 OK. Use ResponseEntity when the status code can vary, when you need to set headers, or when you need fine-grained control over the response.

ResponseEntity lets you set the status code, add headers like Location for created resources, and conditionally return different responses. For simple cases where the status is always 200, returning the object directly is cleaner.

// Return object directly when always 200
@GetMapping("/health")
public HealthStatus health() {
    return new HealthStatus("UP");  // Always 200
}
 
// Use ResponseEntity when status varies
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
    return userRepository.findById(id)
        .map(ResponseEntity::ok)           // 200 if found
        .orElse(ResponseEntity.notFound().build());  // 404 if not
}
 
@PostMapping
public ResponseEntity<User> create(@RequestBody User user) {
    User saved = userRepository.save(user);
    URI location = URI.create("/users/" + saved.getId());
    return ResponseEntity.created(location).body(saved);  // 201 with Location header
}

Spring Data JPA Questions

Spring Data JPA eliminates boilerplate for database operations. But understanding what happens behind the scenes is crucial for performance and debugging.

How does the Spring Data repository pattern work?

Spring Data JPA provides repository interfaces that you extend without implementing. Spring generates the implementation at runtime based on method names and annotations. This pattern eliminates boilerplate CRUD code while remaining flexible enough for complex queries.

The query derivation mechanism parses method names to generate queries. For example, findByEmail generates SELECT FROM entity WHERE email = ?. You can combine conditions with And/Or, add ordering, limit results, and more—all through naming conventions.

// Basic repository - you get CRUD for free
public interface UserRepository extends JpaRepository<User, Long> {
    // No implementation needed for basic operations
}
 
// Query methods - Spring generates queries from method names
public interface UserRepository extends JpaRepository<User, Long> {
 
    Optional<User> findByEmail(String email);
 
    List<User> findByStatus(UserStatus status);
 
    List<User> findByCreatedAtAfter(LocalDateTime date);
 
    List<User> findByNameContainingIgnoreCase(String namePart);
 
    // Combining conditions
    List<User> findByStatusAndCreatedAtAfter(
        UserStatus status,
        LocalDateTime date
    );
 
    // Limiting results
    List<User> findTop10ByOrderByCreatedAtDesc();
 
    // Counting
    long countByStatus(UserStatus status);
 
    // Existence check
    boolean existsByEmail(String email);
}

How does Spring Data generate queries from method names?

Spring Data parses the method name according to specific conventions. Understanding this parsing helps you write correct method names and debug when queries don't work as expected. The method name consists of a subject (find, count, exists, delete), an optional limit (First, Top), and a predicate (By followed by property names and operators).

For findByEmailAndStatus, Spring Data parses it as: find...By indicates a SELECT query, Email maps to the email property, And combines conditions, and Status maps to the status property. It generates: SELECT u FROM User u WHERE u.email = ?1 AND u.status = ?2.

How do you write custom queries in Spring Data JPA?

When method names get unwieldy or you need database-specific features, use @Query for explicit queries. You can write JPQL (object-oriented) or native SQL. For modifying queries (UPDATE, DELETE), add @Modifying.

JPQL queries reference entity classes and properties rather than tables and columns. Native queries use actual SQL and are useful for database-specific features or complex queries that don't translate well to JPQL.

public interface OrderRepository extends JpaRepository<Order, Long> {
 
    // JPQL query
    @Query("SELECT o FROM Order o WHERE o.user.id = :userId AND o.status = :status")
    List<Order> findUserOrdersByStatus(
        @Param("userId") Long userId,
        @Param("status") OrderStatus status
    );
 
    // Native SQL when you need database-specific features
    @Query(value = """
        SELECT * FROM orders o
        WHERE o.created_at >= NOW() - INTERVAL '30 days'
        AND o.total > :minTotal
        ORDER BY o.total DESC
        """, nativeQuery = true)
    List<Order> findRecentHighValueOrders(@Param("minTotal") BigDecimal minTotal);
 
    // Modifying queries
    @Modifying
    @Query("UPDATE Order o SET o.status = :status WHERE o.id IN :ids")
    int updateStatusForOrders(
        @Param("status") OrderStatus status,
        @Param("ids") List<Long> ids
    );
}

How does @Transactional work in Spring?

In the common proxy mode, @Transactional wraps an eligible external method invocation with a transaction interceptor. The selected transaction manager opens or joins a transaction and then commits or rolls it back according to the outcome and rollback rules. By default, unchecked exceptions and Error trigger rollback; checked exceptions do not unless configured.

The proxy boundary has important implications. A call from one method to another on this does not pass through the proxy, and private/final method details depend on proxy type and configuration. A database transaction also cannot roll back a completed HTTP call to a payment provider; use a local transaction plus an outbox/state machine and idempotent external workflow for that boundary.

@Service
public class OrderService {
 
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final OutboxRepository outboxRepository;
 
    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        // All of this happens in one transaction
        Order order = new Order(request);
 
        // Make local database changes in one transaction.
        inventoryService.reserveInDatabase(order.getItems());
        Order saved = orderRepository.save(order);
        outboxRepository.save(PaymentRequested.from(saved));
        return saved;
    }
}

Why doesn't @Transactional work when calling methods within the same class?

This is a classic interview question that tests your understanding of Spring's proxy mechanism. Spring's @Transactional uses proxies—when you call a method within the same class, you bypass the proxy and the annotation has no effect. This is a common bug that catches many developers.

@Service
public class OrderService {
 
    @Transactional
    public void processOrders(List<Long> orderIds) {
        for (Long id : orderIds) {
            // BUG: This call bypasses the proxy
            // Each order does NOT get its own transaction
            processOrder(id);
        }
    }
 
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processOrder(Long orderId) {
        // This annotation is ignored when called from within the class
    }
}

Solutions:

  1. Extract the transactional operation to a separate collaborator and call it through the proxy.
  2. Use TransactionTemplate when explicit programmatic boundaries are clearer.
  3. Use AspectJ transaction management only when its different weaving model is justified and understood.

What is the N+1 query problem and how do you solve it?

The N+1 pattern is a common JPA performance problem. It occurs when one query loads parent entities and later association access causes an additional query per parent.

The issue often appears when association access triggers one query per parent, but eager mappings and generated queries can also produce N+1 behavior. Detect it with SQL/query-count observability and tests. Fix each use case with a projection, targeted fetch join, entity graph, or batching; joining multiple collections can multiply rows, and collection fetch joins interact poorly with pagination.

// Entity with lazy relationship
@Entity
public class Order {
    @ManyToOne(fetch = FetchType.LAZY)
    private User user;
 
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> items;
}
 
// This code triggers N+1
@Transactional(readOnly = true)
public void printOrders() {
    List<Order> orders = orderRepository.findAll();  // 1 query
 
    for (Order order : orders) {
        System.out.println(order.getUser().getName());  // N queries!
    }
}

Solutions:

// JOIN FETCH in JPQL
@Query("SELECT o FROM Order o JOIN FETCH o.user WHERE o.status = :status")
List<Order> findByStatusWithUser(@Param("status") OrderStatus status);
 
// EntityGraph
@EntityGraph(attributePaths = {"user", "items"})
List<Order> findByStatus(OrderStatus status);

Spring Security Questions

Spring Security is powerful but complex. Interviews focus on understanding the architecture, not memorizing configurations.

How does the Spring Security filter chain work?

Spring Security works through a filter chain that intercepts every HTTP request. Understanding this architecture is essential for configuring security correctly and debugging issues. Each filter in the chain has a specific responsibility—some handle authentication, others handle authorization.

The application can expose one or more SecurityFilterChain beans. A FilterChainProxy selects the first chain whose security matcher applies; inside a chain, authorization rules are evaluated in declaration order. Authentication filters and authorization rules are different layers, so distinguish chain selection, filter order, and matcher order.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
            throws Exception {
 
        return http
            // Safe to disable only when authentication never relies on
            // ambient browser credentials such as cookies or HTTP Basic.
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
                .requestMatchers("/api/**").authenticated()
                .anyRequest().denyAll()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .build();
    }
}

Filter evaluation order matters:

  1. /api/auth/login matches first rule → permitted
  2. /api/admin/users matches second rule → requires ADMIN role
  3. GET /api/products matches third rule → permitted
  4. POST /api/products doesn't match third rule, falls to fourth → requires authentication

How do you implement JWT authentication in Spring Boot?

JWT is a token format, not a complete authentication design. For an API receiving OAuth 2.0 Bearer access tokens, prefer Spring Security's resource-server support over a handwritten filter. Configure the trusted issuer and intended audience; the framework obtains keys and validates the signature, iss, aud, and time claims. Pin allowed algorithms and design key rotation, authority mapping, error responses, and revocation/short lifetime according to the threat model.

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com
          audiences: https://api.example.com
@Bean
SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
    return http
        // This API accepts bearer tokens in headers and does not use
        // cookie or other ambient browser authentication.
        .csrf(csrf -> csrf.disable())
        .sessionManagement(session ->
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            .requestMatchers("/api/**").authenticated()
            .anyRequest().denyAll())
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
        .build();
}

How do you implement method-level security?

Method-level security provides fine-grained access control at the service layer rather than just URLs. This is useful when authorization logic depends on method parameters or when you want to secure business logic regardless of how it's invoked.

Enable method security with @EnableMethodSecurity, then use @PreAuthorize for checks before method execution or @PostAuthorize for checks after. SpEL expressions let you reference method parameters and the authenticated principal.

@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
}
 
@Service
public class OrderService {
 
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteOrder(Long orderId) {
        // Only admins can delete
    }
 
    @PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
    public List<Order> getOrdersForUser(Long userId) {
        // Users can only see their own orders (unless admin)
    }
 
    @PostAuthorize("returnObject.user.id == authentication.principal.id")
    public Order getOrder(Long orderId) {
        // Check after fetching - user can only see their own order
    }
}

Testing Questions

Spring Boot provides excellent testing support, but knowing which tools to use for each scenario is key.

What are Spring Boot test slices?

Test slices load only the parts of the Spring context you need, making tests faster and more focused. Instead of loading the entire application, you load just the web layer for controller tests or just the data layer for repository tests. Understanding test slices demonstrates knowledge of efficient testing strategies.

@WebMvcTest loads MVC infrastructure and selected web components; it does not automatically create mocks for your service collaborators. @DataJpaTest loads JPA-focused infrastructure and normally replaces the application data source with an available embedded one unless configured otherwise. @SpringBootTest builds the full Boot context but starts a real server only for a server web environment. Use the narrowest boundary that answers the test's question.

// @WebMvcTest - MVC slice; explicitly override collaborators
@WebMvcTest(UserController.class)
class UserControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @MockitoBean
    private UserService userService;
 
    @Test
    void shouldReturnUser() throws Exception {
        UserDTO user = new UserDTO(1L, "john@example.com", "John");
        when(userService.findById(1L)).thenReturn(Optional.of(user));
 
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.email").value("john@example.com"));
    }
 
    @Test
    void shouldReturn404WhenUserNotFound() throws Exception {
        when(userService.findById(999L)).thenReturn(Optional.empty());
 
        mockMvc.perform(get("/api/users/999"))
            .andExpect(status().isNotFound());
    }
 
    @Test
    void shouldValidateCreateRequest() throws Exception {
        String invalidRequest = """
            {
                "email": "invalid-email",
                "name": ""
            }
            """;
 
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(invalidRequest))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.errors").isArray());
    }
}
// @DataJpaTest - JPA slice; embedded DB replacement when available
@DataJpaTest
class UserRepositoryTest {
 
    @Autowired
    private UserRepository userRepository;
 
    @Autowired
    private TestEntityManager entityManager;
 
    @Test
    void shouldFindByEmail() {
        User user = new User("john@example.com", "John");
        entityManager.persistAndFlush(user);
 
        Optional<User> found = userRepository.findByEmail("john@example.com");
 
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("John");
    }
 
    @Test
    void shouldReturnEmptyForNonexistentEmail() {
        Optional<User> found = userRepository.findByEmail("nobody@example.com");
 
        assertThat(found).isEmpty();
    }
}

How do you write integration tests in Spring Boot?

Integration tests verify a chosen boundary between real components. @SpringBootTest is useful when Boot configuration and the full application context are part of that boundary, but it need not be the default for every integration test.

TestRestTemplate or WebTestClient let you make actual HTTP requests against your running application. Clean up test data after each test to ensure isolation.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderIntegrationTest {
 
    @Autowired
    private TestRestTemplate restTemplate;
 
    @Autowired
    private OrderRepository orderRepository;
 
    @Test
    void shouldCreateAndRetrieveOrder() {
        CreateOrderRequest request = new CreateOrderRequest(
            List.of(new OrderItemRequest(1L, 2))
        );
 
        ResponseEntity<OrderDTO> createResponse = restTemplate
            .postForEntity("/api/orders", request, OrderDTO.class);
 
        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
 
        Long orderId = createResponse.getBody().getId();
 
        ResponseEntity<OrderDTO> getResponse = restTemplate
            .getForEntity("/api/orders/" + orderId, OrderDTO.class);
 
        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(getResponse.getBody().getItems()).hasSize(1);
    }
 
    @AfterEach
    void cleanup() {
        orderRepository.deleteAll();
    }
}

How do you use Testcontainers in Spring Boot tests?

Testcontainers can run a real database engine or other dependency for tests. It improves fidelity for engine-specific SQL, migrations, encoding, and transaction behavior, but isolation depends on container lifecycle and test data management; it is not automatic merely because a container is used.

Use @Testcontainers and @Container for JUnit-managed containers. With the spring-boot-testcontainers test module, @ServiceConnection publishes supported connection details without manually copying URL, username, and password. @DynamicPropertySource remains useful for unsupported services or custom properties.

@SpringBootTest
@Testcontainers
class UserServiceIntegrationTest {
 
    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:18.0")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");
 
    @Autowired
    private UserService userService;
 
    @Test
    void shouldPersistUserToRealDatabase() {
        UserDTO created = userService.create(
            new CreateUserRequest(
                "test@example.com",
                "Test User",
                "Correct-Horse-Battery-Staple-7")
        );
 
        assertThat(created.getId()).isNotNull();
 
        Optional<UserDTO> retrieved = userService.findById(created.getId());
        assertThat(retrieved).isPresent();
    }
}

What is the difference between @SpringBootTest and @WebMvcTest?

This question tests your understanding of when to use different testing approaches. @SpringBootTest loads the full application context—use it for integration tests that need all components wired together. @WebMvcTest loads only the web layer—use it for fast, focused controller tests with mocked services.

@WebMvcTest is faster because it loads fewer beans and doesn't start an embedded server by default. Use it when you want to test controller logic, request/response serialization, and validation without testing service or repository logic.


Production Readiness Questions

Spring Boot includes features specifically for production deployments.

What are Spring Boot Actuator endpoints?

Spring Boot Actuator exposes operational capabilities over HTTP or JMX. Endpoint enablement, exposure, and access are separate controls. Expose only what an operator or monitoring system needs, protect it with application security and network policy, and assume endpoints such as environment, config properties, loggers, heap dumps, and thread dumps can reveal sensitive data.

Key endpoints include /actuator/health for application health status (used by load balancers), /actuator/info for application information, /actuator/metrics for application metrics, and /actuator/prometheus for Prometheus-format metrics.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# Expose specific endpoints
management.endpoints.web.exposure.include=health,info,metrics,prometheus
 
# Health endpoint details
management.endpoint.health.show-details=when_authorized
 
# Custom health indicators contribute to overall health
management.health.diskspace.enabled=true
management.health.db.enabled=true

How do you create custom health indicators?

Custom health indicators can report application-specific readiness or diagnostic state. Keep liveness independent of external dependencies: restarting every instance does not repair a payment-provider outage. Include a dependency in readiness only when this instance cannot serve useful traffic without it, and give every check a short timeout.

Implement HealthIndicator for blocking checks or ReactiveHealthIndicator for reactive checks, then assign it to an intentional health group. Sanitize details and keep show-details restricted; exception messages can leak hosts, credentials, or provider data.

@Component
@RequiredArgsConstructor
public class ExternalServiceHealthIndicator implements HealthIndicator {
 
    private final ExternalServiceClient client;
 
    @Override
    public Health health() {
        try {
            client.pingWithTimeout();
            return Health.up()
                .withDetail("service", "external-api")
                .withDetail("status", "reachable")
                .build();
        } catch (Exception e) {
            return Health.down()
                .withDetail("service", "external-api")
                .withDetail("error", "dependency-unavailable")
                .build();
        }
    }
}

How do you manage configuration in Spring Boot?

Spring Boot supports externalized configuration through config data, properties/YAML, environment variables, system properties, command-line arguments, config trees, and extension integrations. Profiles group configuration but should not become a substitute for explicit deployment environments or feature management.

Profiles can be activated with --spring.profiles.active=prod. Placeholders can include defaults such as ${PORT:8080}. Do not commit secrets or put meaningful production secret defaults in the artifact; use the platform's secret mechanism, scoped identity, rotation, and audit controls.

# application.properties (defaults)
spring.datasource.url=jdbc:h2:mem:devdb
 
# application-prod.properties
spring.datasource.url=jdbc:postgresql://prod-db:5432/app
spring.jpa.hibernate.ddl-auto=validate
 
# application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
# Reference environment variables
spring.datasource.password=${DB_PASSWORD}
 
# With default fallback
server.port=${PORT:8080}

How do you configure logging in Spring Boot?

With the default logging starter, Spring Boot uses Logback and supports level configuration, rolling policies, and structured formats. In containers, writing structured logs to stdout is often simpler than managing files inside the container; the deployment platform should own collection and retention.

Set logging levels for specific packages to control verbosity. For production, consider structured logging with JSON output for easier parsing by log aggregation systems.

# Root level
logging.level.root=INFO
 
# Package-specific levels
logging.level.com.myapp=DEBUG
logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate.SQL=DEBUG
 
# Log to file
logging.file.name=/var/log/myapp/application.log
logging.logback.rollingpolicy.max-file-size=10MB
logging.logback.rollingpolicy.max-history=30
 
# Or emit built-in structured JSON to stdout
logging.structured.format.console=logstash
@Slf4j
@Service
public class OrderService {
 
    public Order createOrder(CreateOrderRequest request) {
        log.atInfo()
            .addKeyValue("itemCount", request.getItems().size())
            .log("Creating order");
 
        // ... create order
 
        log.atInfo()
            .addKeyValue("orderId", order.getId())
            .log("Order created");
 
        return order;
    }
}

What happens when you start a Spring Boot application?

Understanding the startup sequence helps you debug initialization issues and know when to use various hooks. The sequence involves Spring Boot infrastructure, component scanning, auto-configuration, and lifecycle callbacks.

A useful simplified sequence is: SpringApplication.run() prepares the environment, chooses and creates an ApplicationContext, loads application and auto-configuration bean definitions, refreshes the context, creates non-lazy singletons and starts lifecycle components, then invokes ApplicationRunner and CommandLineRunner. Events and availability states mark stages such as started, ready, and failed. The exact order contains more extension points, and the embedded server may bind during context refresh, so route external traffic using readiness rather than assuming a runner blocks every possible connection.

How do you handle database migrations in Spring Boot?

Spring Boot can auto-configure Flyway or Liquibase when the selected tool and database support are present. Decide whether migrations run in each application instance or once as a deployment job; concurrent startup, permissions, locks, and failure recovery are operational concerns. For rolling releases, use expand-and-contract changes so both old and new application versions can operate during the rollout. Backups, restore drills, and forward fixes are still needed because not every production migration has a safe automatic rollback.

spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration

How do you implement caching in Spring Boot?

Enable Spring's cache abstraction with @EnableCaching and annotate eligible proxy-invoked methods with @Cacheable. The provider, serialization, key design, TTL/eviction, null/error policy, consistency, stampede control, and observability remain application decisions. Self-invocation normally bypasses cache advice, just as it does for proxy-mode transactions. Use @CacheEvict or @CachePut only with a clear write/invalidation ordering model.

@EnableCaching
@SpringBootApplication
public class Application { }
 
@Service
public class ProductService {
 
    @Cacheable("products")
    public Product findById(Long id) {
        // Only called on cache miss
        return repository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException(id));
    }
 
    @CacheEvict(value = "products", key = "#product.id")
    public Product update(Product product) {
        return repository.save(product);
    }
}

Sources

Frequently Asked Questions

What is Spring Boot and how does it differ from Spring Framework?

Spring Boot builds on Spring Framework and adds opinionated dependency management, auto-configuration, executable application packaging, starters, and operational integrations. Spring Framework does not inherently require XML or an external server; Boot reduces setup and supplies conditional defaults, but production readiness still requires security, persistence, observability, capacity, and deployment decisions.

How does Spring Boot auto-configuration work?

Auto-configuration uses @Conditional annotations to register beans based on the classpath, properties, application type, and existing definitions. With Spring Boot 4, spring-boot-starter-webmvc supplies the MVC stack and triggers conditional configuration for the embedded server, MVC, and JSON support. @EnableAutoConfiguration, included by @SpringBootApplication, imports candidates listed in AutoConfiguration.imports; user beans commonly make defaults back off.

What is dependency injection and why does Spring use it?

Dependency injection is a design pattern where objects receive their dependencies from external sources rather than creating them. Spring's IoC container manages object creation and wiring. Benefits include loose coupling (classes don't know how dependencies are created), easier testing (inject mocks), and flexible configuration (swap implementations without code changes).

How do you handle exceptions in Spring Boot REST APIs?

Use @ControllerAdvice with focused @ExceptionHandler methods, or extend ResponseEntityExceptionHandler for built-in MVC errors. Map domain errors to appropriate HTTP semantics and return ProblemDetail-compatible RFC 9457 responses without exposing internals. Validation can produce MethodArgumentNotValidException or method-validation errors depending on where constraints are applied.

What is the difference between @Component, @Service, @Repository, and @Controller?

@Component is the base stereotype for any Spring-managed bean. @Service, @Repository, and @Controller are specializations that indicate architectural layer. @Service marks business logic (no special behavior). @Repository marks data access and enables exception translation to Spring's DataAccessException. @Controller marks web controllers and enables request mapping. Use the specific annotation for clarity and layer-specific features.

How do you test Spring Boot applications?

Use plain unit tests for isolated logic, @WebMvcTest for the MVC slice, @DataJpaTest for JPA infrastructure, and @SpringBootTest when the full Boot context is part of the test. Override collaborators with Spring Framework's @MockitoBean when needed. Use MockMvc without a server, or TestRestTemplate/WebTestClient with the appropriate web environment. Prefer Testcontainers @ServiceConnection when database-specific behavior matters.

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides