Spring Interview Q&A
30+ questions with concise, production-flavoured answers. Each answer is 3-5 sentences โ the kind that signals real experience, not textbook recall.
IoC means you describe what you need and the container provides it โ you don't create dependencies yourself.
The Spring IoC container (ApplicationContext) creates, wires, and manages the lifecycle of all beans.
This gives you testability (swap implementations via constructor injection), loose coupling (depend on interfaces, not concrete classes),
and eliminates manual object graph construction. Without IoC, changing one dependency means editing every class that creates it.
Constructor injection, setter injection, and field injection. Constructor injection is always preferred โ
it produces immutable objects (final fields), makes all required dependencies explicit in one place,
and allows testing with plain new without a Spring context. Setter injection is acceptable for optional dependencies.
Field injection (@Autowired on fields) hides dependencies and makes unit testing require the full Spring context โ avoid in production code.
They're all specialisations of @Component โ Spring scans and registers them as beans.
@Service marks business logic (no extra behaviour, just semantic clarity).
@Repository adds persistence exception translation โ Hibernate exceptions get wrapped into Spring's DataAccessException hierarchy.
@Controller marks MVC controllers. Functionally, you could use @Component everywhere, but the specialisations communicate intent and enable layer-specific features.
Scope controls how many instances the container creates. Singleton (default) โ one instance per container, shared everywhere.
Prototype โ new instance every time the bean is requested. Singletons are created at startup and cached; prototypes are created on demand and not managed after creation
(no @PreDestroy callback). Use singleton for stateless services, prototype for stateful short-lived objects.
The singleton gets one instance of the prototype at injection time โ and holds that same reference forever.
Subsequent calls don't create a new prototype. This defeats the purpose of prototype scope.
Fix: use ObjectFactory<T>, Provider<T>, or @Lookup method to get a fresh instance on each call.
Alternatively, inject ApplicationContext and call getBean() โ but that's service locator, which is less clean.
A BeanPostProcessor intercepts every bean's creation โ it has postProcessBeforeInitialization and
postProcessAfterInitialization hooks. Spring uses this internally to create AOP proxies, process @Autowired,
and handle @Value injection. You'd write a custom one for cross-cutting concerns like wrapping every bean with a logging proxy
or validating annotations. In practice, most developers never need to write one โ but understanding it explains how @Transactional gets its proxy.
When a method in a bean calls another method in the same bean, it bypasses the Spring proxy.
So if method A calls method B (which has @Transactional), the transaction annotation is ignored โ the call goes
directly to the target, not through the proxy. Fix: extract method B into a separate bean so the call crosses the proxy boundary.
Alternatively, inject the bean into itself (self-reference) so the call goes through the proxy. This affects @Transactional, @Cacheable, @Async, and any AOP-based annotation.
Spring Framework is the core library โ IoC, DI, AOP, MVC, Data, Security. You configure everything yourself. Spring Boot is an opinionated layer on top that auto-configures beans based on classpath scanning, provides starter dependencies (curated BOMs), and embeds a web server. Boot doesn't replace the framework โ it removes the boilerplate of setting it up. The key shift: you override defaults instead of building from scratch.
Spring Boot scans META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports for auto-config classes.
Each class uses @ConditionalOnClass (is the library on the classpath?) and @ConditionalOnMissingBean (has the user already defined one?).
To override: just declare your own @Bean of the same type โ the auto-config backs off because @ConditionalOnMissingBean fails.
Debug with --debug flag to see the Conditions Evaluation Report.
A starter is a BOM (bill of materials) โ it pulls in a curated set of dependencies at compatible versions. No code of its own.
spring-boot-starter-web brings: Spring MVC, Jackson (JSON serialisation), embedded Tomcat, Bean Validation (Hibernate Validator),
and the web auto-configuration that sets up DispatcherServlet, message converters, and error handling. One dependency replaces a dozen manual ones.
Use application.yml for base config, application-{profile}.yml for environment overrides (dev, staging, prod).
Activate with spring.profiles.active โ set via environment variable in production, never hardcoded.
Secrets go in environment variables or a vault (AWS Secrets Manager, HashiCorp Vault) โ never committed to git.
@ConfigurationProperties with @Validated catches missing config at startup.
It's a meta-annotation combining three annotations: @SpringBootConfiguration (marks it as a configuration class),
@EnableAutoConfiguration (triggers classpath-based auto-config), and @ComponentScan (scans the package and sub-packages for Spring components).
The component scan base package defaults to the package of the annotated class โ this is why your main class should sit at the root package.
Create a @Configuration class with @ConditionalOnClass / @ConditionalOnMissingBean guards.
Register it in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
Publish as a separate module (typically a starter). This is how libraries like Spring Data, Spring Security, and third-party libraries
integrate seamlessly โ they ship auto-config classes that activate when their jars land on the classpath.
The persistence context is a first-level cache that tracks all managed entities within a transaction. When you load an entity, it's "managed" โ JPA tracks changes and flushes them to the DB at commit time (dirty checking). Loading the same entity twice in one transaction returns the same Java object (identity guarantee). It's essentially the unit of work pattern: accumulate changes in memory, write them all at once.
Transient โ new object, not associated with a persistence context.
Managed โ associated with a context, changes are automatically tracked.
Detached โ was managed but the context is closed (after transaction ends or clear()).
Removed โ scheduled for deletion. Calling save() on a detached entity reattaches it (merge).
Understanding these states explains why lazy loading throws LazyInitializationException outside a transaction.
Loading a list of N parent entities triggers 1 query, then N additional queries to load each parent's children (lazy-loaded collections accessed in a loop).
Three fixes: JOIN FETCH in JPQL (SELECT o FROM Order o JOIN FETCH o.items) โ one query, use DISTINCT to avoid duplicates.
@EntityGraph โ declarative, annotation on the repository method. @BatchSize(size=50) โ reduces queries to N/50 instead of N.
Detect with spring.jpa.show-sql=true and count queries, or use p6spy.
EAGER loads the related entity immediately with the parent query (JOIN or subselect). LAZY loads it on first access (triggers a separate query).
Default: @OneToMany and @ManyToMany are LAZY. @ManyToOne and @OneToOne are EAGER โ this is the dangerous default.
Always override @ManyToOne to LAZY. EAGER fetching can't be turned off at query time; LAZY fetching can be upgraded to EAGER with JOIN FETCH when needed.
It hints to the persistence provider that no writes will occur. Hibernate skips dirty checking at flush time (no need to compare entity snapshots). The JDBC driver may route to a read replica if the DataSource supports it. The database transaction may use a lighter isolation mode. Put it on all read-only service methods โ it's a free performance optimisation with no downside.
REQUIRED (default) โ join the existing transaction or create one. This covers 95% of cases.
REQUIRES_NEW โ always starts a new transaction, suspending any existing one. Use for audit logging that must persist even if the outer transaction rolls back.
MANDATORY โ must run inside an existing transaction, throws otherwise. Useful for repository methods that should never be called without a service-layer transaction.
The other propagation types (SUPPORTS, NOT_SUPPORTED, NEVER, NESTED) are rarely used in practice.
save() persists or merges the entity into the persistence context โ the actual SQL may not execute immediately (Hibernate batches writes until flush time).
saveAndFlush() does the same but forces an immediate flush to the database. Use saveAndFlush() when you need the DB-generated ID immediately,
or when subsequent code depends on the row being visible to other transactions or native queries. In most cases, save() is sufficient.
Derived method names (findByEmailAndStatus) are great for simple queries with 1-2 conditions.
Beyond that, they become unreadable (findByStatusAndCreatedAtAfterAndCategoryIn).
Use @Query with JPQL for complex joins and conditions. Use native SQL when you need DB-specific features (window functions, CTEs, full-text search)
or when hand-tuned SQL significantly outperforms generated JPQL. Specifications are the better choice for dynamic search filters.
The front controller for Spring MVC โ every HTTP request passes through it. It consults HandlerMapping to find the right controller method,
invokes it, then uses HttpMessageConverter (Jackson for JSON) to serialise the response.
It also handles view resolution, locale, and multipart file uploads. Spring Boot auto-configures it โ you never create it manually.
@RestController = @Controller + @ResponseBody. With @Controller, return values are interpreted as view names (Thymeleaf templates, JSP pages).
With @RestController, return values are serialised directly to the HTTP response body via Jackson.
For REST APIs, always use @RestController. For server-side rendered HTML, use @Controller.
Create a @ControllerAdvice class with an @ExceptionHandler(MethodArgumentNotValidException.class) method.
Extract field errors from the exception, map them to a structured error response DTO (timestamp, status, field โ message pairs),
and return ResponseEntity<ErrorResponse> with 400 status. This intercepts all validation failures across all controllers in one place.
Spring Boot 3.x also supports RFC 7807 Problem Details natively via ProblemDetail.
@ControllerAdvice is a special @Component that provides global exception handling, model attributes,
and data binding across all controllers. Its @ExceptionHandler methods catch exceptions thrown by any controller โ
specific handlers win over general ones. You can scope it to specific packages or controller classes.
Gotcha: if it's outside the component scan package, it's silently ignored.
Entities are a DB concern; DTOs are an API concern. Exposing entities couples your API shape to your DB schema โ renaming a column breaks the API. Entities may contain fields you don't want to expose (passwords, internal IDs, audit fields). Lazy-loaded collections on entities can trigger N+1 queries during JSON serialisation. DTOs let you evolve API and DB independently, include computed fields, and control exactly what's serialised.
Content negotiation determines the response format (JSON, XML, etc.) based on the Accept header, URL suffix, or request parameter.
Spring MVC uses HttpMessageConverter instances โ Jackson's MappingJackson2HttpMessageConverter handles JSON by default.
If you add jackson-dataformat-xml to the classpath, XML is also supported automatically.
produces on @GetMapping restricts which formats an endpoint supports.
Every HTTP request passes through an ordered chain of security filters before reaching the controller.
Key filters: SecurityContextPersistenceFilter (loads/stores auth state), authentication filters (validate credentials),
ExceptionTranslationFilter (converts auth exceptions to HTTP responses), and FilterSecurityInterceptor (final access decision).
Each filter can short-circuit the chain. You configure the chain via a SecurityFilterChain bean โ the modern replacement for WebSecurityConfigurerAdapter.
Authentication answers "who are you?" โ it produces an Authentication object stored in the SecurityContext.
Authorisation answers "what are you allowed to do?" โ it checks roles/authorities against the requested resource.
Authentication happens first (filter chain validates credentials); authorisation happens second (access rules evaluated).
A 401 means "not authenticated"; a 403 means "authenticated but not authorised."
Two phases. Login: client POSTs credentials to an auth endpoint, which validates them and returns a signed JWT containing user claims (sub, roles, exp).
Subsequent requests: client sends the JWT in the Authorization: Bearer header. A custom OncePerRequestFilter extracts the token,
validates the signature and expiry, extracts claims, creates an Authentication object, and sets it in the SecurityContext.
No session lookup needed โ the token is self-contained. This is why you set session management to STATELESS.
hasRole("ADMIN") automatically prepends ROLE_ โ so it checks for the authority ROLE_ADMIN.
hasAuthority("ROLE_ADMIN") checks the exact string. They're functionally equivalent if you're consistent.
The common mistake: storing ROLE_ADMIN in the database and then checking hasRole("ROLE_ADMIN") โ this looks for ROLE_ROLE_ADMIN.
Pick one convention and stick with it. Most teams use hasRole() and store authorities without the ROLE_ prefix.
CSRF protection defends against forged requests that exploit session cookies โ the browser automatically sends cookies with every request to the domain.
Stateless REST APIs use JWT in the Authorization header, not cookies. Since the browser doesn't automatically attach the header, CSRF attacks aren't possible.
Leaving CSRF enabled on a stateless API causes 403 errors on every POST, PUT, and DELETE.
Disable with csrf(AbstractHttpConfigurer::disable) in your SecurityFilterChain.
OAuth2 Client (spring-boot-starter-oauth2-client): your app IS the client โ it redirects users to an authorization server (Google, Keycloak) for login
and receives tokens back. Used for "Login with Google" flows.
Resource Server (spring-boot-starter-oauth2-resource-server): your API validates JWTs that another service issued.
The client already has the token; your API just verifies it via the JWKS endpoint.
Most backend APIs are resource servers; frontend apps or BFF layers are OAuth2 clients.
Expose only what you need: management.endpoints.web.exposure.include=health,info,metrics.
Never expose /actuator/env or /actuator/shutdown publicly โ they leak secrets or allow remote shutdown.
Secure actuator endpoints with Spring Security โ require authentication for sensitive endpoints, allow /health unauthenticated for load balancer checks.
Run actuator on a separate management port (management.server.port) so it's only reachable from internal networks.
A circuit breaker monitors calls to an external service. After a threshold of failures, it trips "open" and fails fast without making the call โ preventing resource exhaustion. After a wait interval, it moves to "half-open" and allows test calls. If they succeed, it closes; if they fail, it reopens. Use it on any external HTTP call โ third-party APIs, downstream microservices, database connections. Resilience4j is the standard library in Spring Boot. Combine with retry (for transient failures) and bulkhead (for thread isolation).
@Cacheable checks the cache first โ if a value exists for the key, it returns it without executing the method.
@CachePut always executes the method and updates the cache with the result.
Use @Cacheable on read methods, @CachePut on write methods where you want to update the cached value after a save.
@CacheEvict is more common for writes โ it removes the entry so the next read fetches fresh data.
All three are AOP-based, so the self-invocation caveat applies.
Three layers. Unit tests with Mockito (@ExtendWith(MockitoExtension.class)) โ no Spring context, fast, test service logic and pure functions.
Slice tests โ @WebMvcTest for controllers (validation, error handling), @DataJpaTest for repositories (queries, entity mapping with H2).
Integration tests with @SpringBootTest and Testcontainers for end-to-end flows against real databases.
Slice tests are the sweet spot โ fast and focused. Reserve @SpringBootTest for critical paths.
Use @MockBean to replace dependencies in the Spring context, @Mock for pure Mockito unit tests.