Spring Boot
Auto-configuration and conditional beans, Actuator, Spring MVC vs WebFlux, the Spring Security filter chain, and centralised REST exception handling.
9 questions
JuniorTheoryVery commonWhat three annotations does @SpringBootApplication bundle together?
What three annotations does @SpringBootApplication bundle together?
@SpringBootApplication is a meta-annotation bundling three: @SpringBootConfiguration (a @Configuration that marks the class as a bean-definition source), @EnableAutoConfiguration (which turns on Boot's auto-configuration), and @ComponentScan (which scans the class's own package and below for beans). One annotation on the main class replaces declaring all three.
Common mistakes
- ✗Thinking
@SpringBootApplicationbundles the stereotype annotations like@Service - ✗Believing component scanning covers the whole classpath rather than the main class's package downward
- ✗Assuming auto-configuration overrides beans you have already defined yourself
Follow-up questions
- →Why is the main class's package location significant for component scanning?
- →How can you exclude a specific auto-configuration class from being applied?
JuniorTheoryCommonWhat does Spring Boot Actuator provide, and what is exposed over HTTP by default?
What does Spring Boot Actuator provide, and what is exposed over HTTP by default?
Actuator adds ready-made management endpoints under /actuator — health, metrics, env, loggers, threaddump — as soon as its starter is on the classpath. Enabled is not the same as exposed: every endpoint except shutdown is enabled, but only health is served over HTTP by default, and the rest are opted in through management.endpoints.web.exposure.include.
Common mistakes
- ✗Assuming every Actuator endpoint is reachable over HTTP by default
- ✗Confusing an endpoint being enabled with it being exposed to the web
- ✗Expecting
shutdownto work without enabling it explicitly
Follow-up questions
- →Why is exposing
envorheapdumpon a public port dangerous? - →How would you add a custom health indicator to
/actuator/health?
MiddleDesignCommonYou are reviewing a REST service before an external partner integrates with it. Every controller wraps its body in try/catch, each one returns a different error JSON shape, a bean-validation failure surfaces as 500 with a Hibernate stack trace in the body, and a not-found from the service layer comes back as 200 with a null payload. The review demands one error shape for the whole API, a status that matches each failure class, no internal detail in responses, and no handling duplicated across controllers — while one legacy endpoint has to keep its old error body for a client that cannot be changed. How would you restructure exception handling, and where does the mapping from an exception to a status live?
You are reviewing a REST service before an external partner integrates with it. Every controller wraps its body in try/catch, each one returns a different error JSON shape, a bean-validation failure surfaces as 500 with a Hibernate stack trace in the body, and a not-found from the service layer comes back as 200 with a null payload. The review demands one error shape for the whole API, a status that matches each failure class, no internal detail in responses, and no handling duplicated across controllers — while one legacy endpoint has to keep its old error body for a client that cannot be changed. How would you restructure exception handling, and where does the mapping from an exception to a status live?
Controllers stop catching and just throw; one @RestControllerAdvice holds @ExceptionHandler methods mapping each exception class to a status and one body shape (ProblemDetail): validation to 400, not-found to 404, a catch-all Exception to 500 with a generic message, the stack trace going only to the log. A controller-local @ExceptionHandler beats the global advice, so the legacy endpoint keeps its old body.
Common mistakes
- ✗Leaving try/catch in every controller instead of throwing and handling centrally
- ✗Returning 200 with an error payload rather than a status that matches the failure
- ✗Putting the stack trace or SQL text into the response body instead of the log
Follow-up questions
- →How does Spring choose between two
@ExceptionHandlermethods that both match a thrown exception? - →Why does an exception thrown in a servlet
Filternever reach@ControllerAdvice?
MiddleDesignCommonYour team is picking the stack for a new edge service before it ships. Each incoming request fans out to four downstream HTTP APIs and returns an aggregate; the downstreams answer in 200-800 ms, and the service must hold about 10000 requests in flight on a small container. The rest of the platform is Spring MVC on Tomcat, one legacy call goes through a blocking JDBC driver that cannot be replaced this quarter, and nobody on the team has shipped reactive code before. Would you build it on Spring MVC or Spring WebFlux? Explain what each does differently with threads under this load, and name what would force the decision the other way.
Your team is picking the stack for a new edge service before it ships. Each incoming request fans out to four downstream HTTP APIs and returns an aggregate; the downstreams answer in 200-800 ms, and the service must hold about 10000 requests in flight on a small container. The rest of the platform is Spring MVC on Tomcat, one legacy call goes through a blocking JDBC driver that cannot be replaced this quarter, and nobody on the team has shipped reactive code before. Would you build it on Spring MVC or Spring WebFlux? Explain what each does differently with threads under this load, and name what would force the decision the other way.
MVC is thread-per-request: 10000 calls in flight need 10000 blocked Tomcat threads, so memory and context switching set the ceiling. WebFlux serves them from a few event-loop threads freed during I/O — but only if the whole chain is non-blocking (WebClient), since one blocking JDBC call on an event-loop thread stalls every request sharing it. With no reactive experience and that driver in the path, MVC on virtual threads is the safer pick.
Common mistakes
- ✗Expecting WebFlux to scale while a blocking JDBC call still runs on an event-loop thread
- ✗Treating WebFlux as a drop-in speed-up rather than an end-to-end programming model
- ✗Believing a reactive
WebClientinside an MVC controller frees the Tomcat thread
Follow-up questions
- →How do virtual threads change the thread-per-request ceiling in Spring MVC?
- →How would you safely call one blocking legacy API from a WebFlux handler?
MiddleTheoryCommonHow does a request travel through the Spring Security filter chain?
How does a request travel through the Spring Security filter chain?
Spring Security plugs one servlet filter (DelegatingFilterProxy to FilterChainProxy) in front of DispatcherServlet. FilterChainProxy picks the first SecurityFilterChain whose matcher fits and runs its ordered filters: context loading, CsrfFilter, an authentication filter, then AuthorizationFilter. Any filter may short-circuit the request; the result lands in SecurityContextHolder, so a rejected request never reaches a controller.
Common mistakes
- ✗Thinking security runs after
DispatcherServlethas already picked a handler - ✗Assuming every declared
SecurityFilterChainis applied instead of the first matching one - ✗Forgetting that the filter order is what puts authentication before authorization
Follow-up questions
- →Why does an exception thrown inside a servlet
Filternever reach@ControllerAdvice? - →What does
ExceptionTranslationFilterdo with anAccessDeniedException?
MiddleTheoryOccasionalHow does Spring Boot decide whether an auto-configuration class contributes its beans?
How does Spring Boot decide whether an auto-configuration class contributes its beans?
Auto-configuration classes are listed in AutoConfiguration.imports, and each is guarded by conditions — @ConditionalOnClass, @ConditionalOnProperty, @ConditionalOnMissingBean — evaluated at context startup, not at build time. Auto-configuration is applied after your own configuration, so @ConditionalOnMissingBean already sees your bean and backs off: your definition wins. --debug prints the condition report.
Common mistakes
- ✗Thinking auto-configuration overrides a bean you defined yourself
- ✗Believing the conditions are checked at build time rather than at context startup
- ✗Assuming a starter on the classpath always contributes its beans whatever the properties say
Follow-up questions
- →How do you exclude a single auto-configuration class from the context?
- →Why does the order of your configuration against auto-configuration matter for
@ConditionalOnMissingBean?
SeniorDesignOccasionalYour team is split on how a Spring Boot API should authenticate now that it runs on eight instances behind a load balancer. Today it issues an in-memory HttpSession cookie and the balancer is configured for sticky sessions; a browser SPA and a mobile client are both being added. One camp wants stateless JWTs so any instance can serve any request with no shared store. The other points out that security has signed off on a rule that a banned user or a password change must take effect within seconds, and that nobody owns a signing-key rotation process. Argue the trade-off: what does each option actually cost here, and what would you ship?
Your team is split on how a Spring Boot API should authenticate now that it runs on eight instances behind a load balancer. Today it issues an in-memory HttpSession cookie and the balancer is configured for sticky sessions; a browser SPA and a mobile client are both being added. One camp wants stateless JWTs so any instance can serve any request with no shared store. The other points out that security has signed off on a rule that a banned user or a password change must take effect within seconds, and that nobody owns a signing-key rotation process. Argue the trade-off: what does each option actually cost here, and what would you ship?
Where the auth state lives decides revocation. A session keeps it on the server: revoking is one delete in the store, instant — but eight instances need a shared store (Spring Session with Redis), not sticky sessions. A JWT is verified by signature on any instance, yet stays valid until it expires, so a seconds-level ban forces a blocklist or short TTLs — the store you avoided. Ship shared sessions.
Common mistakes
- ✗Believing an issued JWT can be revoked without any server-side lookup
- ✗Assuming a JWT is encrypted rather than merely signed and readable by the client
- ✗Treating sticky sessions as the only way to run sessions on more than one instance
Follow-up questions
- →How short must an access-token TTL be before a blocklist stops being necessary?
- →What breaks in a mobile client if you keep authentication in a cookie?
SeniorDesignOccasionalAfter a release, a request carrying a tenant header intermittently comes back as 403 before any controller code runs, and the audit log — implemented as a servlet Filter — records neither the authenticated user nor the handler method that served the request. Exceptions thrown from that same filter never reach the team's @RestControllerAdvice either, surfacing instead as a raw Tomcat error page. Walk the path a request takes from the embedded Tomcat connector through Spring MVC to a serialized JSON response, say what each stage owns, and use that to place four cross-cutting concerns — authentication, tenant resolution, audit and error mapping — where they actually belong.
After a release, a request carrying a tenant header intermittently comes back as 403 before any controller code runs, and the audit log — implemented as a servlet Filter — records neither the authenticated user nor the handler method that served the request. Exceptions thrown from that same filter never reach the team's @RestControllerAdvice either, surfacing instead as a raw Tomcat error page. Walk the path a request takes from the embedded Tomcat connector through Spring MVC to a serialized JSON response, say what each stage owns, and use that to place four cross-cutting concerns — authentication, tenant resolution, audit and error mapping — where they actually belong.
A Tomcat worker thread builds the HttpServletRequest and runs the servlet filter chain — Spring Security lives there, so its 403 lands before any controller. The chain ends at DispatcherServlet: HandlerMapping resolves the handler and its interceptors, a HandlerAdapter calls the method, an HttpMessageConverter serializes the result. @ControllerAdvice runs inside DispatcherServlet, so a filter knows no handler and its exceptions escape it.
Common mistakes
- ✗Believing
@ControllerAdvicecan catch an exception thrown by a servletFilter - ✗Expecting a filter to see the handler method or the resolved
Authentication - ✗Mixing up filters (outside
DispatcherServlet) with interceptors (inside it)
Follow-up questions
- →Which stage would you put tenant resolution in so both the audit and the controller can read it?
- →How does
HandlerExceptionResolverroute a thrown exception to@ControllerAdvice?
SeniorPerformanceRareA Spring Boot service takes 40 s to start, which cripples autoscaling. What dominates that time, and how would you cut it?
A Spring Boot service takes 40 s to start, which cripples autoscaling. What dominates that time, and how would you cut it?
Measure first: /actuator/startup times each step and --debug prints the condition report. The time goes into classpath scanning, evaluating conditions over every auto-configuration candidate, and eagerly creating singletons — so shrink that set: drop unused starters, narrow the scanning, exclude auto-configurations you do not need. Lazy init only trades startup for first-request latency; AOT with CDS or a native image moves the work to build time.
Common mistakes
- ✗Blaming JVM warm-up rather than context construction for a slow startup
- ✗Treating lazy initialisation as free instead of a shift of cost onto the first request
- ✗Optimising before measuring, so the starters that dominate the time are never found
Follow-up questions
- →What exactly does lazy initialisation postpone, and what does it break at runtime?
- →How does an AOT-processed or native build remove condition evaluation from startup?