Spring · 15

Spring Boot Interview Q&A

95 questions across 15 topics, each with a crisp model answer you can say in under a minute, the follow-up trap interviewers use to separate memorisers from people who understand the mechanism, and a link back to the deep-dive page. Diagrams cover the bean lifecycle, proxy self-invocation, propagation, the filter chain, N+1, circuit breaker states and more.

95
Questions
15
Topic groups
16
Diagrams
95
Follow-up traps
01
Chapter One · Method

How to Answer a Spring Question

Spring interviews reward mechanism over vocabulary. Anyone can say "@Transactional starts a transaction"; the candidates who get hired say that a proxy wraps the bean, which thread owns the connection, and why a call from inside the class never reaches it. Use the same five-beat shape for every question below: define, explain the mechanism, state the trade-off, give a production example, name the trap.

The five-beat answer shape - 60 to 90 seconds, then stop
1 Define one sentence 2 Mechanism what Spring does 3 Trade-off when not to 4 Example real incident 5 Gotcha the trap Interviewers probe beat 2 and beat 5 - volunteer them before they ask.
The question sounds like a definition ("What is IoC?")
▶
Give the one-sentence definition, then immediately show the runtime mechanism: which class does the work (ApplicationContext.refresh(), TransactionInterceptor, FilterChainProxy)
The question is a comparison ("X vs Y")
▶
State the one axis that differs (scope, thread, proxy, timing), give a default recommendation, and say when you would pick the other
The question is a bug scenario ("it returns 200 but nothing is saved")
▶
List hypotheses in order of likelihood, say what log or metric proves or kills each, then fix the root cause - never start with "add a cache" or "increase the pool"
The interviewer says "and what if...?"
▶
That is the follow-up trap. Pause, restate the mechanism, and reason from it. Admitting "I would verify with the conditions report" beats a confident guess
Analogy: a Spring answer is a stack trace in words. Start at the symptom, walk down through the framework classes that were involved, and stop at the line that explains the behaviour. Interviewers are checking whether you can debug without the IDE.
Rules that earn marks:
  • Name the Spring version you assume (Boot 3.x, Framework 6, Security 6, Hibernate 6, jakarta.*). Mentioning removed APIs (WebSecurityConfigurerAdapter, antMatchers) without saying so signals stale knowledge.
  • Quote real exception names: BeanCurrentlyInCreationException, LazyInitializationException, UnexpectedRollbackException.
  • Always give the default (propagation REQUIRED, scope singleton, proxy type CGLIB) before discussing alternatives.
  • Close with a trade-off or a production gotcha - it proves you have run this in anger.
02
Chapter Two · Topic · 7 questions

Core & Dependency Injection

Container basics - the questions that open almost every Spring interview. Deep dive: IoC & DI

Q01 · What is Inversion of Control, and how is Dependency Injection different?

IoC means the framework, not your code, controls object creation and wiring. DI is the main technique: a class declares what it needs (constructor parameters) and the container supplies matching beans. Your code stops calling new on collaborators and depends on types, which makes substitution (mocks, alternative implementations) trivial.

Follow-up trap: Is DI the only form of IoC? No - template-method callbacks, event listeners and the servlet container calling your servlet are IoC too. Also: new is still fine for value objects and DTOs; only collaborators should be injected.

Topic page: IoC & DI

Q02 · BeanFactory vs ApplicationContext - which do you use and why?

BeanFactory is the minimal container: lazy bean creation and dependency resolution. ApplicationContext extends it and adds eager singleton creation, event publication, MessageSource i18n, resource loading, environment/profile support and automatic registration of BeanPostProcessor and BeanFactoryPostProcessor beans. You essentially always use an ApplicationContext; Boot creates an AnnotationConfigServletWebServerApplicationContext for you.

Follow-up trap: Why does a misconfigured bean fail at startup instead of on first request? Because ApplicationContext pre-instantiates non-lazy singletons during refresh() - fail-fast is a feature.

Topic page: IoC & DI

Q03 · Constructor, setter or field injection - which and why?

Prefer constructor injection: dependencies are mandatory and immutable (final fields), the object can never exist half-wired, it works in plain unit tests without Spring or reflection, and cycles surface at startup. Setter injection suits genuinely optional dependencies. Field injection (@Autowired on a field) hides dependencies, blocks final, and needs reflection to test.

@Service class OrderService { private final PaymentGateway gateway; private final OrderRepository repo; OrderService(PaymentGateway gateway, OrderRepository repo) { this.gateway = gateway; this.repo = repo; } }
Follow-up trap: Since Spring 4.3 a single constructor needs no @Autowired. Trap: ten constructor parameters is not a reason to go back to field injection - it is a design smell that the class does too much.

Topic page: IoC & DI

Q04 · Two beans implement the same interface - how does Spring choose?

Resolution order: by type; if several candidates, @Primary wins; then @Qualifier on the injection point; finally the parameter or field name is matched against bean names as a fallback. If it is still ambiguous you get NoUniqueBeanDefinitionException: expected single matching bean but found 2.

Follow-up trap: Can you inject all of them? Yes: List<PaymentGateway>, Map<String,PaymentGateway> (bean name keys) or ObjectProvider. Trap: @Primary is a default, @Qualifier is explicit - the explicit one wins when both exist.

Topic page: IoC & DI

Q05 · How do circular dependencies behave in Spring, and what do you do about them?

With constructor injection A needs B fully built and B needs A, so Spring throws BeanCurrentlyInCreationException. Historically singleton setter/field cycles were resolved with early references from the three-level singleton cache, but since Boot 2.6 circular references are prohibited by default (spring.main.allow-circular-references). The correct fix is design: extract the shared logic into a third bean, invert one direction with an event, or inject lazily.

Constructor cycle: A needs B, B needs A - the container cannot finish either
OrderService ctor(PaymentService p) PaymentService ctor(OrderService o) 1. needs PaymentService 2. needs OrderService (in creation!) BeanCurrentlyInCreationException: Requested bean is currently in creation Fix by design: extract a third bean, publish an event, or inject the dependency lazily - not allow-circular-references=true
Follow-up trap: Why is @Lazy on a constructor parameter a workaround rather than a fix? It injects a proxy that resolves later, hiding the coupling. Also, early references break when AOP proxies are involved because the half-built bean can be exposed unproxied.

Topic page: IoC & DI

Q06 · @Component vs @Bean vs @Configuration - when do you use which?

@Component (and stereotypes @Service, @Repository, @Controller) marks your own classes for component scanning. @Bean methods inside a @Configuration class register objects you do not own (third-party clients, DataSource) or need to build programmatically. @Configuration classes are CGLIB-proxied by default (proxyBeanMethods = true) so calling one @Bean method from another returns the same singleton.

Follow-up trap: What changes with proxyBeanMethods = false? Calls between @Bean methods create new instances (like 'lite mode'); it is faster startup and fine when beans receive collaborators as method parameters. Also @Repository adds exception translation to DataAccessException.

Topic page: IoC & DI

Q07 · How does component scanning decide what to register?

@ComponentScan (inside @SpringBootApplication) walks the package of the annotated class and all sub-packages, reads class files via ASM metadata, and registers any class carrying a stereotype annotation as a BeanDefinition. Nothing is instantiated at that stage - only definitions are collected.

Follow-up trap: Why is my bean missing? Classic: the class lives in a sibling package outside the main class's package tree. Fix by moving the main class up or setting scanBasePackages.

Topic page: IoC & DI

03
Chapter Three · Topic · 7 questions

Beans, Scopes & Lifecycle

Scopes and the exact order of callbacks - an interview favourite for seniors. Deep dive: Scopes & Lifecycle

Q08 · What bean scopes exist, and what is the default?

singleton (default, one instance per container, not per JVM), prototype (new instance per lookup), and web scopes request, session, application, websocket. Singleton does not mean thread-safe - a singleton with mutable fields is shared state across all request threads.

Follow-up trap: Is a Spring singleton the GoF Singleton? No - it is scoped to one ApplicationContext; two contexts in one JVM give two instances.

Topic page: Scopes & Lifecycle

Q09 · What happens when a prototype bean is injected into a singleton?

Injection happens once, when the singleton is created, so the singleton keeps the same 'prototype' instance forever - it behaves like a singleton. To get a fresh instance per use, inject ObjectProvider<T> (call getObject()), use @Lookup, or annotate the prototype with @Scope(value="prototype", proxyMode=TARGET_CLASS) so a scoped proxy resolves the target on every call.

Follow-up trap: How do you spot this bug? Two requests share state that should be per-call. Same trap for request/session-scoped beans injected into singletons without a proxy: IllegalStateException: No thread-bound request found.

Topic page: Scopes & Lifecycle

Q10 · Walk me through the lifecycle of a singleton bean.

Instantiate (constructor) -> populate properties -> BeanNameAware/BeanFactoryAware/ApplicationContextAware -> BeanPostProcessor.postProcessBeforeInitialization (this is where @PostConstruct runs) -> InitializingBean.afterPropertiesSet() -> custom initMethod -> postProcessAfterInitialization (AOP proxies are created here) -> ready. On shutdown: @PreDestroy -> DisposableBean.destroy() -> custom destroyMethod.

Singleton bean lifecycle - the order interviewers expect you to recite
1 Instantiate constructor runs 2 Populate DI of fields/setters 3 Aware callbacks BeanNameAware... 4 BPP before-init @PostConstruct here 5 afterPropertiesSet InitializingBean 6 init-method @Bean(initMethod) 7 BPP after-init AOP proxy created 8 READY in singleton cache 9 @PreDestroy on context close 10 destroy() DisposableBean 11 destroyMethod @Bean(destroyMethod) prototypes skip 9-11: container forgets them Proxies are made in step 7 - so @PostConstruct sees the raw target, not the proxy.
Follow-up trap: Why does @PostConstruct see the raw object and not the transactional proxy? Because the proxy is created in the after-initialization step. Calling this.save() inside @PostConstruct is therefore never transactional.

Topic page: Scopes & Lifecycle

Q11 · BeanPostProcessor vs BeanFactoryPostProcessor?

A BeanFactoryPostProcessor runs once, after bean definitions are loaded but before any bean is instantiated, and can modify definitions (e.g. PropertySourcesPlaceholderConfigurer resolving placeholders, ConfigurationClassPostProcessor processing @Configuration). A BeanPostProcessor runs for every bean instance around initialization and can wrap or replace the instance - this is how @Autowired, @PostConstruct and AOP proxies are implemented.

Follow-up trap: Why must BPP-producing @Bean methods be static? The processor has to be instantiated before the configuration class; non-static methods force early instantiation of the config class and you get 'not eligible for getting processed by all BeanPostProcessors' warnings.

Topic page: Scopes & Lifecycle

Q12 · @PostConstruct vs afterPropertiesSet vs initMethod - differences?

All three are init hooks that run after dependencies are injected. @PostConstruct (jakarta.annotation) is standard and runs first inside the BPP phase; afterPropertiesSet couples you to a Spring interface; initMethod is for classes you cannot annotate. Order: PostConstruct, afterPropertiesSet, initMethod.

Follow-up trap: Do prototype beans get @PreDestroy? No. The container hands a prototype to the caller and forgets it, so destruction callbacks never fire - you must clean up yourself.

Topic page: Scopes & Lifecycle

Q13 · How does the application event mechanism work?

ApplicationEventPublisher.publishEvent(Object) dispatches synchronously, on the publishing thread, to every matching @EventListener method via the ApplicationEventMulticaster. Listeners can be made async with @Async. Use @TransactionalEventListener (default phase AFTER_COMMIT) to react only once the surrounding transaction committed.

Follow-up trap: Why did my AFTER_COMMIT listener's database write silently not persist? The original transaction is already committed; the listener needs @Transactional(propagation = REQUIRES_NEW) to write.

Topic page: Scopes & Lifecycle

Q14 · What is SmartLifecycle and when do you need it over @PostConstruct?

SmartLifecycle beans start after all singletons exist, in phase order, and stop in reverse. Use it for components that start threads or consumers (message listeners, schedulers) and need ordered, graceful start/stop. @PostConstruct only runs per-bean initialization, with no ordering guarantee relative to other beans' readiness.

Follow-up trap: How is it related to graceful shutdown? Each lifecycle phase gets spring.lifecycle.timeout-per-shutdown-phase (default 30s) to stop.

Topic page: Scopes & Lifecycle

04
Chapter Four · Topic · 6 questions

AOP & Proxies

Proxies explain half of the "weird" Spring behaviour interviewers ask about. Deep dive: AOP & Proxies

Q15 · What is AOP and what are the key terms?

AOP modularizes cross-cutting concerns (transactions, security, logging, metrics) that would otherwise be repeated in every method. Terms: aspect (the module), join point (a point in execution - in Spring AOP always a method call), pointcut (expression selecting join points), advice (code to run: @Before, @After, @AfterReturning, @AfterThrowing, @Around), weaving (applying aspects - Spring does it at runtime via proxies).

Follow-up trap: Difference from AspectJ? Spring AOP is proxy-based, method-execution-only on Spring beans; AspectJ weaves bytecode at compile/load time and can intercept field access, constructors and self-calls.

Topic page: AOP & Proxies

Q16 · JDK dynamic proxy vs CGLIB - what does Spring use?

JDK proxies implement the target's interfaces and only intercept interface methods; CGLIB generates a subclass of the target class and overrides non-final methods. Spring Boot defaults to CGLIB (spring.aop.proxy-target-class=true), so you can inject by class and the proxy type is a subclass of your bean.

Follow-up trap: What breaks CGLIB? final classes and final/private methods cannot be overridden, so advice is silently skipped (Kotlin classes are final by default - use the kotlin-spring plugin).

Topic page: AOP & Proxies

Q17 · Why does @Transactional not work when I call another method in the same class?

The annotation is implemented by a proxy that wraps the bean. A call from outside goes through the proxy (begin tx, invoke, commit). A call via this.other() is a plain Java call on the target, bypassing the proxy, so no advice runs. This applies equally to @Cacheable, @Async, @Retryable and @PreAuthorize.

Self-invocation: the call that never crosses the proxy
Caller controller CGLIB proxy interceptors: tx / cache / async OrderService (target) a() @Transactional b() @Transactional (REQUIRES_NEW) service.a() begin tx this.b() plain Java call b() runs inside a()'s tx - REQUIRES_NEW is ignored Fix: move b() to another bean, or inject self via ObjectProvider / TransactionTemplate
Follow-up trap: Fixes, in order of preference: move the method to another bean; use TransactionTemplate programmatically; inject a self-reference via ObjectProvider; or switch to AspectJ weaving mode. Do not just 'make it public' - visibility is not the cause.

Topic page: AOP & Proxies

Q18 · Which Spring features are really just proxies?

@Transactional, @Cacheable/@CacheEvict/@CachePut, @Async, @Retryable, @PreAuthorize/@PostAuthorize and @Validated method validation. Each is a BeanPostProcessor that wraps matching beans in a proxy holding an interceptor chain.

Follow-up trap: How can you verify a bean is proxied? AopUtils.isAopProxy(bean), or print bean.getClass() and look for $$SpringCGLIB$$.

Topic page: AOP & Proxies

Q19 · How do you control the order of multiple aspects?

Implement Ordered or annotate the aspect with @Order(n): lower value = higher precedence = outermost on the way in, innermost on the way out. For example, a retry aspect must wrap the transaction aspect, otherwise each retry reuses an already-failed transaction.

Follow-up trap: What is the default order of @Transactional's advisor? Ordered.LOWEST_PRECEDENCE, i.e. innermost; adjust with @EnableTransactionManagement(order = ...).

Topic page: AOP & Proxies

Q20 · Write a pointcut that matches all public methods of services in com.shop.service.

@Around("execution(public * com.shop.service..*Service.*(..))"). execution matches method signatures (modifiers, return type, declaring type, name, params); within(...), @annotation(...), bean(...) and args(...) narrow further. Prefer annotation-based pointcuts for custom behaviour - they are explicit and refactor-safe.

@Aspect @Component class TimingAspect { @Around("@annotation(Timed)") Object time(ProceedingJoinPoint pjp) throws Throwable { long t0 = System.nanoTime(); try { return pjp.proceed(); } finally { log.info("{} took {} ms", pjp.getSignature(), (System.nanoTime()-t0)/1_000_000); } } }
Follow-up trap: Why does @Around need to call pjp.proceed()? Forgetting it silently swallows the call and returns null; forgetting to return its result drops the return value.

Topic page: AOP & Proxies

05
Chapter Five · Topic · 7 questions

Spring Boot Internals

Auto-configuration and startup - how "magic" is really conditional bean registration. Deep dive: Boot Internals

Q21 · What does @SpringBootApplication actually do?

It is a meta-annotation for @SpringBootConfiguration (a @Configuration), @EnableAutoConfiguration (imports the auto-config candidates) and @ComponentScan (with filters excluding auto-config and type-exclude filters). Nothing more - you can replace it with these three.

Follow-up trap: Why must the main class sit in the root package? @ComponentScan scans from its package downwards; classes outside are not found.

Topic page: Boot Internals

Q22 · Explain how auto-configuration works.

@EnableAutoConfiguration imports AutoConfigurationImportSelector, which reads META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports from every jar on the classpath (Boot 2.7+/3.x; spring.factories is no longer used for this). Each candidate is a @AutoConfiguration class guarded by conditions; only matching ones register beans.

How auto-configuration picks beans - candidates are filtered by conditions
@EnableAutoConfiguration AutoConfigurationImport Selector AutoConfiguration.imports in every jar on the classpath Candidate list (100+ classes) Conditions evaluated Surviving @Configuration classes register bean definitions @ConditionalOnClass: library present? @ConditionalOnMissingBean: user bean wins @ConditionalOnProperty: feature flag on? @ConditionalOnWebApplication: servlet / reactive
Follow-up trap: How does the user override a default? Auto-config beans are almost always @ConditionalOnMissingBean, so declaring your own bean makes the default back off.

Topic page: Boot Internals

Q23 · Name the common @Conditional annotations and what they check.

@ConditionalOnClass (type on classpath), @ConditionalOnMissingClass, @ConditionalOnBean/@ConditionalOnMissingBean (bean presence in the context), @ConditionalOnProperty (property value), @ConditionalOnWebApplication, @ConditionalOnResource, @ConditionalOnExpression (SpEL), @ConditionalOnCloudPlatform.

Follow-up trap: Why is @ConditionalOnBean fragile in ordinary configuration classes? It is evaluated at registration time, so it depends on class processing order; it is reliable only in auto-configurations ordered with @AutoConfiguration(after = ...).

Topic page: Boot Internals

Q24 · What happens inside SpringApplication.run()?

Create SpringApplication and detect web type -> load initializers/listeners -> publish ApplicationStartingEvent -> prepare Environment (property sources, profiles) -> print banner -> create ApplicationContext -> refresh() (process configuration classes and auto-config, register BPPs, instantiate singletons, start embedded web server in onRefresh/finishRefresh) -> call ApplicationRunner/CommandLineRunner beans -> ApplicationReadyEvent.

Follow-up trap: When does the port open? During refresh(), before runners execute, but readiness state flips to ACCEPTING_TRAFFIC only after ApplicationReadyEvent.

Topic page: Boot Internals

Q25 · A bean you expected is missing - how do you find out why?

Run with --debug (or debug=true) to print the conditions evaluation report: 'Positive matches', 'Negative matches' (with the failing condition, e.g. 'did not find required class'), 'Exclusions' and 'Unconditional classes'. The /actuator/conditions endpoint shows the same at runtime.

Follow-up trap: Trap: the negative match often says @ConditionalOnMissingBean because some other bean of that type already exists - that is the back-off working, not a bug.

Topic page: Boot Internals

Q26 · How would you write your own starter?

Two modules by convention: acme-spring-boot-autoconfigure containing an @AutoConfiguration class with @ConditionalOnClass, @EnableConfigurationProperties(AcmeProperties.class), @ConditionalOnMissingBean beans, and the .imports file; and a thin acme-spring-boot-starter pom depending on it plus the library. Add spring-boot-configuration-processor for IDE metadata.

Follow-up trap: Why name it acme-spring-boot-starter and not spring-boot-starter-acme? The spring-boot-starter-* prefix is reserved for official starters.

Topic page: Boot Internals

Q27 · How do you swap Tomcat for Jetty or Undertow?

Exclude spring-boot-starter-tomcat from spring-boot-starter-web and add spring-boot-starter-jetty (or -undertow). The auto-config for the embedded server is selected by classpath conditions (ServletWebServerFactoryAutoConfiguration). Tune with server.* properties or a WebServerFactoryCustomizer.

Follow-up trap: Also remember: virtual threads on Java 21 are enabled with spring.threads.virtual.enabled=true (Boot 3.2+).

Topic page: Boot Internals

06
Chapter Six · Topic · 7 questions

MVC & REST

The request path, binding and API design questions. Deep dive: REST & MVC

Q28 · Describe the flow of an HTTP request through Spring MVC.

Servlet filters -> DispatcherServlet.doDispatch() -> HandlerMapping (RequestMappingHandlerMapping) returns a HandlerExecutionChain (method + interceptors) -> interceptors' preHandle -> HandlerAdapter resolves arguments with HandlerMethodArgumentResolvers (and reads the body with an HttpMessageConverter) -> controller runs -> HandlerMethodReturnValueHandler writes the result with a converter -> postHandle/afterCompletion. Exceptions go to HandlerExceptionResolvers.

DispatcherServlet request path (Spring MVC)
Filters servlet / security DispatcherServlet doDispatch() HandlerMapping finds method + interceptors Interceptors preHandle() HandlerAdapter arg resolvers + converters Controller method your code Return handler message converter Response afterCompletion Exception -> HandlerExceptionResolver -> @ControllerAdvice -> ProblemDetail
Follow-up trap: Where does @ControllerAdvice fit? Inside the exception resolver step (ExceptionHandlerExceptionResolver), so it only sees exceptions thrown from controllers and interceptors - not from servlet filters.

Topic page: REST & MVC

Q29 · @Controller vs @RestController?

@RestController = @Controller + @ResponseBody: return values are written to the body via message converters (JSON by default through Jackson) rather than resolved as view names.

Follow-up trap: What happens if you return a String from a plain @Controller method? It is treated as a view name - a classic cause of 'Whitelabel Error Page' / 404 template-not-found.

Topic page: REST & MVC

Q30 · @PathVariable vs @RequestParam vs @RequestBody vs @ModelAttribute?

@PathVariable binds URI template segments (/users/{id}); @RequestParam binds query/form parameters; @RequestBody deserializes the whole body with a converter (Jackson); @ModelAttribute binds form/query parameters onto an object field-by-field. Add @RequestHeader and @CookieValue for headers/cookies.

Follow-up trap: What status for a missing required @RequestParam? 400 with MissingServletRequestParameterException. Malformed JSON gives HttpMessageNotReadableException (400); wrong Content-Type gives 415.

Topic page: REST & MVC

Q31 · Filter vs HandlerInterceptor vs @ControllerAdvice vs AOP - when to use each?

Filter: servlet-level, wraps everything including static resources and Spring Security; use for auth, CORS, request logging, body wrapping. HandlerInterceptor: MVC-level, knows the chosen handler; use for handler-aware cross-cutting like locale or rate limits. @ControllerAdvice: exception handling, @InitBinder, model attributes. AOP: method-level on any bean (service layer).

Follow-up trap: A filter runs before Spring MVC - how do you return a JSON error from it? You cannot rely on @ControllerAdvice; write the response yourself or delegate to HandlerExceptionResolver.

Topic page: REST & MVC

Q32 · POST vs PUT vs PATCH - semantics and idempotency; which status codes?

GET/PUT/DELETE are idempotent, POST is not, PATCH is not guaranteed. POST creates (return 201 + Location header); PUT replaces the whole resource (200/204); PATCH applies partial change (JSON Merge Patch or JSON Patch); DELETE returns 204. Use 404 for missing, 409 for conflict, 422/400 for validation, 401 vs 403 for authn vs authz.

Follow-up trap: Make POST safe to retry: accept an Idempotency-Key header and store the key with the result.

Topic page: REST & MVC

Q33 · How does content negotiation and JSON conversion work?

The Accept header and the controller's produces decide the media type; RequestResponseBodyMethodProcessor iterates registered HttpMessageConverters and uses the first that canWrite the type. Jackson's ObjectMapper is auto-configured by Boot (spring.jackson.*, Jackson2ObjectMapperBuilder).

Follow-up trap: Gotcha: dates. With jackson-datatype-jsr310 (included by Boot) dates serialize as ISO strings only if WRITE_DATES_AS_TIMESTAMPS is disabled - Boot disables it by default.

Topic page: REST & MVC

Q34 · How do you paginate and version an API?

Pagination: accept Pageable (?page=0&size=20&sort=name,asc) and return a Page or a stable DTO envelope; prefer keyset/cursor pagination for large or fast-changing data. Versioning: URI (/v1/), header, or media type; Spring Framework 7 adds first-class API versioning, earlier versions need manual mapping conditions.

Follow-up trap: Why is offset pagination a problem at scale? OFFSET 1000000 scans and discards rows; concurrent inserts shift pages. Keyset (WHERE id > :last) is O(log n).

Topic page: REST & MVC

07
Chapter Seven · Topic · 6 questions

Validation & Error Handling

Bean Validation, exception mapping and error contracts. Deep dive: Validation & Errors

Q35 · @Valid vs @Validated?

@Valid (jakarta.validation) triggers validation of a request body/model and cascades into nested objects. @Validated (Spring) additionally supports validation groups and, placed on a class, enables method-level validation of parameters (via a proxy) such as @PathVariable @Min(1) long id.

Follow-up trap: Which exception results? Body binding failure: MethodArgumentNotValidException (400). Constraint on a simple parameter: ConstraintViolationException via proxy, and from Spring 6.1 HandlerMethodValidationException for controller parameters. Handle all three.

Topic page: Validation & Errors

Q36 · Why is @NotNull not validating my nested object's fields?

Validation does not cascade by default. Annotate the nested field with @Valid (and container elements with List<@Valid Item>). Without it only the outer object's own constraints are checked.

Follow-up trap: Also check the dependency: Boot 3 needs spring-boot-starter-validation; without it the annotations are silently ignored.

Topic page: Validation & Errors

Q37 · How do you write a custom constraint?

Declare an annotation with @Constraint(validatedBy = ...) plus message, groups, payload; implement ConstraintValidator<A,T>. The validator is a Spring bean, so it can inject repositories (for example a unique-email check).

@Target(FIELD) @Retention(RUNTIME) @Constraint(validatedBy = UniqueEmailValidator.class) @interface UniqueEmail { String message() default "email already used"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } class UniqueEmailValidator implements ConstraintValidator<UniqueEmail,String> { private final UserRepository repo; UniqueEmailValidator(UserRepository repo) { this.repo = repo; } public boolean isValid(String v, ConstraintValidatorContext c) { return v == null || !repo.existsByEmail(v); } }
Follow-up trap: Should the validator treat null as valid? Yes by convention - combine with @NotNull so each constraint has a single responsibility. Cross-field rules belong in a class-level constraint.

Topic page: Validation & Errors

Q38 · How do you centralize error handling?

A @RestControllerAdvice with @ExceptionHandler methods maps exceptions to a consistent error body. Extend ResponseEntityExceptionHandler to get correct handling of Spring's own exceptions (400/404/405/415) and override the hooks you want to customize.

Follow-up trap: Which handler wins when several match? The most specific exception type; among advices, @Order. A catch-all @ExceptionHandler(Exception.class) must never swallow AccessDeniedException, or Spring Security's 403 turns into a 500.

Topic page: Validation & Errors

Q39 · What is ProblemDetail?

Spring 6's implementation of RFC 7807 (updated by RFC 9457): application/problem+json with type, title, status, detail, instance and custom properties. Return it from handlers or enable defaults with spring.mvc.problemdetails.enabled=true.

Follow-up trap: Why bother? One predictable machine-readable error contract; clients switch on type, not on message text. Never leak stack traces or SQL in detail.

Topic page: Validation & Errors

Q40 · What belongs in a good API error response?

A stable error code/type, a human-safe message, HTTP status matching semantics, a correlation/trace id for support, and field-level errors for validation (field, rejected value only if safe, message). Log the full exception server-side once, at the boundary.

Follow-up trap: Trap: returning 200 with {"success":false}. It breaks caches, monitoring and generic clients.

Topic page: Validation & Errors

08
Chapter Eight · Topic · 7 questions

Spring Data JPA & the N+1 Problem

Persistence context, fetching and the performance bug every team ships once. Deep dive: Spring Data JPA

Q41 · What is the persistence context and what are the entity states?

The EntityManager's first-level cache and identity map for one unit of work (usually one transaction). States: transient (new, unknown to Hibernate), managed (tracked; changes are auto-flushed - dirty checking), detached (was managed, session closed), removed. save() on a managed entity is unnecessary; flush happens at commit or before queries.

Follow-up trap: Why did my update persist without calling save()? Dirty checking on a managed entity inside a transaction. And why find twice runs one SQL? Second call hits the first-level cache.

Topic page: Spring Data JPA

Q42 · Explain the N+1 problem and how to fix it.

Loading N parents (1 query) then touching a lazy association on each fires N more queries. Fixes: join fetch in JPQL, @EntityGraph on the repository method, @BatchSize / hibernate.default_batch_fetch_size (turns N into N/size via IN lists), or a DTO projection that selects only needed columns.

public interface OrderRepository extends JpaRepository<Order, Long> { @EntityGraph(attributePaths = "items") List<Order> findByStatus(Status status); @Query("select o from Order o join fetch o.items where o.id = :id") Optional<Order> findWithItems(Long id); }
N+1 before / after - 101 queries become 1
BEFORE: lazy collection in a loop select * from orders (1 query, 100 rows) select * from items where order_id=? select * from items where order_id=? select * from items where order_id=? select * from items where order_id=? ... x 100 more = 101 round trips AFTER: fetch the graph in one query join fetch / @EntityGraph / @BatchSize / DTO query select o.*, i.* from orders o left join items i on i.order_id=o.id 1 query for the whole graph @BatchSize(50): 1 + 2 queries (IN lists) caution: join fetch + Pageable = pagination in memory (collection fetch) Detect it: spring.jpa.show-sql / Hibernate statistics / datasource-proxy query counter in tests
Follow-up trap: Trap: the 'fix' of switching to FetchType.EAGER moves the problem everywhere and often still issues N+1 for JPQL. Keep associations LAZY and fetch per use case.

Topic page: Spring Data JPA

Q43 · What does LazyInitializationException mean and what is open-in-view?

A lazy association was touched after the session closed (outside the transaction): could not initialize proxy - no Session. Boot enables Open Session In View by default (spring.jpa.open-in-view=true) which keeps the session open until the view is rendered and logs a warning at startup.

Follow-up trap: Why do many teams turn OSIV off? It hides lazy-loading queries in the web layer, holds a DB connection for the whole request, and encourages entities leaking into JSON. Fetch what you need in the service and return DTOs.

Topic page: Spring Data JPA

Q44 · Join fetch with Pageable - what goes wrong?

Fetching a collection with pagination makes Hibernate apply LIMIT in memory: it logs 'firstResult/maxResults specified with collection fetch; applying in memory' and loads everything. Fix: paginate IDs first, then fetch by IN; or use @BatchSize; or paginate parents and load children in a second query.

Follow-up trap: Fetching two bags (List) with join fetch gives MultipleBagFetchException; use Set or fetch separately.

Topic page: Spring Data JPA

Q45 · Derived queries vs @Query vs projections vs Specifications?

Derived methods (findByEmailAndActiveTrue) suit simple cases; @Query (JPQL or nativeQuery) for joins/aggregates; interface or record projections fetch only chosen columns; Specification composes dynamic predicates. Prefer DTO projections for read endpoints.

Follow-up trap: Why can a closed interface projection be cheaper than an open one? Closed projections let Spring select only the declared columns; SpEL (open) projections load the full entity.

Topic page: Spring Data JPA

Q46 · equals/hashCode and ID generation pitfalls with entities?

Do not base equals/hashCode on a generated id that is null until persist (breaks Set membership) and do not include lazy collections (triggers loading). Use a business key or a constant hashCode with id-based equals. GenerationType.IDENTITY disables JDBC batch inserts; SEQUENCE with allocationSize supports batching.

Follow-up trap: Why is @Data (Lombok) dangerous on entities? Generated toString/hashCode walk associations - infinite recursion or accidental lazy loads.

Topic page: Spring Data JPA

Q47 · How do optimistic and pessimistic locking differ?

Optimistic: a @Version column; UPDATE ... WHERE id=? AND version=?; zero rows updated throws ObjectOptimisticLockingFailureException, caller retries. Pessimistic: @Lock(PESSIMISTIC_WRITE) issues SELECT ... FOR UPDATE, blocking others until commit.

Follow-up trap: Choose optimistic when conflicts are rare (web forms), pessimistic for hot rows (inventory counters) - and keep pessimistic transactions short to avoid deadlocks.

Topic page: Spring Data JPA

09
Chapter Nine · Topic · 7 questions

Transactions

Where candidates lose points: rollback rules, propagation and self-invocation. Deep dive: Transactions

Q48 · How does @Transactional work under the hood?

A TransactionInterceptor in a proxy asks the PlatformTransactionManager (JpaTransactionManager) for a transaction, binds the connection/EntityManager to the thread (TransactionSynchronizationManager, thread-local), invokes the method, then commits or rolls back. Everything inside that thread shares the same transaction.

Follow-up trap: Consequence: a transaction does not follow you onto another thread (@Async, CompletableFuture, parallel streams) - the new thread has no transaction.

Topic page: Transactions

Q49 · Which exceptions trigger rollback?

By default RuntimeException and Error roll back; checked exceptions commit. Change with rollbackFor = Exception.class or noRollbackFor.

Follow-up trap: What if you catch the exception inside the method? The proxy sees a normal return and commits. And if an inner REQUIRED method threw and you caught it, the outer commit fails with UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only.

Topic page: Transactions

Q50 · Explain the propagation levels.

REQUIRED join or create (default); REQUIRES_NEW suspend and start independent tx; SUPPORTS, NOT_SUPPORTED, NEVER, MANDATORY and NESTED (savepoint) cover the rest - see the matrix.

Transaction propagation matrix - what happens when the caller has / has no transaction
Propagation No tx exists Tx already exists REQUIRED create new join existing REQUIRES_NEW create new suspend, create independent SUPPORTS run non-transactional join existing NOT_SUPPORTED run non-transactional suspend, run non-tx MANDATORY IllegalTransactionStateException join existing NEVER run non-transactional IllegalTransactionStateException NESTED create new savepoint inside existing tx Default is REQUIRED. REQUIRES_NEW needs a second connection from the pool.
Follow-up trap: Typical use of REQUIRES_NEW: write an audit record that must survive a rollback of the main work. Danger: it needs a second pooled connection while the first is held - pool deadlock under load.

Topic page: Transactions

Q51 · What are isolation levels and which anomalies do they prevent?

READ_UNCOMMITTED allows dirty reads; READ_COMMITTED prevents them; REPEATABLE_READ also prevents non-repeatable reads; SERIALIZABLE also prevents phantoms. Actual behaviour is database specific: PostgreSQL defaults to READ_COMMITTED, MySQL InnoDB to REPEATABLE_READ, and PostgreSQL's REPEATABLE_READ is snapshot-based.

Follow-up trap: @Transactional(isolation=...) only asks the driver to set it; if the pool or DB ignores it nothing happens. Lost updates are solved by locking/versioning, not isolation alone.

Topic page: Transactions

Q52 · What does readOnly = true really do?

It is a hint: Spring sets the JDBC connection read-only where supported and Hibernate switches the session to FlushMode.MANUAL (skips dirty checking, saving CPU/memory). It is not a security guarantee and does not forbid writes on every database.

Follow-up trap: Where to put it? On service query methods; and on class level with @Transactional overriding on writers.

Topic page: Transactions

Q53 · How do transactions interact with @Async and events?

@Async runs in another thread with no transaction, so reads may not see uncommitted data. @TransactionalEventListener(phase = AFTER_COMMIT) defers an event until the commit succeeds - the right way to send emails or publish messages only for committed data.

Follow-up trap: Classic bug: publishing to a broker inside the transaction, then the transaction rolls back - phantom message. Or the reverse: commit succeeds but the publish fails. Use the outbox pattern.

Topic page: Transactions

Q54 · Can @Transactional span two databases or a database and a message broker?

A single PlatformTransactionManager manages one resource. XA/JTA two-phase commit is possible but slow and operationally heavy. The modern answer: local transactions plus outbox/inbox, idempotent consumers and sagas for eventual consistency.

Transactional outbox: no dual write, at-least-once delivery
Order service one local transaction: INSERT orders INSERT outbox Relay poller or CDC (Debezium) Broker Kafka / SQS Consumer dedup by id Crash before commit: neither row exists. Crash after commit: relay still publishes later. Relay may publish twice, so consumers must be idempotent (unique message id).
Follow-up trap: Do not claim ChainedTransactionManager gives atomicity - it was best-effort one-phase commit and is deprecated.

Topic page: Transactions

10
Chapter Ten · Topic · 7 questions

Spring Security

Architecture questions - know the filter chain and the authentication flow. Deep dive: Security

Q55 · Describe Spring Security's architecture.

A servlet Filter (DelegatingFilterProxy -> FilterChainProxy) selects the first matching SecurityFilterChain bean and runs its ordered filters. Authentication filters build an Authentication token, hand it to the AuthenticationManager (ProviderManager), and on success the result is stored in the SecurityContext. AuthorizationFilter finally checks the request against your rules.

Servlet filter chain: where Spring Security sits and who answers 401 / 403
Servlet container (Tomcat) DelegatingFilterProxy (bean: springSecurityFilterChain) FilterChainProxy picks first matching chain one SecurityFilterChain (order top to bottom) SecurityContextHolderFilter load SecurityContext HeaderWriterFilter security headers CorsFilter preflight / CORS headers CsrfFilter validate CSRF token LogoutFilter /logout BearerTokenAuthenticationFilter JWT -> Authentication AnonymousAuthenticationFilter fill anonymous principal ExceptionTranslationFilter AuthenticationException -> 401 AuthorizationFilter authorizeHttpRequests rules DispatcherServlet (reached only if all filters pass) 401: AuthenticationEntryPoint 403: AccessDeniedHandler A filter can stop the chain: it simply does not call chain.doFilter().
Follow-up trap: Why can't you put a @ControllerAdvice in front of security errors? Security filters run before DispatcherServlet; 401/403 come from AuthenticationEntryPoint and AccessDeniedHandler.

Topic page: Security

Q56 · Walk through the username/password authentication flow.

UsernamePasswordAuthenticationFilter creates an unauthenticated UsernamePasswordAuthenticationToken -> ProviderManager asks each AuthenticationProvider -> DaoAuthenticationProvider calls UserDetailsService.loadUserByUsername and verifies the PasswordEncoder.matches() -> returns a fully authenticated token with authorities -> stored in SecurityContextHolder (a thread-local) -> persisted by SecurityContextRepository (session) if stateful.

Follow-up trap: Which exceptions? BadCredentialsException, UsernameNotFoundException (hidden by default into BadCredentials to avoid user enumeration), DisabledException, LockedException.

Topic page: Security

Q57 · How do you configure it in Spring Security 6?

Declare a SecurityFilterChain bean using the lambda DSL with authorizeHttpRequests/requestMatchers; WebSecurityConfigurerAdapter, antMatchers and authorizeRequests are removed. Rules are evaluated top to bottom and the first match wins, so put specific rules before anyRequest().

@Bean SecurityFilterChain api(HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf.disable()) // stateless token API only .sessionManagement(s -> s.sessionCreationPolicy(STATELESS)) .authorizeHttpRequests(a -> a .requestMatchers("/actuator/health/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/products/**").hasAuthority("SCOPE_catalog.read") .anyRequest().authenticated()) .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults())) .build(); }
Follow-up trap: Why is anyRequest().permitAll() before specific rules dangerous? It shadows everything below it. End with anyRequest().authenticated(), deny by default.

Topic page: Security

Q58 · When is it safe to disable CSRF?

CSRF exploits browsers automatically attaching credentials (cookies). If the API is stateless and authenticates with an Authorization header token that JavaScript adds explicitly, there is nothing for the browser to auto-send, so CSRF protection is unnecessary. If you use session cookies (even for a REST API), keep CSRF on.

Follow-up trap: Does CORS protect against CSRF? No. CORS relaxes the same-origin policy for reading responses; a cross-site form POST is still sent with cookies.

Topic page: Security

Q59 · CORS - how does it work and where is it configured?

For non-simple cross-origin requests the browser sends a preflight OPTIONS with Origin and Access-Control-Request-* headers; the server answers with Access-Control-Allow-*. With Spring Security, configure http.cors(...) with a CorsConfigurationSource, otherwise the security chain rejects the preflight (no credentials) before MVC's @CrossOrigin is consulted.

Follow-up trap: allowedOrigins("*") combined with allowCredentials(true) is rejected - use explicit origins or allowedOriginPatterns.

Topic page: Security

Q60 · URL rules vs method security?

URL rules (authorizeHttpRequests) are coarse and path-based. Method security (@EnableMethodSecurity, @PreAuthorize("hasRole('ADMIN') or #id == principal.id")) protects the service layer and supports expressions on arguments and returns. Use both - defence in depth.

Follow-up trap: @EnableGlobalMethodSecurity is gone. And method security is proxy-based, so self-invocation bypasses @PreAuthorize as well.

Topic page: Security

Q61 · Hashing vs encrypting passwords - what should you use?

Hash with a slow adaptive algorithm and per-password salt: BCryptPasswordEncoder, Argon2PasswordEncoder, SCryptPasswordEncoder, or PasswordEncoderFactories.createDelegatingPasswordEncoder() which stores the algorithm as {bcrypt}... so you can migrate. Never MD5/SHA-1/plain SHA-256, never reversible encryption.

Follow-up trap: What does There is no PasswordEncoder mapped for the id "null" mean? Stored hashes have no {id} prefix while a DelegatingPasswordEncoder is in use.

Topic page: Security

11
Chapter Eleven · Topic · 7 questions

JWT, OAuth2 & OIDC

Tokens, flows and the validation mistakes that cause breaches. Deep dive: JWT & OAuth2

Q62 · What is a JWT and what must a resource server validate?

A signed (JWS) token of three base64url parts: header, claims, signature. The API must verify the signature against the issuer's key (selected by kid from the JWK Set), pin the accepted algorithm, and check exp/nbf, iss and aud. With Spring: spring.security.oauth2.resourceserver.jwt.issuer-uri configures a JwtDecoder that does this.

JWT = header . payload . signature - and what a resource server must verify
HEADER alg: RS256 kid: key-2026-01 . PAYLOAD (claims) iss, sub, aud, exp, iat scope: "orders.read" . SIGNATURE RSA(sha256(b64(h).b64(p)), private key) 1. Pin allowed algorithm - reject alg=none and HS/RS confusion 2. Verify signature with key from JWK Set (select by kid) 3. exp / nbf with small clock skew 4. iss equals your trusted issuer 5. aud contains this API 6. Map scope/roles claims to GrantedAuthority Base64url is encoding, not encryption - never put secrets in the payload.
Follow-up trap: What is the alg: none attack? Accepting an unsigned token because the header says so. Algorithm confusion (RS256 key used as HS256 secret) is the sibling attack - always configure the expected algorithm explicitly.

Topic page: JWT & OAuth2

Q63 · Session vs JWT - trade-offs?

Sessions: server-side state, trivially revocable, need sticky sessions or a shared store, CSRF-sensitive. JWT: stateless and horizontally scalable, but cannot be revoked before exp; compensate with short lifetimes (5-15 min), refresh tokens, and a deny list for emergencies.

Follow-up trap: 'JWT is more secure than sessions' is false: it is a different trade-off. Large JWTs also bloat every request.

Topic page: JWT & OAuth2

Q64 · Explain the Authorization Code flow with PKCE.

The client generates a random code_verifier and sends its SHA-256 code_challenge in the authorize request; the user logs in at the authorization server; the client receives a one-time code via redirect and exchanges it at /token together with the code_verifier. The server recomputes the hash and issues tokens only on match, so an intercepted code is useless.

Authorization Code + PKCE sequence
Browser / SPA Authorization Server Resource Server (API) 1 GET /authorize?code_challenge=S256(v) 2 login + consent, 302 ?code=abc 3 POST /token code + code_verifier=v 4 access_token (+ refresh_token) 5 GET /orders Authorization: Bearer ... 6 200 JSON (token validated locally) Server stores code_challenge at step 1 and checks hash(code_verifier) at step 3 - a stolen code is useless.
Follow-up trap: Why not the Implicit flow? Tokens in the URL fragment leak; it is removed in OAuth 2.1. Same for Resource Owner Password Credentials. Use Authorization Code + PKCE for SPAs/mobile too.

Topic page: JWT & OAuth2

Q65 · Which grant for which client?

Browser/SPA/mobile/server web app with users: authorization code (+PKCE). Service-to-service without a user: client_credentials. Devices without browsers: device code. Backend-for-frontend pattern keeps tokens server-side with a session cookie for the browser.

Follow-up trap: Where do you store tokens in a SPA? There is no perfect place: localStorage is XSS-readable; httpOnly cookies are CSRF-prone but safer from XSS. The BFF pattern avoids exposing tokens to JS at all.

Topic page: JWT & OAuth2

Q66 · OAuth2 vs OpenID Connect?

OAuth2 is delegated authorization: an access token says what a client may do. OIDC adds authentication on top: an id_token (a JWT about the user), the openid scope, the /userinfo endpoint and discovery at /.well-known/openid-configuration.

Follow-up trap: Never use an access token to decide who the user is, and never send the id_token to an API as a bearer credential.

Topic page: JWT & OAuth2

Q67 · How do JWT claims become Spring authorities?

JwtAuthenticationConverter uses a JwtGrantedAuthoritiesConverter that by default reads scope/scp and prefixes each value with SCOPE_, so hasAuthority("SCOPE_orders.read"). For Keycloak-style realm_access.roles write a custom converter and set the authorityPrefix.

Follow-up trap: Why does hasRole('ADMIN') fail with a valid token? hasRole looks for ROLE_ADMIN; your authority is SCOPE_admin or admin.

Topic page: JWT & OAuth2

Q68 · Access tokens and refresh tokens - lifetimes and rotation?

Access tokens are short-lived and sent to APIs; refresh tokens are long-lived, only sent to the authorization server, and should be rotated on use (a reused old refresh token signals theft and revokes the family). Public clients must never hold a client secret.

Follow-up trap: How do you log a user out of a stateless API? You cannot invalidate the JWT; revoke the refresh token and wait for expiry, or keep a short blocklist keyed by jti.

Topic page: JWT & OAuth2

12
Chapter Twelve · Topic · 5 questions

Configuration & Profiles

Externalized configuration - precedence and binding. Deep dive: Configuration

Q69 · What is the order of property sources?

Highest to lowest: command-line arguments, SPRING_APPLICATION_JSON, Java system properties, OS environment variables, profile-specific files outside the jar, profile-specific inside the jar, application.yml outside then inside, @PropertySource, defaults. Higher wins, and test annotations override everything.

Property source precedence (highest wins) - Spring Boot 3
Command-line args --server.port=9000 SPRING_APPLICATION_JSON / servlet init params Java system properties -Dserver.port= OS environment variables SERVER_PORT application-{profile}.yml outside the jar application-{profile}.yml inside the jar application.yml outside the jar application.yml inside the jar @PropertySource + SpringApplication defaults Test annotations (@SpringBootTest(properties), @DynamicPropertySource) sit above all of these.
Follow-up trap: Which wins, application.yml inside the jar or the one in ./config? The external one. And application-prod.yml overrides application.yml for keys it defines.

Topic page: Configuration

Q70 · @Value vs @ConfigurationProperties?

@Value injects single values with SpEL support but offers no structure or validation. @ConfigurationProperties(prefix="app.mail") binds a hierarchy to a typed object (records supported via constructor binding), supports relaxed binding (app.mail-host = APP_MAIL_HOST), @Validated constraints and IDE metadata.

@ConfigurationProperties(prefix = "app.mail") @Validated public record MailProps(@NotBlank String host, @Min(1) int port, Duration timeout) {} @SpringBootApplication @ConfigurationPropertiesScan class App {}
Follow-up trap: Why is @Value with a missing key a startup failure? No default given: Could not resolve placeholder 'x'. Use @Value("${x:default}") or required properties.

Topic page: Configuration

Q71 · How do profiles work and how do you activate them?

A profile is a named group of beans/properties. Activate with spring.profiles.active=prod, SPRING_PROFILES_ACTIVE, or command line. Mark beans @Profile("dev"); load application-dev.yml. Profile groups (spring.profiles.group.prod=db,metrics) bundle profiles.

Follow-up trap: Avoid putting 'is prod' logic everywhere via @Profile; prefer config values. spring.profiles.active inside a profile-specific file is invalid.

Topic page: Configuration

Q72 · How should secrets be handled?

Never commit them. Inject at runtime through environment variables, Kubernetes Secrets, a vault (Spring Cloud Vault, AWS Secrets Manager) or the config tree import (spring.config.import=configtree:/run/secrets/). Keep them out of logs and /actuator/env (values are sanitized by default; keep it that way).

Follow-up trap: Environment variables show up in process dumps and child processes - a vault with short leases is stronger than a static env var.

Topic page: Configuration

Q73 · What is the 12-factor stance on config?

Config that varies between deploys lives in the environment, not in code or the artifact: one build, many environments. Boot's relaxed binding makes env vars first-class (SPRING_DATASOURCE_URL).

Follow-up trap: Trap: baking application-prod.yml with prod endpoints into the image couples build to environment and leaks hostnames.

Topic page: Configuration

13
Chapter Thirteen · Topic · 4 questions

Actuator & Observability

Operations questions: health, metrics, traces. Deep dive: Actuator & Observability

Q74 · What does Actuator expose and how do you secure it?

Operational endpoints (health, info, metrics, prometheus, env, beans, conditions, loggers, threaddump, heapdump). Over HTTP only health is exposed by default; choose with management.endpoints.web.exposure.include. Protect the rest with a dedicated security chain, or run them on a separate management.server.port not routed publicly.

Follow-up trap: heapdump and env are data-leak vectors. Expose the minimum and never include=* on a public port.

Topic page: Actuator & Observability

Q75 · Liveness vs readiness probes?

Liveness answers 'should this process be restarted?' (the app is not deadlocked/broken); readiness answers 'should traffic be routed to me now?' (dependencies available, warmup finished, draining on shutdown). Boot exposes /actuator/health/liveness and /readiness backed by LivenessState/ReadinessState.

Follow-up trap: Why must liveness not check the database? A DB outage would make Kubernetes restart every pod, turning an outage into a thundering herd. DB belongs in readiness.

Topic page: Actuator & Observability

Q76 · Counter, Gauge, Timer - when to use each in Micrometer?

Counter: monotonically increasing events (orders placed). Gauge: a sampled current value that can go up and down (queue size). Timer: durations and counts with percentiles/histograms (@Timed, Timer.record). DistributionSummary for non-time sizes.

Follow-up trap: High-cardinality tags (user id, URL with ids) explode series count and crash Prometheus - tag with bounded values only.

Topic page: Actuator & Observability

Q77 · How does distributed tracing and log correlation work in Boot 3?

Micrometer Tracing (with an OpenTelemetry or Brave bridge) creates spans per request and propagates W3C traceparent headers across RestClient/WebClient/messaging; trace and span ids are put in the SLF4J MDC so every log line carries them. Sleuth is gone in Boot 3.

Follow-up trap: MDC is thread-local: @Async and thread pools drop it unless you use a context-propagating executor/task decorator.

Topic page: Actuator & Observability

14
Chapter Fourteen · Topic · 5 questions

Caching & Resilience

Cache semantics and failure-handling patterns. Deep dive: Caching & Resilience

Q78 · How does @Cacheable work, and how is the key built?

CacheInterceptor (a proxy) checks the Cache for the key before calling the method; on miss it invokes the method and stores the result. Needs @EnableCaching and a CacheManager (Caffeine, Redis, ...). Default key: the single parameter, or a SimpleKey of all parameters; override with key="#id". @CacheEvict removes, @CachePut always runs and updates.

Follow-up trap: Why is my cache never hit? Self-invocation (proxy bypass), differing key types (Integer 1 vs Long 1), or mutable return objects. Also null results are cached unless unless="#result == null".

Topic page: Caching & Resilience

Q79 · Caffeine vs Redis?

Caffeine: in-process, nanosecond access, per-instance (each pod has its own copy, so entries diverge). Redis: shared across instances, network hop plus serialization, survives restarts. Often combine: a small local near-cache over Redis with short TTL.

Follow-up trap: Consistency trap: evicting in one pod does not evict in the others when using local caches - use pub/sub invalidation or short TTLs.

Topic page: Caching & Resilience

Q80 · What is a cache stampede and how do you prevent it?

When a hot key expires, many concurrent requests miss simultaneously and hit the database. Mitigate: @Cacheable(sync = true) (per-key lock within one JVM), single-flight/lock in Redis, TTL jitter, and refresh-ahead / stale-while-revalidate.

Cache stampede: 500 requests, one expired key
Without protection req req req cache MISS x500 DB: 500 identical queries -> overload With sync=true / single-flight req req req cache MISS, per-key lock 1 loader hits DB, 499 wait for result Add TTL jitter so many keys do not expire in the same second. Redis: use a lock or probabilistic early refresh.
Follow-up trap: sync=true only protects within a JVM - ten pods can still stampede together.

Topic page: Caching & Resilience

Q81 · Explain the circuit breaker pattern and its states.

CLOSED: calls pass and results are recorded in a sliding window. When the failure (or slow-call) rate exceeds a threshold it trips to OPEN: calls fail immediately with CallNotPermittedException and the failing dependency gets breathing room. After waitDurationInOpenState it goes HALF_OPEN and allows a few probe calls; success closes it, failure re-opens it.

@CircuitBreaker(name = "inventory", fallbackMethod = "stockFallback") public Stock stock(String sku) { return client.get(sku); } Stock stockFallback(String sku, Throwable t) { return Stock.unknown(sku); }
Resilience4j circuit breaker state machine
CLOSED calls pass, recorded OPEN fail fast HALF_OPEN limited probe calls failure rate >= 50% (window of 100 calls) after waitDuration (60s) probes succeed probes fail default numbers: slidingWindowSize=100, failureRateThreshold=50, waitDurationInOpenState=60s, permittedNumberOfCallsInHalfOpenState=10 Fallback runs on any exception, including CallNotPermittedException - fail fast and degrade gracefully.
Follow-up trap: How does it differ from retry? Retry repeats a call hoping it works; the breaker stops calling when it will probably not. They compose: Resilience4j's default aspect order is Retry(CircuitBreaker(RateLimiter(TimeLimiter(Bulkhead)))).

Topic page: Caching & Resilience

Q82 · Retry, rate limiter, bulkhead, time limiter - what does each protect?

Retry: transient faults (with exponential backoff and jitter). Rate limiter: caps calls per period to protect a downstream or yourself. Bulkhead: limits concurrent calls so one slow dependency cannot exhaust all threads. Time limiter: bounds waiting. All need timeouts on the HTTP client as the base layer.

Follow-up trap: Never retry non-idempotent operations blindly (duplicate charge). Retrying inside every layer multiplies load: 3 retries at 3 tiers = 27 calls.

Topic page: Caching & Resilience

15
Chapter Fifteen · Topic · 5 questions

Testing

Slices, mocks and the pitfalls that make tests slow or lying. Deep dive: Testing

Q83 · Unit test vs slice test vs @SpringBootTest?

Plain unit tests: no Spring, fastest, test logic with mocks. Slice tests load only a layer: @WebMvcTest (controllers, advice, filters, converters), @DataJpaTest (entities, repositories, in-memory DB, auto-rollback), @JsonTest, @RestClientTest. @SpringBootTest boots the whole context - use sparingly for a few end-to-end checks.

What each test style loads
@WebMvcTest Controllers, @ControllerAdvice Filters, converters, MockMvc services = @MockitoBean NO repositories / DB fast: ~1s @DataJpaTest Entities + Spring Data repos Hibernate, DataSource @Transactional, rolled back NO web layer, NO services fast: ~2s @SpringBootTest The whole ApplicationContext Real beans, real wiring webEnvironment: MOCK or RANDOM_PORT slow: starts everything Identical context configuration => Spring reuses the cached context across test classes. @MockitoBean splits the cache.
Follow-up trap: Slice tests load only what they need: if a @WebMvcTest complains about a missing bean, you forgot a @MockitoBean/@MockBean for the controller's service.

Topic page: Testing

Q84 · @MockBean vs @MockitoBean?

Both replace a bean with a Mockito mock in the context. From Spring Boot 3.4 / Framework 6.2, @MockBean/@SpyBean are deprecated in favour of Spring Framework's @MockitoBean/@MockitoSpyBean. In a plain Mockito unit test use @Mock + @InjectMocks.

Follow-up trap: Each distinct combination of mocked beans creates a new cached ApplicationContext - a common reason test suites get slow.

Topic page: Testing

Q85 · How do you test with a real database?

Use Testcontainers (PostgreSQLContainer) so tests run against the same engine as production. Boot 3.1+ @ServiceConnection wires the container's URL automatically; before that use @DynamicPropertySource. Make the container static and shared to avoid starting one per test.

@Testcontainers @DataJpaTest @AutoConfigureTestDatabase(replace = NONE) class OrderRepositoryTest { @Container @ServiceConnection static PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:16"); @Autowired OrderRepository repo; }
Follow-up trap: H2 passes, production fails: dialect differences (JSON, locking, ON CONFLICT) are exactly what H2 hides.

Topic page: Testing

Q86 · Why can @Transactional on a test hide bugs?

The test wraps everything in a transaction that rolls back, so the code under test never actually commits: no flush to the database, no constraint violations, no @TransactionalEventListener(AFTER_COMMIT), and lazy loading works where it would fail in production. With RANDOM_PORT the server runs on another thread and sees none of the test's uncommitted data.

Follow-up trap: Call entityManager.flush() and clear() to force SQL and a fresh first-level cache, or drop test-level @Transactional and clean up explicitly.

Topic page: Testing

Q87 · How do you test secured endpoints?

With spring-security-test: @WithMockUser(roles="ADMIN"), .with(jwt().authorities(...)), .with(csrf()) on MockMvc requests; or SecurityMockMvcRequestPostProcessors. For real wiring use a full @SpringBootTest with a signed test JWT.

Follow-up trap: Why do my POST tests return 403 in @WebMvcTest? CSRF is on and the request carries no token - add .with(csrf()) or disable CSRF for the stateless chain.

Topic page: Testing

16
Chapter Sixteen · Topic · 8 questions

Scenario & System Questions

Senior-level, open-ended questions: structure your answer, name the mechanism. Back to the Spring hub

Q88 · An endpoint returns 200 but the data was not saved. Where do you look?

First suspects: (1) @Transactional not applied - self-invocation, private/final method, bean created with new; (2) transaction committed but the exception was swallowed or checked and no rollbackFor; (3) write happened in an @Async thread or after commit in a listener; (4) readOnly=true. Enable logging.level.org.springframework.transaction.interceptor=TRACE to see begin/commit/rollback.

Follow-up trap: Interviewer's follow-up: 'how do you prove it is a proxy bypass?' Log AopUtils.isAopProxy(this) / check the call stack for TransactionInterceptor.

Topic page: Transactions

Q89 · An endpoint went from 200 ms to 5 s after a release. How do you investigate?

Measure before guessing: Micrometer timer and traces locate the slow span (DB, HTTP client, GC). Check query counts (N+1 after a mapping change), missing index (EXPLAIN), connection pool wait (hikaricp.connections.pending), thread dump for blocked threads, and recent config diffs. Reproduce with the same data volume.

Follow-up trap: Anti-answer: 'add a cache'. Caching before diagnosing hides the bug and adds invalidation problems.

Topic page: Actuator & Observability

Q90 · Hikari reports 'Connection is not available, request timed out after 30000ms'. Why?

The pool (default 10) is exhausted: connections are held too long (slow queries, long transactions, REQUIRES_NEW while holding another connection, remote calls inside @Transactional, open-in-view) or leaked. Diagnose with leakDetectionThreshold, hikaricp.connections.* metrics and a thread dump. Fix root cause; raise the pool only after.

Follow-up trap: Bigger pool is not the answer: databases handle a limited number of active connections, and more threads waiting on locks makes it worse.

Topic page: Transactions

Q91 · Design: publish an event to Kafka when an order is saved, without losing or phantom-publishing it.

Dual writes (DB then broker) cannot be atomic. Use the transactional outbox: in the same local transaction insert the order and an outbox row; a relay (poller or Debezium CDC) publishes and marks rows sent; consumers deduplicate by message id (at-least-once, idempotent processing).

Follow-up trap: Why not @TransactionalEventListener(AFTER_COMMIT) publishing directly? A crash between commit and publish loses the event with no way to recover it.

Topic page: Transactions

Q92 · How do you make a payment API idempotent?

Client sends an Idempotency-Key. In one transaction insert the key (unique constraint) with status; if the insert conflicts, return the stored response instead of re-executing. Combine with a state machine on the payment so repeated transitions are no-ops, and pass the key to the PSP.

Follow-up trap: Race: two identical requests arrive at once - the unique constraint serialises them; do not rely on a check-then-insert in application code.

Topic page: REST & MVC

Q93 · How do you achieve zero-downtime deployments and graceful shutdown?

Run several instances behind a load balancer with readiness probes; rolling update. On SIGTERM set server.shutdown=graceful so Tomcat stops accepting new requests and finishes in-flight ones within spring.lifecycle.timeout-per-shutdown-phase; readiness goes OUT_OF_SERVICE first. Schema changes must be backward compatible (expand/contract with Flyway/Liquibase).

Follow-up trap: Kubernetes sends traffic to a terminating pod for a moment - use a preStop sleep or readiness drain, and set terminationGracePeriodSeconds above the shutdown timeout.

Topic page: Actuator & Observability

Q94 · Startup fails with 'Parameter 0 of constructor in X required a bean of type Y that could not be found'. Triage?

The context has no bean of type Y. Check: is Y annotated and inside the component-scan root; does its auto-config's @ConditionalOnClass fail (missing dependency); is it behind a @Profile or @ConditionalOnProperty not active; run --debug and read the negative matches for Y.

Follow-up trap: Other common startup messages: Port 8080 was already in use, Failed to configure a DataSource: 'url' attribute is not specified, NoUniqueBeanDefinitionException.

Topic page: Boot Internals

Q95 · Memory keeps growing and the pod is OOMKilled. What is your approach?

Distinguish heap from container memory: check jvm.memory.used and GC metrics, take a heap dump (/actuator/heapdump or jcmd), look for unbounded caches, big result sets (use paging/streaming), ThreadLocal and MDC leaks, and the persistence context growing in long batch transactions (flush() + clear()). Size -XX:MaxRAMPercentage to leave headroom for metaspace and direct buffers.

Follow-up trap: Follow-up: the first-level cache keeps every loaded entity until the session closes, so a loop over a million rows in one transaction eventually fails.

Topic page: Spring Data JPA

17
Chapter Seventeen · Review

Study Checklist

Tick an item only if you can explain it aloud without notes, including the failure mode. Anything you cannot is a reading assignment on the linked page.

☑
Core, beans, AOP
  • ☐ Draw the constructor-cycle failure and name the exception
  • ☐ Recite the 11-step bean lifecycle
  • ☐ Explain why prototype-in-singleton is a singleton
  • ☐ Explain proxy self-invocation and three fixes
  • ☐ JDK vs CGLIB and what final breaks

Start here: IoC & DI

☑
Boot, MVC, validation
  • ☐ Decompose @SpringBootApplication
  • ☐ Read a conditions report to find a missing bean
  • ☐ Trace a request through DispatcherServlet
  • ☐ Filter vs interceptor vs advice
  • ☐ @Valid vs @Validated and the three exceptions

Start here: Boot Internals

☑
JPA and transactions
  • ☐ Entity states and dirty checking
  • ☐ N+1: reproduce, detect, three fixes
  • ☐ Why join fetch + Pageable is in memory
  • ☐ All seven propagation levels from memory
  • ☐ Rollback rules and UnexpectedRollbackException

Start here: Spring Data JPA

☑
Security and tokens
  • ☐ Filter chain order and who answers 401 vs 403
  • ☐ Authentication flow to SecurityContext
  • ☐ When CSRF can be disabled and why
  • ☐ Auth code + PKCE step by step
  • ☐ JWT validation checklist (alg, kid, exp, iss, aud)

Start here: Security

☑
Config and operations
  • ☐ Precedence ladder of property sources
  • ☐ @Value vs @ConfigurationProperties
  • ☐ Liveness vs readiness and why DB is not liveness
  • ☐ Counter vs gauge vs timer; cardinality
  • ☐ Graceful shutdown settings

Start here: Configuration

☑
Resilience and testing
  • ☐ Circuit breaker states and default thresholds
  • ☐ Cache stampede and sync=true limits
  • ☐ Slice test vs @SpringBootTest
  • ☐ @MockitoBean and context caching
  • ☐ Outbox pattern and idempotent consumers

Start here: Testing

18
Chapter Eighteen · Review

Quick-Fire Table

The one-liners to have ready for the rapid-fire round. Each row links to the page that explains the mechanism.

QuestionAnswerRead
Default bean scope?singleton, one per ApplicationContextIoC & DI
Preferred injection style?Constructor injection with final fieldsIoC & DI
Ambiguous beans of one type?@Primary or @Qualifier; else NoUniqueBeanDefinitionExceptionIoC & DI
When is @PostConstruct called?After DI, in the BPP before-init phase, before the proxy existsScopes & Lifecycle
Are prototype beans destroyed by Spring?No, @PreDestroy never firesScopes & Lifecycle
Default proxy type in Boot?CGLIB (proxy-target-class=true)AOP & Proxies
Why does @Transactional on this.other() fail?Self-call bypasses the proxyAOP & Proxies
Which file lists auto-configurations?META-INF/spring/...AutoConfiguration.importsBoot Internals
How to see why a bean was not created?--debug conditions report or /actuator/conditionsBoot Internals
@RestController =?@Controller + @ResponseBodyREST & MVC
Status for created resource?201 Created + Location headerREST & MVC
Exception for invalid @RequestBody?MethodArgumentNotValidException (400)Validation & Errors
Error body standard?ProblemDetail, RFC 9457 (application/problem+json)Validation & Errors
Fix N+1?join fetch, @EntityGraph, @BatchSize, DTO projectionSpring Data JPA
Default of open-in-view?true (disable it)Spring Data JPA
Default rollback exceptions?RuntimeException and Error onlyTransactions
Default propagation?REQUIREDTransactions
REQUIRES_NEW needs?A second connection from the poolTransactions
Spring Security 6 config style?SecurityFilterChain bean + lambda DSLSecurity
401 vs 403?Not authenticated vs not allowedSecurity
Disable CSRF when?Stateless, header-token auth, no cookie credentialsSecurity
JWT must-check claims?exp, nbf, iss, aud + signature with pinned algJWT & OAuth2
Grant for service-to-service?client_credentialsJWT & OAuth2
Default authority prefix for scopes?SCOPE_JWT & OAuth2
Highest property precedence?Command-line argumentsConfiguration
Typed config binding?@ConfigurationProperties (records, @Validated)Configuration
DB check belongs in?Readiness, not livenessActuator & Observability
Gauge vs counter?Gauge goes up and down; counter only upActuator & Observability
Circuit breaker states?CLOSED, OPEN, HALF_OPENCaching & Resilience
Cache stampede fix?sync=true / single-flight, TTL jitterCaching & Resilience
Controller-only test slice?@WebMvcTest + @MockitoBean servicesTesting
Real DB in tests?Testcontainers + @ServiceConnectionTesting
∑ Spring interviews - what to remember
  • Proxies explain most surprises: @Transactional, @Cacheable, @Async, @PreAuthorize all vanish on self-invocation, final or private methods.
  • Boot is conditional bean registration: read the conditions report before blaming the framework.
  • Transactions are thread-bound and proxy-driven: rollback rules, propagation and pool exhaustion are the three classic traps.
  • Security is a filter chain: know what runs before MVC and who writes 401/403.
  • Performance: N+1, open-in-view, long transactions and cache stampedes cause most real incidents.
  • Always answer with mechanism, default, trade-off, gotcha.
Related pages: