50+ Spring Boot Interview Questions 2025: Auto-Configuration, REST APIs & Data JPA

·27 min read
spring-bootjavabackendspring-frameworkrest-apiinterview-preparation

Spring Boot changed how Java developers build applications. Before Spring Boot, setting up a Spring project meant days of XML configuration, dependency version conflicts, and server deployment hassles. Now you can have a production-ready REST API running in minutes.

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 from fundamentals to production patterns—the concepts that separate developers who use Spring Boot from developers who understand it.

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 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 is preferred because dependencies are explicit and documented in the constructor signature, objects are immutable after construction with final fields, tests can instantiate the class directly without Spring context, it prevents circular dependencies at compile time, and required dependencies are enforced without NullPointerException surprises.

// 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 begins when the container starts and instantiates the bean via its constructor. Next, dependency injection occurs through setters and fields. Then @PostConstruct methods are called, followed by InitializingBean.afterPropertiesSet() if implemented, and finally any custom init-method. The bean is then ready for use. When the application shuts down, the reverse happens: @PreDestroy methods are called, then DisposableBean.destroy(), and finally any custom destroy-method.

Container starts
    ↓
Bean instantiation (constructor called)
    ↓
Dependency injection (setters, fields)
    ↓
@PostConstruct method called
    ↓
InitializingBean.afterPropertiesSet() if implemented
    ↓
Custom init-method if specified
    ↓
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 RestTemplate restTemplate() {
        // Can't annotate RestTemplate with @Component - it's not your code
        RestTemplate template = new RestTemplate();
        template.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
        return template;
    }
 
    @Bean
    public ObjectMapper objectMapper() {
        // Complex configuration that doesn't belong in a constructor
        return new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .disable(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 Spring Boot's killer feature—and one of the most misunderstood. Understanding how it works separates competent developers from those who just copy configurations.

How does Spring Boot auto-configuration work?

Auto-configuration is the magic that makes Spring Boot "just work," and understanding it deeply impresses interviewers. When your application starts, Spring Boot scans for auto-configuration classes, evaluates conditions, and registers beans for configurations where conditions pass. This process is what allows you to add a dependency and have everything configured automatically.

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.

When you add spring-boot-starter-web, it brings in spring-webmvc, spring-boot-starter-tomcat for the embedded server, spring-boot-starter-json for Jackson, and spring-boot-starter for core functionality and logging. It also triggers auto-configuration for DispatcherServlet, error handling, HTTP message converters, and static resource handling.

<!-- This single dependency brings in: -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
 
<!--
- spring-webmvc (Spring MVC)
- spring-boot-starter-tomcat (embedded server)
- spring-boot-starter-json (Jackson)
- spring-boot-starter (core + logging)
 
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?

This is a classic troubleshooting question that tests your understanding of how auto-configuration conditions work. The JPA starter brings in DataSource auto-configuration, but auto-configuration needs connection details to succeed. Without spring.datasource.url in your properties, the @ConditionalOnProperty conditions fail.

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?

There are three ways to customize auto-configuration, from simple to advanced. Understanding all three shows you can work with Spring Boot at any level of complexity. Properties are the most common and simplest approach. Defining your own beans overrides auto-configured ones. Excluding auto-configuration entirely gives you full control.

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 your own bean (overrides auto-configured bean)

@Configuration
public class CustomConfig {
 
    @Bean
    public ObjectMapper objectMapper() {
        // Auto-configuration sees this bean exists
        // and skips its own ObjectMapper creation
        return new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }
}

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 by setting debug=true in your properties. This prints a conditions evaluation report showing positive matches (what was auto-configured and why), negative matches (what wasn't configured and why), and exclusions (what you explicitly excluded).

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")
    @Pattern(regexp = ".*[A-Z].*", message = "Password must contain uppercase letter")
    private String password;
 
    // getters, setters
}

How do you handle exceptions globally in Spring Boot?

Production APIs need consistent error responses regardless of where exceptions occur. @ControllerAdvice creates a global exception handler that intercepts exceptions from all controllers. This centralizes error handling and ensures clients always receive the same error format.

Create handler methods annotated with @ExceptionHandler for specific exception types. Map business exceptions to appropriate HTTP status codes and return structured error response DTOs. Always handle unexpected exceptions with a generic handler that logs the full stack trace but returns a safe message to clients.

@ControllerAdvice
public class GlobalExceptionHandler {
 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationErrors(
            MethodArgumentNotValidException ex) {
 
        List<FieldError> fieldErrors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> new FieldError(
                error.getField(),
                error.getDefaultMessage()))
            .toList();
 
        ErrorResponse response = new ErrorResponse(
            "VALIDATION_ERROR",
            "Request validation failed",
            fieldErrors
        );
 
        return ResponseEntity.badRequest().body(response);
    }
 
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(
            ResourceNotFoundException ex) {
 
        ErrorResponse response = new ErrorResponse(
            "NOT_FOUND",
            ex.getMessage(),
            null
        );
 
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
    }
 
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleUnexpected(Exception ex) {
        // Log the full exception for debugging
        log.error("Unexpected error", ex);
 
        // Return generic message to client (don't leak internals)
        ErrorResponse response = new ErrorResponse(
            "INTERNAL_ERROR",
            "An unexpected error occurred",
            null
        );
 
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(response);
    }
}

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?

@Transactional is deceptively simple but frequently misunderstood. It uses proxies to wrap method calls with transaction management—begin transaction before the method, commit on success, rollback on exception. Understanding its limitations is crucial for avoiding subtle bugs.

The proxy-based implementation has important implications. When you call a @Transactional method within the same class, you bypass the proxy and the annotation has no effect. This is one of the most common Spring bugs.

@Service
public class OrderService {
 
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
 
    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        // All of this happens in one transaction
        Order order = new Order(request);
 
        // Reserve inventory
        inventoryService.reserve(order.getItems());
 
        // Process payment
        paymentService.charge(order.getTotal());
 
        // Save order
        return orderRepository.save(order);
 
        // If anything throws, everything rolls back
    }
}

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. Inject the service into itself (ugly but works)
  2. Extract to a separate service class (cleaner)
  3. Use TransactionTemplate for programmatic control

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

The N+1 problem is the most common JPA performance issue. It occurs when you load a collection of entities (1 query), then access a lazy relationship on each entity (N additional queries). Understanding and solving this problem is essential for production applications.

The issue happens because JPA's lazy loading fetches related entities one at a time as you access them. Instead of fetching all data in one query, you end up with N+1 database round trips.

// 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 filter chain is configured through the SecurityFilterChain bean. Filters execute in order, and for authorization rules, the first matching rule wins. This ordering is crucial when you have overlapping patterns.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
            throws Exception {
 
        return http
            .csrf(csrf -> csrf.disable())  // Disable for REST APIs with JWT
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
                .requestMatchers("/api/**").authenticated()
                .anyRequest().permitAll()
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .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?

Modern REST APIs typically use JWT for stateless authentication. The implementation involves a filter that extracts the token from the Authorization header, validates it, and populates the SecurityContext. Understanding this flow demonstrates practical security experience.

The JWT filter runs before the standard authentication filter, checking for a Bearer token in every request. If valid, it creates an authentication object and sets it in the SecurityContext so subsequent filters and your application code can access the authenticated user.

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
 
    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;
 
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
 
        String authHeader = request.getHeader("Authorization");
 
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
 
        String token = authHeader.substring(7);
        String username = jwtService.extractUsername(token);
 
        if (username != null &&
            SecurityContextHolder.getContext().getAuthentication() == null) {
 
            UserDetails userDetails = userDetailsService
                .loadUserByUsername(username);
 
            if (jwtService.isTokenValid(token, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                    );
 
                authToken.setDetails(
                    new WebAuthenticationDetailsSource()
                        .buildDetails(request)
                );
 
                SecurityContextHolder.getContext()
                    .setAuthentication(authToken);
            }
        }
 
        filterChain.doFilter(request, response);
    }
}

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 only controllers and web-related beans, mocking the service layer. @DataJpaTest loads only JPA components with an embedded database. @SpringBootTest loads the full context for integration tests. Use the narrowest slice that covers what you need to test.

// @WebMvcTest - Controllers only, mocks services
@WebMvcTest(UserController.class)
class UserControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @MockBean
    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 - Repositories only, embedded database
@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 that multiple components work together correctly. Use @SpringBootTest to load the full application context and test real interactions between layers. These tests are slower but catch integration issues that unit tests miss.

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 lets you run real databases and other services in Docker containers during tests. This provides more realistic testing than embedded databases while maintaining test isolation. Each test class gets its own container instance.

Use the @Testcontainers annotation and @Container for your container definitions. @DynamicPropertySource injects the container's connection details into Spring's environment so your application connects to the test container.

@SpringBootTest
@Testcontainers
class UserServiceIntegrationTest {
 
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");
 
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
 
    @Autowired
    private UserService userService;
 
    @Test
    void shouldPersistUserToRealDatabase() {
        UserDTO created = userService.create(
            new CreateUserRequest("test@example.com", "Test User", "password123")
        );
 
        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 provides operational information about your running application through HTTP endpoints. These endpoints are essential for monitoring, health checks, and debugging production issues. Understanding Actuator shows you know how to operate Spring Boot applications, not just develop them.

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 let you include application-specific health checks in the /actuator/health endpoint. This is useful for checking connections to external services, verifying critical resources are available, or any condition that affects your application's ability to serve requests.

Implement the HealthIndicator interface and return Health.up() or Health.down() with relevant details. Spring aggregates all health indicators into the overall health status.

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

How do you manage configuration in Spring Boot?

Spring Boot supports externalized configuration through properties files, YAML, environment variables, and command-line arguments. Profiles let you define environment-specific configuration. Understanding configuration management is essential for deploying applications across different environments.

Profiles are activated with --spring.profiles.active=prod. Environment variables can be referenced with ${VAR_NAME} syntax and can include defaults with ${VAR_NAME:default}. Never commit sensitive values like passwords to source control.

# 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?

Spring Boot uses Logback by default and provides sensible logging configuration out of the box. You can customize log levels per package, configure file output with rotation, and use structured logging for production systems that aggregate logs.

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.file.max-size=10MB
logging.file.max-history=30
@Slf4j
@Service
public class OrderService {
 
    public Order createOrder(CreateOrderRequest request) {
        log.info("Creating order",
            kv("userId", request.getUserId()),
            kv("itemCount", request.getItems().size()));
 
        // ... create order
 
        log.info("Order created",
            kv("orderId", order.getId()),
            kv("total", order.getTotal()));
 
        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.

The process is: main() calls SpringApplication.run(), Spring creates the ApplicationContext, component scanning finds @Component classes, auto-configuration runs based on classpath and conditions, beans are instantiated and dependencies injected, @PostConstruct methods execute, ApplicationRunner and CommandLineRunner beans run, and finally the application is ready to handle requests.

How do you handle database migrations in Spring Boot?

Use Flyway or Liquibase for database migrations. Spring Boot auto-configures them when they're on the classpath. Migrations run automatically at startup, applying any pending changes to your database schema in a versioned, repeatable way.

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

How do you implement caching in Spring Boot?

Enable caching with @EnableCaching and annotate methods with @Cacheable. Spring caches the return value keyed by method parameters. Use @CacheEvict to remove entries when data changes.

@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);
    }
 
    @CacheEvict(value = "products", key = "#product.id")
    public Product update(Product product) {
        return repository.save(product);
    }
}

Ready to ace your interview?

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

View PDF Guides