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.
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.
ApplicationContext.refresh(), TransactionInterceptor, FilterChainProxy)- 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, scopesingleton, proxy type CGLIB) before discussing alternatives. - Close with a trade-off or a production gotcha - it proves you have run this in anger.
Core & Dependency Injection
Container basics - the questions that open almost every Spring interview. Deep dive: IoC & DI
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.
new is still fine for value objects and DTOs; only collaborators should be injected.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.
refresh() - fail-fast is a feature.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.
@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.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.
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.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.
@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.@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.
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.@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.
scanBasePackages.Beans, Scopes & Lifecycle
Scopes and the exact order of callbacks - an interview favourite for seniors. Deep dive: Scopes & Lifecycle
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.
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.
IllegalStateException: No thread-bound request found.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.
@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.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.
@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.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.
@PreDestroy? No. The container hands a prototype to the caller and forgets it, so destruction callbacks never fire - you must clean up yourself.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.
@Transactional(propagation = REQUIRES_NEW) to write.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.
spring.lifecycle.timeout-per-shutdown-phase (default 30s) to stop.AOP & Proxies
Proxies explain half of the "weird" Spring behaviour interviewers ask about. Deep dive: AOP & Proxies
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).
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.
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).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.
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.@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.
AopUtils.isAopProxy(bean), or print bean.getClass() and look for $$SpringCGLIB$$.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.
Ordered.LOWEST_PRECEDENCE, i.e. innermost; adjust with @EnableTransactionManagement(order = ...).@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.
@Around need to call pjp.proceed()? Forgetting it silently swallows the call and returns null; forgetting to return its result drops the return value.Spring Boot Internals
Auto-configuration and startup - how "magic" is really conditional bean registration. Deep dive: Boot Internals
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.
@ComponentScan scans from its package downwards; classes outside are not found.@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.
@ConditionalOnMissingBean, so declaring your own bean makes the default back off.@ConditionalOnClass (type on classpath), @ConditionalOnMissingClass, @ConditionalOnBean/@ConditionalOnMissingBean (bean presence in the context), @ConditionalOnProperty (property value), @ConditionalOnWebApplication, @ConditionalOnResource, @ConditionalOnExpression (SpEL), @ConditionalOnCloudPlatform.
@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 = ...).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.
refresh(), before runners execute, but readiness state flips to ACCEPTING_TRAFFIC only after ApplicationReadyEvent.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.
@ConditionalOnMissingBean because some other bean of that type already exists - that is the back-off working, not a bug.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.
acme-spring-boot-starter and not spring-boot-starter-acme? The spring-boot-starter-* prefix is reserved for official starters.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.
spring.threads.virtual.enabled=true (Boot 3.2+).MVC & REST
The request path, binding and API design questions. Deep dive: REST & 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.
@ControllerAdvice fit? Inside the exception resolver step (ExceptionHandlerExceptionResolver), so it only sees exceptions thrown from controllers and interceptors - not from servlet filters.@RestController = @Controller + @ResponseBody: return values are written to the body via message converters (JSON by default through Jackson) rather than resolved as view names.
@Controller method? It is treated as a view name - a classic cause of 'Whitelabel Error Page' / 404 template-not-found.@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.
@RequestParam? 400 with MissingServletRequestParameterException. Malformed JSON gives HttpMessageNotReadableException (400); wrong Content-Type gives 415.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).
@ControllerAdvice; write the response yourself or delegate to HandlerExceptionResolver.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.
Idempotency-Key header and store the key with the result.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).
jackson-datatype-jsr310 (included by Boot) dates serialize as ISO strings only if WRITE_DATES_AS_TIMESTAMPS is disabled - Boot disables it by default.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.
OFFSET 1000000 scans and discards rows; concurrent inserts shift pages. Keyset (WHERE id > :last) is O(log n).Validation & Error Handling
Bean Validation, exception mapping and error contracts. Deep dive: Validation & Errors
@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.
MethodArgumentNotValidException (400). Constraint on a simple parameter: ConstraintViolationException via proxy, and from Spring 6.1 HandlerMethodValidationException for controller parameters. Handle all three.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.
spring-boot-starter-validation; without it the annotations are silently ignored.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).
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.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.
@Order. A catch-all @ExceptionHandler(Exception.class) must never swallow AccessDeniedException, or Spring Security's 403 turns into a 500.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.
type, not on message text. Never leak stack traces or SQL in detail.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.
{"success":false}. It breaks caches, monitoring and generic clients.Spring Data JPA & the N+1 Problem
Persistence context, fetching and the performance bug every team ships once. Deep dive: Spring Data JPA
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.
save()? Dirty checking on a managed entity inside a transaction. And why find twice runs one SQL? Second call hits the first-level cache.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.
FetchType.EAGER moves the problem everywhere and often still issues N+1 for JPQL. Keep associations LAZY and fetch per use case.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.
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.
List) with join fetch gives MultipleBagFetchException; use Set or fetch separately.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.
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.
@Data (Lombok) dangerous on entities? Generated toString/hashCode walk associations - infinite recursion or accidental lazy loads.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.
Transactions
Where candidates lose points: rollback rules, propagation and self-invocation. Deep dive: Transactions
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.
@Async, CompletableFuture, parallel streams) - the new thread has no transaction.By default RuntimeException and Error roll back; checked exceptions commit. Change with rollbackFor = Exception.class or noRollbackFor.
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.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.
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.
@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.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.
@Transactional overriding on writers.@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.
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.
ChainedTransactionManager gives atomicity - it was best-effort one-phase commit and is deprecated.Spring Security
Architecture questions - know the filter chain and the authentication flow. Deep dive: Security
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.
@ControllerAdvice in front of security errors? Security filters run before DispatcherServlet; 401/403 come from AuthenticationEntryPoint and AccessDeniedHandler.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.
BadCredentialsException, UsernameNotFoundException (hidden by default into BadCredentials to avoid user enumeration), DisabledException, LockedException.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().
anyRequest().permitAll() before specific rules dangerous? It shadows everything below it. End with anyRequest().authenticated(), deny by default.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.
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.
allowedOrigins("*") combined with allowCredentials(true) is rejected - use explicit origins or allowedOriginPatterns.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.
@EnableGlobalMethodSecurity is gone. And method security is proxy-based, so self-invocation bypasses @PreAuthorize as well.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.
There is no PasswordEncoder mapped for the id "null" mean? Stored hashes have no {id} prefix while a DelegatingPasswordEncoder is in use.JWT, OAuth2 & OIDC
Tokens, flows and the validation mistakes that cause breaches. Deep dive: JWT & OAuth2
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.
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.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.
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.
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.
localStorage is XSS-readable; httpOnly cookies are CSRF-prone but safer from XSS. The BFF pattern avoids exposing tokens to JS at all.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.
id_token to an API as a bearer credential.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.
hasRole('ADMIN') fail with a valid token? hasRole looks for ROLE_ADMIN; your authority is SCOPE_admin or admin.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.
jti.Configuration & Profiles
Externalized configuration - precedence and binding. Deep dive: Configuration
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.
application.yml inside the jar or the one in ./config? The external one. And application-prod.yml overrides application.yml for keys it defines.@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.
@Value with a missing key a startup failure? No default given: Could not resolve placeholder 'x'. Use @Value("${x:default}") or required properties.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.
@Profile; prefer config values. spring.profiles.active inside a profile-specific file is invalid.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).
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).
application-prod.yml with prod endpoints into the image couples build to environment and leaks hostnames.Actuator & Observability
Operations questions: health, metrics, traces. Deep dive: Actuator & Observability
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.
heapdump and env are data-leak vectors. Expose the minimum and never include=* on a public port.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.
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.
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.
@Async and thread pools drop it unless you use a context-propagating executor/task decorator.Caching & Resilience
Cache semantics and failure-handling patterns. Deep dive: Caching & Resilience
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.
Integer 1 vs Long 1), or mutable return objects. Also null results are cached unless unless="#result == null".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.
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.
sync=true only protects within a JVM - ten pods can still stampede together.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.
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.
Testing
Slices, mocks and the pitfalls that make tests slow or lying. Deep dive: Testing
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.
@WebMvcTest complains about a missing bean, you forgot a @MockitoBean/@MockBean for the controller's service.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.
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.
ON CONFLICT) are exactly what H2 hides.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.
entityManager.flush() and clear() to force SQL and a fresh first-level cache, or drop test-level @Transactional and clean up explicitly.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.
@WebMvcTest? CSRF is on and the request carries no token - add .with(csrf()) or disable CSRF for the stateless chain.Scenario & System Questions
Senior-level, open-ended questions: structure your answer, name the mechanism. Back to the Spring hub
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.
AopUtils.isAopProxy(this) / check the call stack for TransactionInterceptor.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.
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.
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).
@TransactionalEventListener(AFTER_COMMIT) publishing directly? A crash between commit and publish loses the event with no way to recover it.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.
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).
preStop sleep or readiness drain, and set terminationGracePeriodSeconds above the shutdown timeout.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.
Port 8080 was already in use, Failed to configure a DataSource: 'url' attribute is not specified, NoUniqueBeanDefinitionException.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.
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.
- ☐ 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
finalbreaks
- ☐ Decompose
@SpringBootApplication - ☐ Read a conditions report to find a missing bean
- ☐ Trace a request through
DispatcherServlet - ☐ Filter vs interceptor vs advice
- ☐
@Validvs@Validatedand the three exceptions
- ☐ Entity states and dirty checking
- ☐ N+1: reproduce, detect, three fixes
- ☐ Why join fetch +
Pageableis in memory - ☐ All seven propagation levels from memory
- ☐ Rollback rules and
UnexpectedRollbackException
- ☐ 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)
- ☐ Precedence ladder of property sources
- ☐
@Valuevs@ConfigurationProperties - ☐ Liveness vs readiness and why DB is not liveness
- ☐ Counter vs gauge vs timer; cardinality
- ☐ Graceful shutdown settings
- ☐ Circuit breaker states and default thresholds
- ☐ Cache stampede and
sync=truelimits - ☐ Slice test vs
@SpringBootTest - ☐
@MockitoBeanand context caching - ☐ Outbox pattern and idempotent consumers
Quick-Fire Table
The one-liners to have ready for the rapid-fire round. Each row links to the page that explains the mechanism.
| Question | Answer | Read |
|---|---|---|
| Default bean scope? | singleton, one per ApplicationContext | IoC & DI |
| Preferred injection style? | Constructor injection with final fields | IoC & DI |
| Ambiguous beans of one type? | @Primary or @Qualifier; else NoUniqueBeanDefinitionException | IoC & DI |
| When is @PostConstruct called? | After DI, in the BPP before-init phase, before the proxy exists | Scopes & Lifecycle |
| Are prototype beans destroyed by Spring? | No, @PreDestroy never fires | Scopes & Lifecycle |
| Default proxy type in Boot? | CGLIB (proxy-target-class=true) | AOP & Proxies |
| Why does @Transactional on this.other() fail? | Self-call bypasses the proxy | AOP & Proxies |
| Which file lists auto-configurations? | META-INF/spring/...AutoConfiguration.imports | Boot Internals |
| How to see why a bean was not created? | --debug conditions report or /actuator/conditions | Boot Internals |
| @RestController =? | @Controller + @ResponseBody | REST & MVC |
| Status for created resource? | 201 Created + Location header | REST & 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 projection | Spring Data JPA |
| Default of open-in-view? | true (disable it) | Spring Data JPA |
| Default rollback exceptions? | RuntimeException and Error only | Transactions |
| Default propagation? | REQUIRED | Transactions |
| REQUIRES_NEW needs? | A second connection from the pool | Transactions |
| Spring Security 6 config style? | SecurityFilterChain bean + lambda DSL | Security |
| 401 vs 403? | Not authenticated vs not allowed | Security |
| Disable CSRF when? | Stateless, header-token auth, no cookie credentials | Security |
| JWT must-check claims? | exp, nbf, iss, aud + signature with pinned alg | JWT & OAuth2 |
| Grant for service-to-service? | client_credentials | JWT & OAuth2 |
| Default authority prefix for scopes? | SCOPE_ | JWT & OAuth2 |
| Highest property precedence? | Command-line arguments | Configuration |
| Typed config binding? | @ConfigurationProperties (records, @Validated) | Configuration |
| DB check belongs in? | Readiness, not liveness | Actuator & Observability |
| Gauge vs counter? | Gauge goes up and down; counter only up | Actuator & Observability |
| Circuit breaker states? | CLOSED, OPEN, HALF_OPEN | Caching & Resilience |
| Cache stampede fix? | sync=true / single-flight, TTL jitter | Caching & Resilience |
| Controller-only test slice? | @WebMvcTest + @MockitoBean services | Testing |
| Real DB in tests? | Testcontainers + @ServiceConnection | Testing |
- Proxies explain most surprises:
@Transactional,@Cacheable,@Async,@PreAuthorizeall vanish on self-invocation,finalorprivatemethods. - 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.
- Spring hub - the full learning path.
- Transactions and AOP & Proxies - the two highest-yield pages for interviews.
- Spring Data JPA and Security - the other two most-asked areas.