Spring BootJavaJPASpring SecurityMicroservices

Spring Boot Interview — The Complete Guide

Everything you need for Spring Boot rounds at product companies. Core internals, auto-configuration, JPA deep dive, security, testing strategies, microservices patterns, and the questions that actually get asked.

150 min read13 sections
01

Spring Core & IoC Container

Spring's foundation is the IoC (Inversion of Control) container. Instead of your code creating dependencies with new, the container creates and wires them for you. Understanding this deeply is the difference between using Spring and understanding Spring.

🔥 Inversion of Control — The Core Idea

Traditional code: OrderService creates its own OrderRepository. With IoC: the container creates both and injects the repository into the service. The "inversion" is that the framework controls object creation, not your code. This enables loose coupling, testability (inject mocks), and lifecycle management.

IoC.javajava
// ❌ Without IoC — tight coupling
public class OrderService {
    private final OrderRepository repo = new OrderRepository(); // creates its own dependency
}

// ✅ With IoC — container injects the dependency
@Service
public class OrderService {
    private final OrderRepository repo;

    public OrderService(OrderRepository repo) {
        this.repo = repo; // injected by Spring
    }
}

🔥 ApplicationContext vs BeanFactory

AspectBeanFactoryApplicationContext
LoadingLazy — creates beans on first requestEager — creates all singleton beans at startup
FeaturesBasic DI onlyDI + events, i18n, AOP, resource loading
Use caseMemory-constrained environments (rare)99% of applications — always use this
StartupFaster (lazy)Slower but catches config errors early

💡 Interview answer

"ApplicationContext is the superset. It extends BeanFactory and adds enterprise features like event publishing, internationalization, and AOP support. In practice, you always use ApplicationContext — BeanFactory is only relevant for extremely resource-constrained environments."

🔥 @Component vs @Bean — Must Know

Both register beans in the container, but they work differently. @Component (and its specializations) is a class-level annotation — Spring discovers it via component scanning. @Bean is a method-level annotation inside a @Configuration class — you manually define the bean creation logic. Use @Bean when you need to configure third-party classes you can't annotate.

ComponentVsBean.javajava
// @Component — Spring discovers and creates this automatically
@Service // specialization of @Component
public class UserService {
    // ...
}

// @Bean — you control instantiation (useful for third-party classes)
@Configuration
public class AppConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplateBuilder()
            .setConnectTimeout(Duration.ofSeconds(5))
            .setReadTimeout(Duration.ofSeconds(10))
            .build();
    }
}

Stereotype Annotations

AnnotationLayerExtra behavior
@ComponentGenericBase annotation. No extra behavior.
@ServiceBusiness logicSemantic only — no extra behavior over @Component.
@RepositoryData accessTranslates persistence exceptions to Spring's DataAccessException.
@ControllerWeb layerEnables request mapping. Returns views.
@RestControllerWeb layer (API)@Controller + @ResponseBody. Returns JSON/XML directly.

🔥 Bean Lifecycle

Understanding the bean lifecycle is critical for debugging initialization issues and managing resources. The full sequence:

  1. Instantiation — constructor called
  2. Populate properties — dependencies injected
  3. @PostConstruct — initialization logic runs
  4. InitializingBean.afterPropertiesSet() — if implemented
  5. Custom init method — if specified in @Bean(initMethod)
  6. Bean is ready to use
  7. @PreDestroy — cleanup logic runs (on shutdown)
  8. DisposableBean.destroy() — if implemented
BeanLifecycle.javajava
@Component
public class CacheWarmer {

    @PostConstruct
    public void warmCache() {
        // Runs after DI is complete — load frequently accessed data
        log.info("Warming cache...");
        cacheService.loadHotData();
    }

    @PreDestroy
    public void cleanup() {
        // Runs on application shutdown — release resources
        log.info("Flushing cache to disk...");
        cacheService.flush();
    }
}

Spring Profiles

Profiles let you swap beans and configuration per environment. Activate with spring.profiles.active=dev in properties or -Dspring.profiles.active=prod as a JVM argument. Use application-dev.yml and application-prod.yml for environment-specific config.

Profiles.javajava
// Different implementations per environment
@Profile("dev")
@Service
public class MockPaymentGateway implements PaymentGateway {
    public PaymentResult charge(Money amount) {
        return PaymentResult.success(); // always succeeds in dev
    }
}

@Profile("prod")
@Service
public class StripePaymentGateway implements PaymentGateway {
    public PaymentResult charge(Money amount) {
        return stripeClient.charge(amount); // real payment in prod
    }
}

📝 Quick Revision

Quick Revision Cheat Sheet

IoC: Container creates and wires objects. You don't use 'new' for managed beans.

ApplicationContext: Use this always. Eager loading, events, AOP, i18n. Superset of BeanFactory.

@Component vs @Bean: @Component = class-level, auto-scanned. @Bean = method-level, manual config.

Stereotypes: @Service (logic), @Repository (data + exception translation), @Controller (web).

Lifecycle: Constructor → DI → @PostConstruct → ready → @PreDestroy → destroyed.

Profiles: Swap beans per env. application-{profile}.yml for config.

Common Interview Questions

Q:What is Inversion of Control and how does Spring implement it?

A: IoC means the framework controls object creation and wiring, not your code. Spring implements it through the ApplicationContext container — you declare beans (via annotations or @Bean methods), and Spring creates them, resolves dependencies, and manages their lifecycle. The key benefit: loose coupling and testability.

Q:What's the difference between @Component, @Service, @Repository, and @Controller?

A: All are stereotype annotations that register beans. @Component is the base. @Service is semantic (no extra behavior). @Repository adds persistence exception translation. @Controller enables request mapping for MVC. @RestController adds @ResponseBody for API responses. Use the right one for the right layer.

02

Auto-Configuration

Auto-configuration is what makes Spring Boot "opinionated." Add a dependency to your classpath, and Spring Boot automatically configures the beans you need. No XML, no manual setup. Understanding how this works internally separates Boot users from Boot experts.

🔥 How Auto-Configuration Works Internally

When you annotate your main class with @SpringBootApplication, it triggers three things: (1) @Configuration — marks it as a bean source, (2) @EnableAutoConfiguration — loads auto-config classes, (3) @ComponentScan — scans the package tree for beans.

The auto-config classes are listed in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (Spring Boot 3.x) or META-INF/spring.factories (2.x). Spring loads all of them, but each class uses @Conditional annotations to decide whether to activate.

🔥 @Conditional Annotations — The Decision Engine

AnnotationActivates when...Example
@ConditionalOnClassA class is on the classpathDataSource.class → configure DB
@ConditionalOnMissingBeanNo bean of that type existsDon't override user's custom bean
@ConditionalOnPropertyA property has a specific valuespring.cache.type=redis → configure Redis cache
@ConditionalOnBeanA specific bean already existsConfigure JPA only if DataSource bean exists
@ConditionalOnMissingClassA class is NOT on the classpathFallback configuration
@ConditionalOnWebApplicationRunning as a web appConfigure DispatcherServlet
AutoConfigExample.javajava
// How Spring Boot auto-configures a DataSource
@AutoConfiguration
@ConditionalOnClass(DataSource.class)                    // only if JDBC is on classpath
@ConditionalOnProperty(name = "spring.datasource.url")   // only if URL is configured
public class DataSourceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean                             // only if user hasn't defined their own
    public DataSource dataSource(DataSourceProperties props) {
        return DataSourceBuilder.create()
            .url(props.getUrl())
            .username(props.getUsername())
            .password(props.getPassword())
            .build();
    }
}

Key insight

@ConditionalOnMissingBean is the escape hatch. If you define your own DataSource bean, Spring Boot's auto-configured one backs off. This is how you override defaults without fighting the framework.

Starter Dependencies

Starters are curated dependency bundles. spring-boot-starter-web pulls in Tomcat, Spring MVC, Jackson, and validation. spring-boot-starter-data-jpa pulls in Hibernate, Spring Data JPA, and HikariCP. You don't pick individual libraries — the starter gives you a tested, compatible set.

application.yml vs application.properties

Aspectapplication.propertiesapplication.yml
FormatKey=value pairsYAML hierarchy
ReadabilityFlat — repetitive prefixesNested — cleaner for deep config
Multi-profileSeparate files per profileCan use --- separator in one file
PreferenceSimpler for small configsBetter for complex, nested configs

Debugging Auto-Configuration

Set debug=true in application.properties to see the auto-configuration report at startup. It shows which configs were applied (positive matches) and which were skipped (negative matches) with the reason. This is your first tool when something isn't being configured as expected.

📝 Quick Revision

Quick Revision Cheat Sheet

@SpringBootApplication: @Configuration + @EnableAutoConfiguration + @ComponentScan.

Auto-config: Reads classpath + properties. Uses @Conditional to decide what to configure.

@ConditionalOnMissingBean: The escape hatch. Define your own bean to override auto-config.

Starters: Curated dependency bundles. Tested compatible versions.

Debug: debug=true shows which auto-configs matched and why.

03

Dependency Injection Deep Dive

DI is the mechanism that implements IoC. Spring supports three injection styles, but only one is recommended. Interviewers test whether you know the trade-offs and can handle edge cases like circular dependencies and ambiguous beans.

🔥 Constructor vs Field vs Setter Injection

StyleHowProsCons
ConstructorDependencies as constructor paramsImmutable (final), testable, fails fast, explicitVerbose with many deps (but that's a design smell)
Field (@Autowired)@Autowired on private fieldLess boilerplateCan't be final, hidden deps, hard to test, reflection-based
Setter@Autowired on setter methodOptional deps, reconfigurableMutable, easy to forget to call setter

🔥 The official recommendation

Spring team recommends constructor injection. Since Spring 4.3, if a class has only one constructor, @Autowired is optional — Spring auto-injects. This is the cleanest pattern.

InjectionStyles.javajava
// ✅ Constructor injection (preferred)
@Service
public class OrderService {
    private final OrderRepository repo;
    private final NotificationService notifications;

    // @Autowired is optional with single constructor (Spring 4.3+)
    public OrderService(OrderRepository repo, NotificationService notifications) {
        this.repo = repo;
        this.notifications = notifications;
    }
}

// ❌ Field injection (avoid)
@Service
public class OrderService {
    @Autowired private OrderRepository repo;           // not final, hidden dependency
    @Autowired private NotificationService notifications; // can't easily mock in tests
}

🔥 @Qualifier and @Primary — Resolving Ambiguity

When multiple beans implement the same interface, Spring doesn't know which to inject. @Primary marks a default choice. @Qualifier lets you pick a specific one by name. @Qualifier overrides @Primary.

QualifierPrimary.javajava
public interface NotificationService {
    void send(String message);
}

@Service
@Primary // default when no qualifier specified
public class EmailNotificationService implements NotificationService { ... }

@Service("smsNotifier")
public class SmsNotificationService implements NotificationService { ... }

// Uses EmailNotificationService (the @Primary)
@Service
public class OrderService {
    public OrderService(NotificationService notifications) { ... }
}

// Uses SmsNotificationService (explicit @Qualifier)
@Service
public class AlertService {
    public AlertService(@Qualifier("smsNotifier") NotificationService notifications) { ... }
}

🔥 Bean Scopes

ScopeInstancesWhen to use
singleton (default)One per ApplicationContextStateless services, repositories, configs — 95% of beans
prototypeNew instance per injection/requestStateful beans, builders, non-thread-safe objects
requestOne per HTTP requestRequest-scoped data (user context, request metadata)
sessionOne per HTTP sessionUser session data (shopping cart in traditional web apps)
applicationOne per ServletContextShared across all sessions — similar to singleton

💡 Gotcha: Injecting prototype into singleton

If you inject a prototype-scoped bean into a singleton, the prototype is created once and reused — defeating the purpose. Fix: inject ObjectProvider<T> or Provider<T> and call .getObject() each time you need a new instance.

🔥 Circular Dependencies

A depends on B, B depends on A. With constructor injection, Spring fails fast at startup — it can't create either bean. This is a good thing — it forces you to fix the design. Solutions:

  • Redesign — extract shared logic into a third service
  • Use @Lazy on one dependency — creates a proxy, breaks the cycle
  • Use setter injection on one side (not recommended — hides the problem)
  • Use events — A publishes an event, B listens (decoupled)
CircularDep.javajava
// ❌ Circular dependency — fails at startup with constructor injection
@Service
public class ServiceA {
    public ServiceA(ServiceB b) { }
}
@Service
public class ServiceB {
    public ServiceB(ServiceA a) { }
}

// ✅ Fix 1: @Lazy — creates a proxy, resolves lazily
@Service
public class ServiceA {
    public ServiceA(@Lazy ServiceB b) { }
}

// ✅ Fix 2 (better): Redesign — extract shared logic
@Service
public class SharedLogic { }

@Service
public class ServiceA {
    public ServiceA(SharedLogic shared) { }
}
@Service
public class ServiceB {
    public ServiceB(SharedLogic shared) { }
}

📝 Quick Revision

Quick Revision Cheat Sheet

Constructor injection: Preferred. Immutable, testable, fails fast. @Autowired optional with single constructor.

@Primary: Default bean when multiple candidates. @Qualifier overrides it.

Singleton scope: Default. One instance per container. Use for stateless services.

Prototype scope: New instance per injection. Use ObjectProvider to get fresh instances in singletons.

Circular deps: Redesign first. @Lazy as escape hatch. Events for decoupling.

04

REST API Design with Spring

Spring MVC is the web layer of Spring Boot. Every HTTP request flows through the DispatcherServlet, which routes it to the right controller method. Understanding this flow and the annotations that control it is essential.

🔥 Request Handling Flow

Client → Filter chain (servlet-level: CORS, logging, security) → DispatcherServlet HandlerMapping (finds the controller method) → HandlerAdapter (invokes it) → Controller → response serialized by HttpMessageConverter (Jackson for JSON). Interceptors run between DispatcherServlet and the controller.

@RestController vs @Controller

AnnotationReturnsUse case
@ControllerView name (resolved by ViewResolver)Server-rendered HTML (Thymeleaf, JSP)
@RestControllerObject → serialized to JSON/XMLREST APIs — 99% of modern Spring Boot apps

@RestController = @Controller + @ResponseBody. Every method's return value is automatically serialized to JSON by Jackson.

🔥 Request Mapping Annotations

RestController.javajava
@RestController
@RequestMapping("/api/v1/users")
public class UserController {

    @GetMapping                          // GET /api/v1/users
    public List<UserDto> listUsers(@RequestParam(defaultValue = "0") int page,
                                   @RequestParam(defaultValue = "20") int size) {
        return userService.findAll(page, size);
    }

    @GetMapping("/{id}")                 // GET /api/v1/users/123
    public UserDto getUser(@PathVariable Long id) {
        return userService.findById(id);
    }

    @PostMapping                         // POST /api/v1/users
    @ResponseStatus(HttpStatus.CREATED)
    public UserDto createUser(@Valid @RequestBody CreateUserRequest request) {
        return userService.create(request);
    }

    @PutMapping("/{id}")                 // PUT /api/v1/users/123
    public UserDto updateUser(@PathVariable Long id,
                              @Valid @RequestBody UpdateUserRequest request) {
        return userService.update(id, request);
    }

    @DeleteMapping("/{id}")              // DELETE /api/v1/users/123
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);
    }
}

@PathVariable vs @RequestParam vs @RequestBody

AnnotationSourceExample
@PathVariableURL path segment/users/{id} → @PathVariable Long id
@RequestParamQuery string/users?page=2 → @RequestParam int page
@RequestBodyRequest body (JSON)POST body → @RequestBody CreateUserRequest req
@RequestHeaderHTTP headerAuthorization header → @RequestHeader String auth

🔥 Bean Validation (@Valid)

Spring integrates with Jakarta Bean Validation. Annotate DTO fields with constraints, add @Valid on the controller parameter, and Spring automatically validates before the method executes. Invalid input throws MethodArgumentNotValidException (400).

Validation.javajava
public record CreateUserRequest(
    @NotBlank(message = "Name is required")
    String name,

    @Email(message = "Must be a valid email")
    @NotBlank
    String email,

    @Size(min = 8, max = 100, message = "Password must be 8-100 characters")
    String password,

    @Min(value = 18, message = "Must be at least 18")
    Integer age
) {}

// Custom validator
@Constraint(validatedBy = UniqueEmailValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueEmail {
    String message() default "Email already exists";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Content Negotiation

Spring uses HttpMessageConverter to serialize/deserialize request and response bodies. Jackson (MappingJackson2HttpMessageConverter) handles JSON by default. You can support XML by adding jackson-dataformat-xml to the classpath. The Accept header determines the response format.

📝 Quick Revision

Quick Revision Cheat Sheet

Request flow: Filter → DispatcherServlet → HandlerMapping → Controller → MessageConverter → Response.

@RestController: @Controller + @ResponseBody. Returns JSON directly.

@Valid: Triggers Bean Validation on @RequestBody. Throws 400 on failure.

@PathVariable: From URL path. @RequestParam from query string. @RequestBody from JSON body.

ResponseEntity: Full control over status code, headers, and body. Use for custom responses.

05

Exception Handling

Clean error handling separates production APIs from tutorial code. Spring provides a layered approach: controller-level, global, and framework-level exception handling.

🔥 @ControllerAdvice + @ExceptionHandler

@RestControllerAdvice is a global exception handler that catches exceptions thrown by any controller. Each @ExceptionHandler method handles a specific exception type. This centralizes error handling — no try-catch in controllers.

GlobalExceptionHandler.javajava
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        var error = new ErrorResponse("NOT_FOUND", ex.getMessage());
        return ResponseEntity.status(404).body(error);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        List<FieldError> errors = ex.getBindingResult().getFieldErrors().stream()
            .map(f -> new FieldError(f.getField(), f.getDefaultMessage()))
            .toList();
        var error = new ErrorResponse("VALIDATION_ERROR", "Invalid input", errors);
        return ResponseEntity.badRequest().body(error);
    }

    @ExceptionHandler(DataIntegrityViolationException.class)
    public ResponseEntity<ErrorResponse> handleConflict(DataIntegrityViolationException ex) {
        var error = new ErrorResponse("CONFLICT", "Resource already exists");
        return ResponseEntity.status(409).body(error);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
        log.error("Unhandled exception", ex);
        var error = new ErrorResponse("INTERNAL_ERROR", "Something went wrong");
        return ResponseEntity.internalServerError().body(error);
    }
}

Consistent Error Response Shape

ErrorResponse.javajava
// Always return the same shape — clients can parse predictably
public record ErrorResponse(
    String code,
    String message,
    List<FieldError> details,  // null for non-validation errors
    Instant timestamp,
    String requestId
) {
    public ErrorResponse(String code, String message) {
        this(code, message, null, Instant.now(), MDC.get("requestId"));
    }
    public ErrorResponse(String code, String message, List<FieldError> details) {
        this(code, message, details, Instant.now(), MDC.get("requestId"));
    }
}

public record FieldError(String field, String message) {}

Problem Detail (RFC 7807)

Spring Boot 3 supports RFC 7807 Problem Detail natively. Instead of custom error shapes, you can return standardized problem responses with type, title, status, detail, and instance fields. Enable with spring.mvc.problemdetails.enabled=true.

ResponseStatusException

For simple cases, throw ResponseStatusException directly from service or controller code without needing a custom exception class. Useful for quick prototyping, but @ControllerAdvice is better for production — it centralizes handling and keeps controllers clean.

📝 Quick Revision

Quick Revision Cheat Sheet

@RestControllerAdvice: Global exception handler. Catches exceptions from all controllers.

@ExceptionHandler: Maps exception type to handler method. Most specific match wins.

Error shape: Consistent: code, message, details, timestamp, requestId.

RFC 7807: Standard problem detail format. Native in Spring Boot 3.

Order: Specific handlers first, generic Exception.class as fallback.

06

Data Access — Spring Data JPA

🔥 This is the most interview-heavy section

JPA/Hibernate questions dominate Spring Boot interviews. N+1 queries, lazy loading, transactions, and entity relationships are asked in almost every round. Know the pitfalls, not just the happy path.

Repository Pattern

InterfaceMethodsWhen to use
CrudRepositorysave, findById, findAll, delete, countBasic CRUD — sufficient for most cases
JpaRepositoryCrudRepository + flush, saveAll, batch ops, PageableWhen you need pagination, sorting, or batch operations
PagingAndSortingRepositoryCrudRepository + findAll(Pageable), findAll(Sort)Explicit paging/sorting without full JPA features

🔥 Query Methods — How Spring Generates SQL

Spring Data JPA parses method names and generates queries automatically. The method name is the query. This is one of the most magical (and most asked-about) features.

QueryMethods.javajava
public interface UserRepository extends JpaRepository<User, Long> {

    // Spring generates: SELECT * FROM users WHERE email = ?
    Optional<User> findByEmail(String email);

    // SELECT * FROM users WHERE status = ? AND age > ?
    List<User> findByStatusAndAgeGreaterThan(String status, int age);

    // SELECT * FROM users WHERE name LIKE '%keyword%' ORDER BY created_at DESC
    List<User> findByNameContainingOrderByCreatedAtDesc(String keyword);

    // SELECT COUNT(*) FROM users WHERE status = ?
    long countByStatus(String status);

    // SELECT * FROM users WHERE email = ? (returns boolean)
    boolean existsByEmail(String email);

    // Custom JPQL when method names get too long
    @Query("SELECT u FROM User u WHERE u.department.name = :dept AND u.active = true")
    List<User> findActiveByDepartment(@Param("dept") String department);

    // Native SQL for complex queries
    @Query(value = "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '30 days'",
           nativeQuery = true)
    List<User> findRecentUsers();
}

🔥 Entity Relationships

RelationshipAnnotationDefault fetchExample
One-to-One@OneToOneEAGERUser → Profile
Many-to-One@ManyToOneEAGEROrder → User
One-to-Many@OneToManyLAZYUser → Orders
Many-to-Many@ManyToManyLAZYStudent ↔ Course

💡 Best practice

Always set fetch = FetchType.LAZY on all relationships. Eager fetching loads related entities even when you don't need them — this cascades and can load your entire database into memory. Use JOIN FETCH or @EntityGraph when you explicitly need the related data.

🔥 N+1 Query Problem — MUST KNOW

Fetch 100 orders. Each has a lazy user. Accessing order.getUser() for each fires 100 separate queries. Total: 1 (orders) + 100 (users) = 101 queries. This is the #1 performance killer in JPA applications.

N1Problem.javajava
// ❌ N+1 — 101 queries for 100 orders
List<Order> orders = orderRepo.findAll();
orders.forEach(o -> log.info(o.getUser().getName())); // 100 extra SELECTs!

// ✅ Fix 1: JOIN FETCH in JPQL — single query
@Query("SELECT o FROM Order o JOIN FETCH o.user")
List<Order> findAllWithUsers();

// ✅ Fix 2: @EntityGraph — declarative
@EntityGraph(attributePaths = {"user", "items"})
List<Order> findAll();

// ✅ Fix 3: @BatchSize — Hibernate batches lazy loads
@Entity
public class Order {
    @ManyToOne(fetch = FetchType.LAZY)
    @BatchSize(size = 50) // loads users in batches of 50
    private User user;
}

Pagination & Sorting

Pagination.javajava
// Repository — just add Pageable parameter
Page<User> findByStatus(String status, Pageable pageable);

// Service
public Page<UserDto> getActiveUsers(int page, int size) {
    Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
    return userRepo.findByStatus("ACTIVE", pageable)
        .map(this::toDto); // map entity to DTO
}

// Controller
@GetMapping
public Page<UserDto> listUsers(@RequestParam(defaultValue = "0") int page,
                                @RequestParam(defaultValue = "20") int size) {
    return userService.getActiveUsers(page, size);
}
// Response includes: content, totalElements, totalPages, number, size

🔥 @Transactional Deep Dive

@Transactional wraps the method in a database transaction using a proxy. Three critical gotchas that interviewers love:

  1. Public methods only — the proxy can't intercept private/protected methods
  2. Self-invocation bypasses proxy — calling another @Transactional method from the same class goes through this, not the proxy
  3. Checked exceptions don't rollback — only unchecked (RuntimeException) trigger rollback by default. Use rollbackFor = Exception.class
PropagationBehaviorUse case
REQUIRED (default)Join existing tx or create newMost service methods
REQUIRES_NEWSuspend current, create new txAudit logs that must persist even if parent fails
NESTEDSavepoint within current txPartial rollback within a larger operation
SUPPORTSUse tx if exists, else run withoutRead-only methods that work either way
NOT_SUPPORTEDSuspend current tx, run withoutOperations that shouldn't be in a tx
TransactionalGotchas.javajava
@Service
public class OrderService {

    // ✅ Correct — public method, proxy intercepts
    @Transactional
    public void placeOrder(OrderRequest request) {
        Order order = orderRepo.save(toEntity(request));
        paymentService.charge(order);  // if this fails, order save is rolled back
    }

    // ❌ Self-invocation — proxy is bypassed!
    public void processOrders(List<OrderRequest> requests) {
        requests.forEach(this::placeOrder); // calls through 'this', not proxy
    }

    // ✅ Fix: inject self or use TransactionTemplate
    @Autowired private OrderService self; // proxy reference
    public void processOrders(List<OrderRequest> requests) {
        requests.forEach(self::placeOrder); // calls through proxy
    }

    // ✅ Rollback on checked exceptions
    @Transactional(rollbackFor = Exception.class)
    public void riskyOperation() throws PaymentException {
        // checked exception will now trigger rollback
    }
}

📝 Quick Revision

Quick Revision Cheat Sheet

JpaRepository: Extends CrudRepository. Adds pagination, batch ops, flush.

Query methods: findByEmailAndStatus → Spring generates SQL from method name.

N+1: Lazy collections cause N extra queries. Fix: JOIN FETCH or @EntityGraph.

Fetch types: Always use LAZY. Use JOIN FETCH when you need related data.

@Transactional: Proxy-based. Public only. Self-invocation bypasses. Checked exceptions don't rollback.

Propagation: REQUIRED (default), REQUIRES_NEW (independent tx), NESTED (savepoint).

07

Security — Spring Security

Spring Security is a filter-based framework that handles authentication (who are you?) and authorization (what can you do?). It's powerful but complex — understanding the filter chain architecture is the key to mastering it.

🔥 Filter Chain Architecture

Every request passes through a chain of security filters before reaching your controller. Each filter handles one concern. The order matters — authentication must happen before authorization.

  1. CorsFilter — handles CORS preflight
  2. CsrfFilter — CSRF protection (disabled for stateless APIs)
  3. Your JWT Filter — extract and validate token
  4. UsernamePasswordAuthenticationFilter — form login (if enabled)
  5. AuthorizationFilter — checks roles/permissions
  6. ExceptionTranslationFilter — converts security exceptions to HTTP responses

🔥 SecurityFilterChain Configuration

SecurityConfig.javajava
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())                    // stateless API — no CSRF needed
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()  // public endpoints
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
                .anyRequest().authenticated()                 // everything else needs auth
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint((req, res, e) ->
                    res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"))
                .accessDeniedHandler((req, res, e) ->
                    res.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden"))
            )
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

🔥 JWT Authentication Flow

The JWT filter runs before Spring's authentication filter. It extracts the token from the Authorization header, validates it (signature, expiry), and sets the SecurityContext with the user's details. Downstream filters and controllers can then access the authenticated user.

JwtAuthFilter.javajava
@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain chain) throws ServletException, IOException {
        String header = request.getHeader("Authorization");

        if (header == null || !header.startsWith("Bearer ")) {
            chain.doFilter(request, response); // no token — let other filters handle
            return;
        }

        String token = header.substring(7);
        try {
            Claims claims = jwtService.validateToken(token);
            var auth = new UsernamePasswordAuthenticationToken(
                claims.getSubject(),
                null,
                List.of(new SimpleGrantedAuthority("ROLE_" + claims.get("role")))
            );
            SecurityContextHolder.getContext().setAuthentication(auth);
        } catch (JwtException e) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
            return;
        }

        chain.doFilter(request, response);
    }
}

Method-Level Security

MethodSecurity.javajava
@Configuration
@EnableMethodSecurity // enables @PreAuthorize, @PostAuthorize
public class MethodSecurityConfig {}

@Service
public class UserService {

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long id) { ... }

    @PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
    public UserDto getUser(Long userId) { ... }

    @PostAuthorize("returnObject.owner == authentication.principal.username")
    public Document getDocument(Long docId) { ... }
}

CORS Configuration

CorsConfig.javajava
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://myapp.com"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
    config.setAllowCredentials(true);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", config);
    return source;
}

📝 Quick Revision

Quick Revision Cheat Sheet

Filter chain: CORS → CSRF → JWT filter → Auth filter → Authorization → Exception handling.

JWT flow: Extract token → validate → set SecurityContext → chain continues.

SecurityFilterChain: Bean that configures which endpoints need auth and how.

@PreAuthorize: Method-level security. SpEL expressions for role/permission checks.

CORS: Configure allowed origins, methods, headers. CorsConfigurationSource bean.

08

Testing

Spring Boot provides a layered testing approach — test slices that load only the parts of the application you need. Understanding which annotation to use and when is a common interview topic.

🔥 Test Slices — The Core Concept

AnnotationWhat it loadsUse caseSpeed
@SpringBootTestFull application contextIntegration tests, end-to-end flowsSlowest
@WebMvcTestControllers + filters + advice onlyTesting REST endpoints in isolationFast
@DataJpaTestJPA repos + embedded DBTesting queries and entity mappingsFast
@MockBeanReplaces a bean with a Mockito mockIsolating layers in slice tests
@JsonTestJackson serialization onlyTesting JSON serialization/deserializationFastest

🔥 Interview favorite

"When would you use @WebMvcTest vs @SpringBootTest?" — @WebMvcTest for testing controller logic in isolation (fast, no DB). @SpringBootTest for testing the full request flow including service and repository layers (slow, needs DB or mocks).

@WebMvcTest — Testing Controllers

UserControllerTest.javajava
@WebMvcTest(UserController.class) // loads only this controller
class UserControllerTest {

    @Autowired private MockMvc mockMvc;
    @MockBean private UserService userService; // mock the service layer

    @Test
    void shouldReturnUser() throws Exception {
        var user = new UserDto(1L, "Alice", "alice@example.com");
        when(userService.findById(1L)).thenReturn(user);

        mockMvc.perform(get("/api/v1/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("Alice"))
            .andExpect(jsonPath("$.email").value("alice@example.com"));
    }

    @Test
    void shouldReturn400ForInvalidInput() throws Exception {
        var invalidBody = "{\"name\": \"\", \"email\": \"not-an-email\"}";

        mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(invalidBody))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }

    @Test
    void shouldReturn404WhenNotFound() throws Exception {
        when(userService.findById(999L))
            .thenThrow(new ResourceNotFoundException("User not found"));

        mockMvc.perform(get("/api/v1/users/999"))
            .andExpect(status().isNotFound());
    }
}

@DataJpaTest — Testing Repositories

UserRepositoryTest.javajava
@DataJpaTest // loads JPA components + embedded H2 by default
class UserRepositoryTest {

    @Autowired private UserRepository userRepo;
    @Autowired private TestEntityManager em;

    @Test
    void shouldFindByEmail() {
        em.persistAndFlush(new User("Alice", "alice@example.com"));

        Optional<User> found = userRepo.findByEmail("alice@example.com");

        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("Alice");
    }

    @Test
    void shouldReturnEmptyForNonExistentEmail() {
        Optional<User> found = userRepo.findByEmail("nobody@example.com");
        assertThat(found).isEmpty();
    }
}

@MockBean vs @Mock

AnnotationContextWhen to use
@MockBeanSpring context — replaces bean in containerSlice tests (@WebMvcTest, @SpringBootTest)
@Mock (Mockito)No Spring context — plain unit testService/logic tests without Spring

Testcontainers — Real DB in Tests

Embedded H2 doesn't match production Postgres behavior. Testcontainers spins up a real Postgres (or any DB) in Docker for your tests. Slower than H2 but catches real SQL compatibility issues. Use for integration tests, not unit tests.

TestcontainersSetup.javajava
@SpringBootTest
@Testcontainers
class OrderIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
        .withDatabaseName("testdb");

    @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);
    }

    @Test
    void shouldPlaceOrderEndToEnd() {
        // full integration test with real Postgres
    }
}

Testing Strategy

LayerTest typeAnnotationWhat to test
ControllerSlice test@WebMvcTestRequest mapping, validation, response shape, status codes
ServiceUnit test@Mock (Mockito)Business logic, edge cases, error handling
RepositorySlice test@DataJpaTestCustom queries, entity mappings, constraints
Full flowIntegration@SpringBootTest + TestcontainersEnd-to-end happy path, critical flows

📝 Quick Revision

Quick Revision Cheat Sheet

@WebMvcTest: Controller slice. MockMvc + @MockBean for services. Fast.

@DataJpaTest: Repository slice. Embedded DB. Tests queries and mappings.

@SpringBootTest: Full context. Use for integration tests. Slowest.

@MockBean: Replaces bean in Spring context. @Mock for plain Mockito tests.

Testcontainers: Real DB in Docker. Use for integration tests that need Postgres behavior.

09

Caching, Async & Scheduling

Spring Boot provides declarative abstractions for caching, async execution, and scheduling. A single annotation can add caching to a method or make it run asynchronously. Understanding the internals and pitfalls is what interviewers test.

🔥 @Cacheable / @CacheEvict

CachingExample.javajava
@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        // Simple in-memory cache (use Redis in production)
        return new ConcurrentMapCacheManager("users", "products");
    }
}

@Service
public class UserService {

    @Cacheable(value = "users", key = "#id")
    public UserDto findById(Long id) {
        log.info("Cache miss — querying DB for user {}", id);
        return userRepo.findById(id).map(this::toDto)
            .orElseThrow(() -> new ResourceNotFoundException("User not found"));
    }

    @CacheEvict(value = "users", key = "#id")
    public UserDto updateUser(Long id, UpdateUserRequest request) {
        // cache entry for this id is evicted after method executes
        return userRepo.findById(id).map(user -> {
            user.setName(request.name());
            return toDto(userRepo.save(user));
        }).orElseThrow();
    }

    @CacheEvict(value = "users", allEntries = true)
    public void clearUserCache() {
        // evicts all entries in the "users" cache
    }
}
AnnotationBehaviorUse case
@CacheableReturns cached value if exists, else executes method and caches resultRead-heavy methods (findById, getConfig)
@CacheEvictRemoves entry from cacheAfter updates or deletes
@CachePutAlways executes method and updates cacheWhen you want to refresh cache on every call

💡 Cache providers

ConcurrentMapCacheManager is fine for dev. In production, use Redis (spring-boot-starter-data-redis) or Caffeine (spring-boot-starter-cache + Caffeine dependency) for TTL support, size limits, and distributed caching.

🔥 @Async — Asynchronous Execution

AsyncExample.javajava
@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
}

@Service
public class NotificationService {

    @Async // runs on a separate thread from the pool
    public CompletableFuture<Void> sendWelcomeEmail(String email) {
        emailClient.send(email, "Welcome!", "...");
        return CompletableFuture.completedFuture(null);
    }

    @Async
    public CompletableFuture<ReportDto> generateReport(Long userId) {
        // heavy computation — doesn't block the caller
        ReportDto report = buildReport(userId);
        return CompletableFuture.completedFuture(report);
    }
}

🔥 @Async gotchas

Same proxy rules as @Transactional: (1) only works on public methods, (2) self-invocation bypasses the proxy — calling an @Async method from the same class runs it synchronously. Always configure a custom thread pool — the default uses SimpleAsyncTaskExecutor which creates a new thread per task (no pooling).

@Scheduled — Cron Jobs

ScheduledTasks.javajava
@Configuration
@EnableScheduling
public class SchedulingConfig {}

@Component
public class ScheduledTasks {

    @Scheduled(fixedRate = 60000) // every 60 seconds
    public void refreshCache() {
        cacheService.refreshHotData();
    }

    @Scheduled(cron = "0 0 2 * * ?") // every day at 2 AM
    public void cleanupExpiredSessions() {
        sessionRepo.deleteExpired();
    }

    @Scheduled(fixedDelay = 30000) // 30s after previous execution completes
    public void processQueue() {
        queueService.processPendingItems();
    }
}
PropertyBehaviorExample
fixedRateRuns every N ms regardless of previous executionHeartbeat checks, cache refresh
fixedDelayWaits N ms after previous execution completesQueue processing (avoid overlap)
cronCron expression (second min hour day month weekday)Nightly cleanup, weekly reports

📝 Quick Revision

Quick Revision Cheat Sheet

@Cacheable: Cache method result. Key by params. Use Redis/Caffeine in prod.

@CacheEvict: Remove from cache on update/delete. allEntries=true to clear all.

@Async: Runs on separate thread. Configure thread pool. Proxy rules apply.

@Scheduled: fixedRate (every N ms), fixedDelay (after completion), cron (expression).

Proxy gotcha: Self-invocation bypasses proxy for @Cacheable, @Async, @Transactional.

10

Microservices Patterns

Spring Cloud extends Spring Boot for distributed systems. Each microservice is a standalone Boot app with its own database, deployed independently. The challenge is coordination — discovery, communication, resilience, and configuration.

🔥 Service Discovery

In a microservices architecture, services need to find each other. Hardcoding URLs breaks when services scale or move. Service discovery solves this: services register themselves with a registry (Eureka, Consul), and clients look up the registry to find available instances.

ToolTypeHow it works
Eureka (Netflix)Client-side discoveryServices register with Eureka server. Clients query Eureka to find instances.
Consul (HashiCorp)Server-side discoveryServices register. Consul provides DNS/HTTP API for lookup.
Kubernetes DNSPlatform-nativeK8s provides built-in service discovery via DNS names.

API Gateway

A single entry point for all client requests. Routes requests to the right microservice, handles cross-cutting concerns (auth, rate limiting, logging), and can aggregate responses from multiple services. Spring Cloud Gateway is the standard choice in the Spring ecosystem.

GatewayConfig.javajava
// Spring Cloud Gateway configuration
@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("user-service", r -> r
                .path("/api/users/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Request-Source", "gateway"))
                .uri("lb://user-service"))  // lb:// = load-balanced via discovery
            .route("order-service", r -> r
                .path("/api/orders/**")
                .uri("lb://order-service"))
            .build();
    }
}

🔥 Circuit Breaker — Resilience4j

When a downstream service is failing, continuing to call it wastes resources and cascades failures. A circuit breaker monitors failure rates and "opens" the circuit when failures exceed a threshold — subsequent calls fail fast without hitting the broken service. After a timeout, it allows a few test requests to check if the service recovered.

StateBehaviorTransitions to
CLOSED (normal)All requests pass through. Failures are counted.OPEN when failure rate exceeds threshold
OPEN (tripped)All requests fail immediately (fallback). No calls to downstream.HALF_OPEN after wait duration
HALF_OPEN (testing)Limited requests pass through to test recovery.CLOSED if successful, OPEN if still failing
CircuitBreaker.javajava
// application.yml
// resilience4j.circuitbreaker.instances.paymentService:
//   failureRateThreshold: 50        # open after 50% failures
//   waitDurationInOpenState: 10s    # wait 10s before half-open
//   slidingWindowSize: 10           # evaluate last 10 calls

@Service
public class OrderService {

    @CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
    public PaymentResult processPayment(Order order) {
        return paymentClient.charge(order.getTotal()); // calls external service
    }

    // Fallback when circuit is open or call fails
    private PaymentResult paymentFallback(Order order, Throwable throwable) {
        log.warn("Payment service unavailable, queuing for retry", throwable);
        retryQueue.enqueue(order);
        return PaymentResult.pending("Payment queued for retry");
    }
}

Inter-Service Communication

StyleHowProsCons
Synchronous (REST)HTTP calls between servicesSimple, familiar, request-responseTight coupling, cascading failures, latency
Synchronous (gRPC)Binary protocol, protobufFast, type-safe, streaming supportMore complex setup, not browser-friendly
Asynchronous (messaging)Kafka, RabbitMQ eventsDecoupled, resilient, scalableEventual consistency, harder to debug

💡 Interview answer

"I prefer async messaging for most inter-service communication. It decouples services — if the order service publishes an OrderPlaced event, the notification service and inventory service consume it independently. If one is down, messages queue up and get processed when it recovers. I use synchronous REST only when the caller needs an immediate response."

Config Server & Distributed Tracing

  • Spring Cloud Config — centralized configuration for all services. Stores config in Git. Services pull config on startup and can refresh without restart.
  • Distributed tracing (Micrometer + Zipkin) — traces a request across multiple services. Each service adds a trace ID to logs and headers. Zipkin visualizes the full request path and latency per service.

📝 Quick Revision

Quick Revision Cheat Sheet

Service discovery: Eureka/Consul for dynamic lookup. K8s DNS in containerized environments.

API Gateway: Single entry point. Routing, auth, rate limiting. Spring Cloud Gateway.

Circuit breaker: CLOSED → OPEN (fail fast) → HALF_OPEN (test). Resilience4j.

Communication: Async messaging preferred. REST for immediate responses. gRPC for performance.

Config server: Centralized config in Git. Services pull on startup.

11

Actuator, Monitoring & Production

Spring Boot Actuator exposes production-ready endpoints for health checks, metrics, and diagnostics. Combined with Micrometer and external tools, it gives you full observability into your running application.

🔥 Actuator Endpoints

EndpointWhat it showsDefault exposure
/actuator/healthApplication health status (UP/DOWN)Exposed over HTTP
/actuator/infoApplication info (version, git commit)Exposed over HTTP
/actuator/metricsApplication metrics (JVM, HTTP, custom)Exposed over JMX only
/actuator/envEnvironment properties (with sanitization)Not exposed by default
/actuator/beansAll beans in the ApplicationContextNot exposed by default
/actuator/loggersView and change log levels at runtimeNot exposed by default
application.ymlyaml
# Expose specific actuator endpoints
management:
  endpoints:
    web:
      exposure:
        include: health, info, metrics, prometheus
  endpoint:
    health:
      show-details: when-authorized  # show DB, disk, Redis status
  health:
    db:
      enabled: true
    redis:
      enabled: true

Custom Health Indicators

CustomHealthIndicator.javajava
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        try {
            boolean reachable = paymentClient.ping();
            if (reachable) {
                return Health.up()
                    .withDetail("provider", "Stripe")
                    .withDetail("latency", "45ms")
                    .build();
            }
            return Health.down()
                .withDetail("error", "Payment gateway unreachable")
                .build();
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
}
// GET /actuator/health → { "status": "UP", "components": { "paymentGateway": { ... } } }

Micrometer Metrics + Prometheus

Micrometer is the metrics facade for Spring Boot (like SLF4J for logging). It collects metrics and exports them to monitoring systems. Prometheus scrapes the /actuator/prometheus endpoint, and Grafana visualizes the data. Spring Boot auto-configures JVM metrics, HTTP request metrics, and database connection pool metrics.

CustomMetrics.javajava
@Service
public class OrderService {
    private final Counter orderCounter;
    private final Timer orderTimer;

    public OrderService(MeterRegistry registry) {
        this.orderCounter = Counter.builder("orders.placed")
            .description("Total orders placed")
            .tag("type", "online")
            .register(registry);
        this.orderTimer = Timer.builder("orders.processing.time")
            .description("Order processing duration")
            .register(registry);
    }

    public OrderDto placeOrder(OrderRequest request) {
        return orderTimer.record(() -> {
            OrderDto order = processOrder(request);
            orderCounter.increment();
            return order;
        });
    }
}

Structured Logging

Use MDC (Mapped Diagnostic Context) to add request-scoped data (request ID, user ID) to every log line. This makes debugging production issues across distributed systems possible. Spring Boot uses Logback by default — configure structured JSON logging for production.

RequestIdFilter.javajava
@Component
public class RequestIdFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain chain) throws ServletException, IOException {
        String requestId = Optional.ofNullable(request.getHeader("X-Request-Id"))
            .orElse(UUID.randomUUID().toString());

        MDC.put("requestId", requestId);
        response.setHeader("X-Request-Id", requestId);

        try {
            chain.doFilter(request, response);
        } finally {
            MDC.clear(); // prevent memory leaks
        }
    }
}
// Every log line now includes: {"requestId": "abc-123", "message": "Order placed", ...}

Graceful Shutdown

Enable with server.shutdown=graceful. When the app receives a shutdown signal, it stops accepting new requests but finishes processing in-flight ones. Set spring.lifecycle.timeout-per-shutdown-phase=30s to control how long to wait before force-killing.

📝 Quick Revision

Quick Revision Cheat Sheet

Actuator: /health, /metrics, /info. Expose selectively. Never expose /env in prod.

Health indicators: Custom checks for external deps (DB, Redis, payment gateway).

Micrometer: Metrics facade. Counter, Timer, Gauge. Export to Prometheus/Grafana.

MDC logging: Add requestId/userId to every log line. Clear in finally block.

Graceful shutdown: server.shutdown=graceful. Finish in-flight requests before stopping.

12

Top 30 Interview Questions

These are the questions that actually get asked

Compiled from real interview experiences at product companies. Grouped by topic. Practice answering each in 2-3 minutes.

Core & IoC (1-6)

Q:1. What is Inversion of Control and how does Spring implement it?

A: IoC means the framework controls object creation and wiring, not your code. Spring implements it through the ApplicationContext container — you declare beans (via annotations or @Bean methods), and Spring creates them, resolves dependencies, and manages their lifecycle. The key benefit: loose coupling and testability.

Q:2. Explain the difference between @Component, @Service, @Repository, and @Controller.

A: All register beans via component scanning. @Component is the base. @Service is semantic (no extra behavior). @Repository adds persistence exception translation — wraps JDBC/JPA exceptions into Spring's DataAccessException hierarchy. @Controller enables request mapping. @RestController adds @ResponseBody.

Q:3. How does Spring Boot auto-configuration work?

A: @EnableAutoConfiguration loads auto-config classes from META-INF/spring/...AutoConfiguration.imports. Each class uses @Conditional annotations to decide whether to activate — @ConditionalOnClass checks the classpath, @ConditionalOnMissingBean backs off if you define your own bean. This is why adding a starter dependency automatically configures everything.

Q:4. What is the bean lifecycle in Spring?

A: Constructor → dependency injection → @PostConstruct → InitializingBean.afterPropertiesSet() → custom init → ready to use → @PreDestroy → DisposableBean.destroy() → garbage collected. @PostConstruct is the most common hook for initialization logic.

Q:5. Constructor injection vs field injection — which is preferred and why?

A: Constructor injection. Fields can be final (immutable), dependencies are explicit in the signature, easy to test (pass mocks via constructor), and it fails fast if a dependency is missing. Field injection hides dependencies, prevents final fields, and requires reflection — making testing harder.

Q:6. What are bean scopes? When would you use prototype over singleton?

A: Singleton (default): one instance per container — use for stateless services. Prototype: new instance per injection — use for stateful objects like builders or request-specific processors. Gotcha: injecting prototype into singleton creates it once. Use ObjectProvider to get fresh instances.

JPA & Data (7-13)

Q:7. Explain the N+1 query problem in JPA and how to solve it.

A: Fetch N parent entities, each with a lazy collection. Accessing the collection triggers N separate queries (1 for parents + N for children). Fix: JOIN FETCH in JPQL loads everything in one query, @EntityGraph declaratively specifies what to fetch, @BatchSize loads children in batches instead of one-by-one.

Q:8. How does @Transactional work internally?

A: Spring creates a proxy around the bean. When a @Transactional method is called, the proxy starts a transaction before the method and commits/rolls back after. Three gotchas: only works on public methods (proxy limitation), self-invocation bypasses the proxy, and checked exceptions don't trigger rollback by default.

Q:9. Explain @Transactional propagation levels.

A: REQUIRED (default): join existing tx or create new. REQUIRES_NEW: always create a new tx, suspend current — use for audit logs that must persist even if parent fails. NESTED: savepoint within current tx for partial rollback. NOT_SUPPORTED: run without tx.

Q:10. What's the difference between JpaRepository and CrudRepository?

A: CrudRepository provides basic CRUD (save, findById, findAll, delete, count). JpaRepository extends it with JPA-specific features: flush(), saveAllAndFlush(), batch deletes, and Pageable/Sort support. Use JpaRepository when you need pagination or batch operations.

Q:11. How does Spring Data JPA generate queries from method names?

A: Spring parses the method name at startup. findByEmailAndStatus becomes SELECT * FROM users WHERE email = ? AND status = ?. It supports keywords like And, Or, Between, LessThan, Like, OrderBy, etc. For complex queries, use @Query with JPQL or native SQL.

Q:12. Lazy vs eager loading — what's the default and what are the pitfalls?

A: @OneToMany and @ManyToMany default to LAZY. @ManyToOne and @OneToOne default to EAGER. Best practice: set everything to LAZY and use JOIN FETCH when you need related data. The main pitfall: LazyInitializationException when accessing a lazy collection outside a transaction — fix with DTOs mapped inside the service layer.

Q:13. How do you handle circular dependencies?

A: With constructor injection, Spring fails fast at startup — it can't create either bean. This is a design signal. Fix: extract shared logic into a third service, use @Lazy on one dependency (creates a proxy), or use application events for decoupling. Avoid setter injection as a fix — it hides the problem.

Security & API (14-19)

Q:14. How does Spring Security's filter chain work?

A: Every request passes through a chain of servlet filters: CORS → CSRF → Authentication (your JWT filter) → Authorization → Exception handling. Each filter handles one concern. The SecurityFilterChain bean configures which endpoints need auth, which are public, and where your custom filters sit in the chain.

Q:15. How do you implement JWT authentication in Spring Security?

A: Create a filter extending OncePerRequestFilter. Extract the Bearer token from the Authorization header, validate it (signature + expiry), create an Authentication object with the user's details and roles, and set it in SecurityContextHolder. Register this filter before UsernamePasswordAuthenticationFilter in the chain.

Q:16. What is @ControllerAdvice and how do you structure global error handling?

A: @RestControllerAdvice is a global exception handler. Each @ExceptionHandler method handles a specific exception type. Structure: specific handlers first (ResourceNotFoundException → 404, ValidationException → 400, DataIntegrityViolation → 409), generic Exception.class as fallback (500). Always return a consistent error shape.

Q:17. What is the DispatcherServlet and how does request routing work?

A: DispatcherServlet is the front controller. Every request flows through: Filter chain → DispatcherServlet → HandlerMapping (finds the right controller method via @RequestMapping) → HandlerAdapter (invokes it) → Controller → HttpMessageConverter (serializes response to JSON). Interceptors run between DispatcherServlet and the controller.

Q:18. How do you handle API versioning in Spring Boot?

A: URL path versioning (/api/v1/users) is most common — explicit and easy to test. Header versioning (Accept: application/vnd.api.v2+json) is cleaner but harder to test. In Spring: use @RequestMapping with path prefix for URL versioning, or custom RequestCondition for header versioning. Pick one and be consistent.

Q:19. How do you configure connection pooling (HikariCP)?

A: HikariCP is Spring Boot's default pool. Configure via properties: spring.datasource.hikari.maximum-pool-size (default 10, set to CPU cores × 2), minimum-idle, connection-timeout, max-lifetime. Monitor with /actuator/metrics (hikaricp.connections.*). Too many connections = context switching overhead. Too few = request queuing.

Advanced (20-25)

Q:20. What is Spring AOP and where is it used internally?

A: AOP (Aspect-Oriented Programming) handles cross-cutting concerns without modifying business code. Spring uses proxy-based AOP. Internally: @Transactional (wraps methods in transactions), @Cacheable (caches results), @Async (runs on separate thread), Spring Security (method-level auth). You can create custom aspects with @Aspect + @Around.

Q:21. Explain the circuit breaker pattern with Resilience4j.

A: Monitors failure rates for downstream calls. Three states: CLOSED (normal, counting failures), OPEN (fail fast with fallback, no calls to downstream), HALF_OPEN (test with limited requests). Configure failure threshold, wait duration, and sliding window size. Use @CircuitBreaker annotation with a fallback method.

Q:22. What are Spring profiles and how do you use them?

A: Profiles swap beans and config per environment. @Profile('dev') on a bean means it's only created in dev. application-dev.yml overrides application.yml when dev profile is active. Activate with spring.profiles.active=dev (property) or -Dspring.profiles.active=prod (JVM arg). Use for different DB configs, mock services in dev, feature flags.

Q:23. How does @Async work and what are the pitfalls?

A: @Async makes a method run on a separate thread. Requires @EnableAsync. Pitfalls: (1) self-invocation bypasses proxy — runs synchronously, (2) default executor creates a new thread per task (no pooling) — always configure a ThreadPoolTaskExecutor, (3) exceptions in void methods are lost — use CompletableFuture return type to handle errors.

Q:24. What is the difference between application.properties and application.yml?

A: Same purpose, different format. Properties: flat key=value pairs, repetitive prefixes. YAML: hierarchical, cleaner for nested config, supports multi-document (--- separator) for profiles in one file. YAML is preferred for complex configs. Both support Spring Expression Language and property placeholders.

Q:25. How do you implement pagination in Spring Data JPA?

A: Add Pageable parameter to repository methods. Create PageRequest.of(page, size, Sort) in the service. Return Page<DTO> which includes content, totalElements, totalPages, number, size. In the controller, accept page/size as @RequestParam with defaults. Always map entities to DTOs before returning.

Testing & Production (26-30)

Q:26. When would you use @WebMvcTest vs @SpringBootTest?

A: @WebMvcTest loads only the web layer (controllers, filters, advice) — fast, use for testing request mapping, validation, and response shapes. @SpringBootTest loads the full context — slow, use for integration tests that need the real service and repository layers. Use @MockBean in @WebMvcTest to mock services.

Q:27. How do you test repositories with @DataJpaTest?

A: @DataJpaTest loads only JPA components with an embedded H2 database. Use TestEntityManager to set up test data. Test custom query methods, entity mappings, and constraints. For Postgres-specific behavior, use Testcontainers to spin up a real Postgres in Docker.

Q:28. What is Spring Boot Actuator and how do you use it in production?

A: Actuator exposes operational endpoints: /health (liveness/readiness), /metrics (JVM, HTTP, custom), /info (app version), /loggers (change log levels at runtime). Expose selectively — never expose /env or /beans in production. Combine with Micrometer + Prometheus + Grafana for full observability.

Q:29. How do you handle graceful shutdown?

A: Set server.shutdown=graceful in properties. On shutdown signal, Spring stops accepting new requests but finishes in-flight ones. Configure spring.lifecycle.timeout-per-shutdown-phase=30s for the maximum wait. Essential for zero-downtime deployments in Kubernetes — the pod drains connections before terminating.

Q:30. How do you structure a Spring Boot application for maintainability?

A: Layered architecture: Controller (HTTP) → Service (business logic) → Repository (data access). Each layer depends only on the layer below. Use DTOs for API input/output, entities for JPA. Never expose entities in API responses. Use @ControllerAdvice for global error handling. Keep controllers thin — delegate to services.

13

Last Day Revision Sheet

📋 Scan this the night before your interview

This is the compressed version of everything above. If you can explain each line, you're ready.

Core & IoC

Quick Revision Cheat Sheet

IoC: Container creates and wires objects. You don't use 'new' for managed beans.

@SpringBootApplication: @Configuration + @EnableAutoConfiguration + @ComponentScan.

Auto-config: @Conditional annotations decide what to configure based on classpath + properties.

@ConditionalOnMissingBean: Define your own bean to override auto-config. The escape hatch.

Stereotypes: @Service (logic), @Repository (data + exception translation), @Controller (web).

Bean lifecycle: Constructor → DI → @PostConstruct → ready → @PreDestroy → destroyed.

Profiles: Swap beans/config per env. application-{profile}.yml.

Dependency Injection

Quick Revision Cheat Sheet

Constructor injection: Preferred. Immutable (final), testable, fails fast. @Autowired optional with single constructor.

@Primary vs @Qualifier: @Primary = default. @Qualifier = explicit pick. Qualifier wins.

Singleton scope: Default. One per container. Use for stateless services.

Prototype gotcha: Injecting prototype into singleton creates it once. Use ObjectProvider.

Circular deps: Redesign first. @Lazy as escape hatch. Events for decoupling.

REST & Error Handling

Quick Revision Cheat Sheet

Request flow: Filter → DispatcherServlet → HandlerMapping → Controller → MessageConverter → Response.

@RestController: @Controller + @ResponseBody. Returns JSON directly.

@Valid: Triggers Bean Validation. Throws MethodArgumentNotValidException (400).

@ControllerAdvice: Global exception handler. Specific handlers first, Exception.class as fallback.

Error shape: Consistent: code, message, details, timestamp, requestId.

JPA / Hibernate

Quick Revision Cheat Sheet

N+1 problem: Lazy collections cause N extra queries. Fix: JOIN FETCH or @EntityGraph.

Fetch strategy: Always LAZY. Use JOIN FETCH when you need related data.

LazyInitException: Accessing lazy collection outside tx. Fix: map to DTOs inside service.

@Transactional: Proxy-based. Public only. Self-invocation bypasses. Checked exceptions don't rollback.

Propagation: REQUIRED (default), REQUIRES_NEW (independent tx), NESTED (savepoint).

Query methods: findByEmailAndStatus → Spring generates SQL from method name.

Security

Quick Revision Cheat Sheet

Filter chain: CORS → CSRF → JWT filter → Auth filter → Authorization → Exception handling.

JWT flow: Extract token → validate → set SecurityContext → chain continues.

@PreAuthorize: Method-level security. SpEL expressions for role/permission checks.

SecurityFilterChain: Bean that configures endpoints, auth rules, and filter order.

Testing

Quick Revision Cheat Sheet

@WebMvcTest: Controller slice. MockMvc + @MockBean. Fast.

@DataJpaTest: Repository slice. Embedded DB. Tests queries and mappings.

@SpringBootTest: Full context. Integration tests. Slowest.

Testcontainers: Real DB in Docker. Use for Postgres-specific behavior.

Caching, Async & Microservices

Quick Revision Cheat Sheet

@Cacheable: Cache method result. @CacheEvict on writes. Redis/Caffeine in prod.

@Async: Separate thread. Configure pool. Proxy rules apply. CompletableFuture for errors.

@Scheduled: fixedRate (every N ms), fixedDelay (after completion), cron (expression).

Circuit breaker: CLOSED → OPEN (fail fast) → HALF_OPEN (test). Resilience4j.

Service discovery: Eureka/Consul for dynamic lookup. K8s DNS in containers.

Production

Quick Revision Cheat Sheet

Actuator: /health, /metrics, /info. Expose selectively. Never /env in prod.

Micrometer: Counter, Timer, Gauge. Export to Prometheus. Visualize in Grafana.

MDC logging: Add requestId to every log line. Clear in finally block.

HikariCP: Default pool. max-pool-size ≈ CPU cores × 2. Monitor via actuator.

Graceful shutdown: server.shutdown=graceful. Finish in-flight requests before stopping.