Spring & Spring Boot Interview Questions
The Spring questions asked at every level answered with mechanisms: how auto-configuration decides, why self-invocation breaks @Transactional, proxy modes, bean scopes and testing slices.
On this page
Spring questions separate people who use the framework from people who understand it. The difference shows in one place: whether you can explain the mechanism rather than the annotation.
Key Takeaways
- Spring AOP works by proxying. Every surprising behaviour in
@Transactional,@Async,@Cacheableand@PreAuthorizefollows from that. - Self-invocation bypasses the proxy — the single most-asked Spring gotcha.
- Auto-configuration is conditional configuration classes discovered from the classpath.
@Transactionalrolls back on unchecked exceptions only, unless you say otherwise.- Injecting a prototype into a singleton gives you one instance forever.
Inversion of control, briefly
The container owns object creation and wiring; your code declares what it needs. The practical payoff is testability — a class taking its dependencies through a constructor can be instantiated in a unit test with fakes, and needs no framework at all.
@Service
public class OrderService {
private final OrderRepository repository; // final: cannot be reassigned
private final PaymentClient payments;
// No @Autowired needed on a single constructor since Spring 4.3.
public OrderService(OrderRepository repository, PaymentClient payments) {
this.repository = repository;
this.payments = payments;
}
}Three reasons constructor injection beats field injection, and they are all worth saying: the fields
can be final so the object is immutable and thread-safe; the class is usable without Spring, so unit
tests need no context; and a constructor with eight parameters is visibly a design problem, whereas
eight @Autowired fields hide it.
Bean lifecycle
1. Instantiate (constructor)
2. Populate properties / inject dependencies
3. *Aware callbacks: BeanNameAware, ApplicationContextAware
4. BeanPostProcessor.postProcessBeforeInitialization
5. @PostConstruct
6. InitializingBean.afterPropertiesSet
7. Custom init-method
8. BeanPostProcessor.postProcessAfterInitialization <- proxies are created HERE
9. ... bean is in use ...
10. @PreDestroy
11. DisposableBean.destroy
12. Custom destroy-methodStep 8 is the one that matters. AOP proxies — transactions, caching, security, async — are created by
a BeanPostProcessor after initialisation. That is why a @Transactional method called from
inside @PostConstruct is not transactional: the proxy does not exist yet.
The proxy, and self-invocation
@Service
public class OrderService {
public void processAll(List<String> ids) {
for (String id : ids) {
processOne(id); // `this.processOne` — the proxy is bypassed.
} // NO transaction. NO retry. NO caching.
}
@Transactional
public void processOne(String id) { ... }
}Three fixes, in order of preference:
@Service
public class OrderService {
private final OrderProcessor processor; // a different bean = a real proxy call
public void processAll(List<String> ids) { ids.forEach(processor::processOne); }
}@Service
public class OrderService {
@Lazy private final OrderService self; // @Lazy breaks the circular dependency
public void processAll(List<String> ids) { ids.forEach(self::processOne); }
}@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
// Weaves the advice into the bytecode, so self-invocation works — at the cost
// of a weaving agent and a more complex build.The same rule explains why @Transactional on a private, final or static method does nothing:
the proxy cannot override it.
@Transactional in depth
@Transactional(
propagation = Propagation.REQUIRED, // default: join, or start one
isolation = Isolation.READ_COMMITTED,
timeout = 10,
readOnly = false,
rollbackFor = Exception.class // by default, CHECKED exceptions do NOT roll back
)Rollback rules. By default Spring rolls back on RuntimeException and Error, and commits on
a checked exception. That surprises almost everyone the first time. If you throw a checked
InsufficientFundsException, the transaction commits unless you add rollbackFor.
Propagation. REQUIRED joins an existing transaction or starts one. REQUIRES_NEW suspends the
current one and starts an independent one — useful for audit records that must survive a rollback, and
a common source of connection-pool pressure, because it holds two connections at once.
NESTED uses a savepoint. MANDATORY throws if there is no transaction; NEVER throws if there is.
readOnly. Sets the JDBC connection read-only, which lets Hibernate skip dirty checking and lets a routing datasource send the query to a replica. Worth setting on every read path.
Auto-configuration
@AutoConfiguration
@ConditionalOnClass(DataSource.class) // is the class on the classpath?
@ConditionalOnMissingBean(DataSource.class) // did the user define their own?
@ConditionalOnProperty(prefix = "spring.datasource", name = "url")
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
DataSource dataSource(DataSourceProperties props) { ... }
}@SpringBootApplication implies @EnableAutoConfiguration, which loads candidate class names from
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports in every jar on the
classpath. Each candidate's conditions are evaluated at startup, and it applies only if they all pass.
That single mechanism explains the two behaviours people find magical: adding a dependency
configures things (a new class on the classpath satisfies @ConditionalOnClass), and defining your
own bean silently disables the default (@ConditionalOnMissingBean now fails).
java -jar app.jar --debug
# Prints the CONDITIONS EVALUATION REPORT: positive matches, negative
# matches with the exact condition that failed, and exclusions.Knowing that flag is a strong practical signal — it is how you debug "why is my bean not being created?" in thirty seconds instead of an hour.
Scopes
| Scope | One instance per |
|---|---|
singleton (default) | Container |
prototype | Injection or getBean call |
request | HTTP request |
session | HTTP session |
application | ServletContext |
@Service
public class ReportService {
// Injected ONCE when ReportService is created. The same instance forever.
private final ReportBuilder builder; // wrong for a prototype
}
@Service
public class ReportService {
private final ObjectProvider<ReportBuilder> builders; // a factory
public Report build() {
return builders.getObject().build(); // a new instance per call
}
}The alternative is a scoped proxy: @Scope(value = "prototype", proxyMode = TARGET_CLASS), which
injects a proxy that resolves a fresh instance on each method call.
Singleton beans must be stateless, because one instance serves every concurrent request. A mutable
field on a @Service is a data race — and, if it holds request data, a security bug.
Testing slices
@ExtendWith(MockitoExtension.class) // no Spring at all — milliseconds
class OrderServiceTest { }
@DataJpaTest // JPA, an in-memory or Testcontainers database, nothing else
class OrderRepositoryTest { }
@WebMvcTest(OrderController.class) // the web layer only; services are @MockBean
class OrderControllerTest { }
@SpringBootTest(webEnvironment = RANDOM_PORT) // the whole application — slow, use sparingly
class OrderIntegrationTest { }Two things worth saying about this. Slices exist because a full @SpringBootTest per test class means
a multi-second context start each time, and a suite of two hundred of them is unusable. And Spring
caches contexts by their configuration, so every distinct combination of @MockBeans or
properties creates another context — which is why a suite gets mysteriously slower as it grows.
The questions by level
Junior. IoC and dependency injection. Constructor versus field injection. What
@SpringBootApplication does. @Component versus @Service versus @Repository. Bean scopes.
Mid. The bean lifecycle. How auto-configuration works. @Transactional propagation and rollback
rules. Testing slices. Configuration properties and profiles.
Senior. The self-invocation problem and its fixes. Proxy modes (JDK dynamic versus CGLIB, and that Spring Boot defaults to CGLIB). Why a network call inside a transaction is dangerous. Context caching in tests. How you would write a starter.
The single highest-value thing to prepare is the proxy mechanism, because it is the honest answer to at least four separate questions.
Frequently Asked Questions
Why does calling a @Transactional method from another method in the same class do nothing?
How does Spring Boot auto-configuration actually work?
What happens if you inject a prototype bean into a singleton?
Related tutorials
- The Coding Round: Patterns That Keep Coming BackThe six patterns that cover most coding-screen questions, with Java templates, the language-specific traps that cost points, and how to talk while you code without losing your place.
- System Design for Java Backend EngineersA 45-minute structure that works: clarify and estimate, data model first, then the API, then scale what the numbers say to scale — plus idempotency, the outbox pattern and talking in numbers.
- The Behavioural Round: STAR Stories for EngineersWhy the behavioural round is scored harder than candidates expect, the six stories that cover almost every question, how to quantify impact honestly, and surviving the follow-up questions.
- The Eight-Week Preparation PlanA week-by-week plan that fits into eight hours a week: what to cover when, how to use spaced repetition on the topics you forget, when to start applying, and how to handle the offer stage.