Spring Boot interviews test both core Spring concepts (dependency injection, the bean lifecycle) and Boot-specific conventions like auto-configuration. These are the recurring questions across Java backend roles.
Plain Spring requires extensive XML or Java-based configuration to wire up beans, data sources, and web servers. Spring Boot provides auto-configuration (sensible defaults based on what's on the classpath), embedded servers (Tomcat/Jetty bundled in the JAR, no separate deployment needed), and starter dependencies (curated dependency bundles like spring-boot-starter-web) — dramatically reducing boilerplate so you can get a production-ready app running with minimal setup.
Dependency injection is a pattern where an object's dependencies are provided to it externally (by a container) rather than the object creating them itself, which decouples classes and makes them easier to test/mock. Spring's IoC container manages bean creation and injects dependencies via constructor injection, setter injection, or field injection (@Autowired), based on bean definitions discovered through component scanning or explicit configuration.
All four are specializations of @Component and get picked up the same way by component scanning, registering the class as a Spring-managed bean. The distinction is semantic/organizational: @Service marks business logic classes, @Repository marks data access classes (and additionally enables automatic exception translation for persistence exceptions), and @Controller (or @RestController) marks web layer classes that handle HTTP requests.
Spring instantiates a bean, injects its dependencies, then invokes any @PostConstruct-annotated method, followed by afterPropertiesSet() from InitializingBean if the bean implements it — both run, in that order, if both are present, completing initialization. The bean then lives in the application context until shutdown, at which point @PreDestroy-annotated methods run first, followed by destroy() from DisposableBean, giving you a chance to release resources like connections or thread pools before the context closes.
@RequestParam extracts a query string parameter (e.g., ?id=5) or form field. @PathVariable extracts a value embedded in the URL path itself (e.g., /users/{id}). @RequestBody deserializes the entire HTTP request body (typically JSON) into a Java object, used for POST/PUT endpoints receiving structured payloads.
By extending an interface like JpaRepository<Entity, IdType>, you get standard CRUD operations (save, findById, delete, etc.) implemented automatically at runtime via a dynamic proxy — no implementation code needed. You can also define query methods by naming convention alone (e.g., findByEmailAndStatus) or use @Query for custom JPQL/native SQL when the naming convention isn't expressive enough.
REQUIRED (the default) joins an existing transaction if one is already active on the calling thread, or starts a new one if none exists — so a rollback in the inner method also rolls back the outer transaction. REQUIRES_NEW always suspends any existing transaction and starts a fresh, independent one, meaning a failure in the inner transaction doesn't automatically roll back the outer one — commonly used for things like audit logging that should persist even if the main operation later fails.
Profiles let you maintain environment-specific configuration (dev, test, prod) and activate only the relevant set at runtime via spring.profiles.active or an environment variable, letting you swap things like database URLs or logging levels without code changes. You define profile-specific properties in files like application-dev.properties/yml, which override the base application.properties/yml when that profile is active. application.properties uses flat dot-separated keys (e.g., spring.datasource.url=...), while application.yml uses nested, indentation-based YAML syntax that's more compact for hierarchical configuration and supports multiple profiles in a single file separated by --- documents. Functionally they're equivalent — Spring binds both to the same Environment abstraction — so the choice is mostly team preference, though YAML doesn't support the @PropertySource annotation for loading it directly on plain Java config classes.
@ExceptionHandler methods can be defined inside a controller to catch specific exception types thrown by that controller's handlers and return a custom response. To centralize this across the whole application rather than repeating it in every controller, you annotate a class with @ControllerAdvice (or @RestControllerAdvice for REST APIs, which adds @ResponseBody implicitly), and it intercepts exceptions from any controller. Inside it, each @ExceptionHandler method maps a specific exception type to an HTTP status and response body, typically returning a consistent error structure like a ProblemDetail or custom ErrorResponse DTO with fields like status, message, and timestamp. This keeps error-handling logic out of business code and ensures consistent, predictable error responses across the API.
Spring Security intercepts every incoming request through a chain of servlet filters registered before your DispatcherServlet, with each filter responsible for one concern — for example, UsernamePasswordAuthenticationFilter handles form login, and BasicAuthenticationFilter handles HTTP Basic auth. Authentication establishes who the user is: a filter extracts credentials, passes them to an AuthenticationManager, which delegates to an AuthenticationProvider that verifies them against a UserDetailsService and password encoder, and on success populates the SecurityContext with an authenticated principal. Authorization then decides what that authenticated principal can do, enforced either via URL-based rules in a SecurityFilterChain bean (authorizeHttpRequests) or method-level annotations like @PreAuthorize. If authentication fails or is missing for a protected resource, the chain short-circuits and returns 401/403 before the request ever reaches your controller.
Singleton, the default scope, means Spring creates exactly one instance of the bean per application context and returns that same shared instance for every injection point — this works fine for stateless beans like services and repositories. Prototype scope creates a brand-new instance every time the bean is requested or injected, which matters when a bean holds mutable, request-specific state that shouldn't be shared. A subtlety is that Spring only manages a prototype bean's creation and initialization, not its destruction — @PreDestroy won't be called automatically, so you're responsible for cleanup if needed. Other scopes like request and session exist too, scoping a bean's lifetime to an HTTP request or session in web applications, but singleton and prototype are the two you'll be asked about most.
Constructor injection makes dependencies explicit and mandatory — the class simply cannot be instantiated without them, which surfaces missing dependencies at compile time or context-startup time rather than a NullPointerException at runtime. It also allows dependencies to be declared final, making the class immutable and inherently easier to reason about and thread-safe. Field injection with @Autowired is more concise but hides the dependency graph, makes the class harder to unit test without a Spring context or reflection-based mocking, and permits circular dependencies to compile even though they're a design smell. As of recent Spring versions, if a class has exactly one constructor, @Autowired on it isn't even required — Spring infers it automatically — which is another reason it's now the recommended default over field or setter injection.
Actuator adds production-ready monitoring and management endpoints to your application, exposed under /actuator, covering things like /actuator/health, /actuator/metrics, /actuator/info, and /actuator/env. The /health endpoint aggregates status from HealthIndicator beans — built-in ones check things like database connectivity, disk space, and message broker reachability, and you can register custom HealthIndicators for your own dependencies. Each indicator reports UP, DOWN, or OUT_OF_SERVICE, and the aggregate endpoint rolls these up into an overall status, which is what load balancers, Kubernetes liveness/readiness probes, or orchestration tooling typically poll to decide whether to route traffic to an instance. By default only a limited set of endpoints and details are exposed publicly; you configure exposure and sensitive-detail visibility via management.endpoints.web.exposure.include and security rules, since these endpoints can leak internal information if left wide open.
The N+1 problem happens when you fetch a list of N parent entities with one query, and then, because an associated collection or relation is lazily loaded, Hibernate issues one additional query per parent to fetch that association — resulting in 1 + N queries instead of one efficient query, which becomes a serious performance problem as N grows. It's most common with @OneToMany or @ManyToMany relationships accessed in a loop after the initial query, especially inside a view layer that's still within the persistence context. You avoid it by explicitly fetching the association eagerly for that specific query, either with a JPQL fetch join (SELECT p FROM Parent p JOIN FETCH p.children), a Spring Data @EntityGraph on the repository method, or a batch-fetching strategy (@BatchSize) that groups the N follow-up queries into fewer IN-clause queries. The fix should be applied per-query rather than by globally switching the association to EAGER fetch type, since that just moves the problem to every query that loads that entity, whether it needs the association or not.
Bean Validation (JSR 380, implemented by Hibernate Validator) lets you annotate fields on a DTO with constraints like @NotNull, @NotBlank, @Size, @Email, or @Min/@Max. When a controller method parameter representing a request body is annotated with @Valid (or @Validated for group-based validation), Spring runs these constraints automatically before the method body executes, and if any fail, it throws a MethodArgumentNotValidException rather than letting invalid data reach your business logic. That exception carries all the individual field errors, which you typically catch in a @ControllerAdvice exception handler and convert into a structured 400 Bad Request response listing which fields failed and why. You can also define custom constraint annotations by implementing ConstraintValidator when the built-in constraints aren't expressive enough, such as validating that a password meets a complex policy.
A circular dependency happens when Bean A requires Bean B during construction and Bean B simultaneously requires Bean A, which is a problem for constructor injection because Spring can't fully construct either one first — it needs a complete instance of the other before it can finish. With field or setter injection, Spring can sometimes work around it by injecting a partially-initialized, proxy-backed reference, but this is a fragile workaround rather than a real fix, and Spring Boot 2.6+ disables it by default, failing startup with a BeanCurrentlyInCreationException unless you explicitly opt back in via spring.main.allow-circular-references=true. The better fix is almost always to break the cycle by refactoring: extract the shared logic both beans need into a third bean that each depends on, or use setter/method injection for one side combined with @Lazy so that one dependency is resolved after both beans exist. In most cases a circular dependency is a sign of poor separation of concerns between the two classes rather than something to route around technically.
AOP lets you modularize cross-cutting concerns — logic like logging, security checks, caching, or transaction management that would otherwise be scattered and duplicated across many unrelated classes — into a single reusable module called an aspect. You define an aspect with @Aspect on a class, and within it, advice methods annotated with @Before, @After, @AfterReturning, @AfterThrowing, or @Around are triggered at matching join points defined by a pointcut expression (e.g., matching all public methods in a package or annotated with a custom annotation). Spring implements this using proxies — either JDK dynamic proxies for interface-based beans or CGLIB subclass proxies otherwise — which wrap the target bean and run the advice logic around the actual method call. @Transactional itself is a well-known example of Spring AOP in action: the annotation triggers a proxy that opens and commits/rolls back a transaction around the annotated method, without that logic being written in your business code.
Auto-configuration is driven by classes annotated with @AutoConfiguration (previously registered via a spring.factories file, now via META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports), which Spring Boot loads and conditionally applies at startup based on what's on the classpath and already defined in the context. Each auto-configuration class is guarded by conditional annotations like @ConditionalOnClass (only apply if a given class is present, e.g., only configure a DataSource if a JDBC driver is on the classpath), @ConditionalOnMissingBean (only apply if you haven't already defined your own bean of that type, so your explicit configuration always wins), and @ConditionalOnProperty (only apply if a property is set to a certain value). This is why adding spring-boot-starter-data-jpa alone is enough to get a configured EntityManagerFactory, DataSource, and transaction manager — Boot detects the JPA and driver classes on the classpath and wires sensible defaults, but backs off immediately the moment you define your own equivalent bean. @SpringBootApplication itself is a meta-annotation that bundles @EnableAutoConfiguration (which kicks this whole mechanism off) together with @ComponentScan and @Configuration.