Spring Framework
Spring Essentials Overview
Spring Framework Overview
The Spring Framework is an open-source, modular framework for building Java applications. It provides infrastructure for common application concerns so that developers can focus on business logic rather than low-level plumbing.
Its core principle is to let application code remain composed of plain Java objects (POJOs). Spring connects those objects and supplies infrastructure through configuration, dependency injection, and abstractions over common Java APIs.
Major areas of the framework include:
- Core container: dependency injection, bean creation, configuration, and lifecycle management
- Aspect-oriented programming: reusable handling of cross-cutting concerns
- Data access: JDBC support, ORM integration, and transaction management
- Web: Spring MVC and the reactive Spring WebFlux stack
- Integration: events, messaging, scheduling, caching, and other enterprise services
- Testing: mock objects and integration with the Spring TestContext Framework
Three terms in that list are worth defining up front:
- JDBC (Java Database Connectivity): Java’s standard API for executing SQL and working with relational databases.
- ORM (object-relational mapping): maps Java objects and their relationships to relational database tables; JPA and Hibernate are common examples.
- Spring WebFlux: Spring’s reactive web framework for non-blocking request processing and asynchronous data streams. Spring MVC is the traditional Servlet-based alternative.
The terms Spring, Spring Framework, and Spring Boot are related but not interchangeable:
- Spring Framework is the foundation that provides the container and core programming model.
- Spring can refer to the entire ecosystem, including projects such as Spring Data and Spring Security.
- Spring Boot builds on Spring Framework and provides opinionated defaults, auto-configuration, and production-ready tooling to start applications faster.
Interview point: Spring Framework does not replace Java. It manages application objects and provides reusable infrastructure around ordinary Java code.
See the Spring Framework overview and reference documentation.
IoC, Dependency Injection, and the Container
Inversion of Control (IoC) means that an object does not create or locate the objects it depends on. Control of object creation and assembly is transferred to an external container.
Dependency injection (DI) is Spring’s primary implementation of IoC. An object declares its dependencies through constructor arguments, factory-method arguments, or properties, and the container supplies them when it creates the object.
An object created, configured, and managed by the Spring container is called a bean. The container is represented by the ApplicationContext interface and is responsible for:
- Reading configuration metadata from annotated classes,
@Configurationclasses and@Beanmethods, or XML. - Creating the required beans.
- Resolving and injecting their dependencies.
- Managing lifecycle callbacks and making the configured beans available to the application.
The container turns bean definitions and application classes into a fully configured object graph inside the ApplicationContext.
Common annotations used to register and connect beans are:
@Component: marks a class as a general-purpose bean candidate for component scanning.@Service: a specialized@Componentthat communicates that a class contains service-layer or business logic.@Configuration: marks a class that declares bean definitions.@Bean: marks a factory method inside a configuration class; the returned object becomes a Spring bean.@Autowired: requests dependency injection. It is optional on a class’s only constructor because Spring selects that constructor automatically.
A factory method is a method whose main responsibility is to create, configure, and return an object, rather than perform ongoing business logic or fetch runtime data. An @Bean method can call a constructor, builder, or third-party factory; Spring then manages the returned object. In short, the factory method creates the worker, and the returned bean does the work.
The example below assumes component scanning is enabled, so Spring discovers @Service and @Component classes. Component scanning and explicit @Bean registration are covered in later sections.
import org.springframework.stereotype.Service;
@Service
public class CheckoutService {
private final PaymentGateway paymentGateway;
public CheckoutService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public void checkout(int amount) {
paymentGateway.charge(amount);
}
} public interface PaymentGateway {
void charge(int amount);
} import org.springframework.stereotype.Component;
@Component
public class StripePaymentGateway implements PaymentGateway {
@Override
public void charge(int amount) {
System.out.println("Charging $" + amount);
}
} CheckoutService declares what it needs but does not construct a specific PaymentGateway. Spring finds a matching bean and passes it to the constructor. This reduces coupling and makes the class easy to test with a stub or mock implementation.
Spring supports two main injection styles:
- Constructor injection: preferred for required dependencies; it allows immutable fields and ensures that an object is fully initialized when created.
- Setter injection: useful for optional dependencies or dependencies that may need to change after construction.
Field injection is possible with @Autowired, but constructor injection usually produces clearer dependencies and is easier to test without the Spring container.
BeanFactory is the basic container interface. ApplicationContext extends it and adds features such as event publication, internationalization, resource loading, and easier integration with Spring AOP. Most applications use an ApplicationContext.
Interview point: IoC is the broader design principle; DI is the technique Spring uses to achieve it. The container creates beans and injects their collaborators instead of letting objects construct or locate those collaborators themselves.
See Introduction to the Spring IoC Container and Beans, Container Overview, and Dependency Injection.
Java Configuration
Defining and Wiring Beans
Java configuration means expressing Spring container metadata in Java classes and methods instead of XML. It is the name of a configuration style, not a Java package. Both annotations come from org.springframework.context.annotation, which is part of Spring’s spring-context module:
| Annotation | Applied to | Purpose |
|---|---|---|
@Configuration | A class | Marks the class as a source of bean definitions. Spring processes its @Bean methods when it builds the application context. |
@Bean | A method | Tells Spring to register the method’s returned object as a bean and manage its scope, dependencies, and lifecycle. |
By default, a @Bean method’s name becomes the bean name, singleton is its scope, and the declared return type describes the bean. The method body is ordinary Java, so it can use constructors, third-party factories, or other APIs to create the object.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class AppConfig {
@Bean
public MessageRepository messageRepository() {
return new MessageRepository();
}
@Bean
public MessageService messageService(MessageRepository repository) {
return new MessageService(repository);
}
} public class MessageService {
private final MessageRepository repository;
public MessageService(MessageRepository repository) {
this.repository = repository;
}
public void send(String message) {
repository.save(message);
}
} public class MessageRepository {
public void save(String message) {
System.out.println("Saved: " + message);
}
} AppConfig contains configuration metadata; it is not one of the application services being configured. Its two factory methods tell Spring exactly how to construct MessageRepository and MessageService. With the default singleton scope, Spring creates one managed instance of each bean and returns that instance wherever it is injected or retrieved.
A @Bean method can receive dependencies as parameters. Spring resolves them from the context before invoking the method, so messageService(MessageRepository repository) receives the managed repository bean.
Method-parameter injection works in both configuration modes and avoids relying on calls between @Bean methods. proxyBeanMethods affects only whether Spring intercepts those direct calls; it does not change bean scope. With false, injection and getBean() still return the same Spring-managed singleton.
| Setting | Direct call to another @Bean method | Best use |
|---|---|---|
true (default, full mode) | A generated CGLIB subclass intercepts the call and returns the bean according to its scope, such as the existing singleton. | Use when one @Bean method intentionally calls another. |
false (lite mode) | No interception occurs. An explicit Java call executes the factory method again and may create an additional object outside the container; injection still receives the managed singleton. | Use when methods are self-contained and receive dependencies through parameters. |
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public final class ProxyBeanMethodsExamples {
private ProxyBeanMethodsExamples() {}
@Configuration(proxyBeanMethods = true)
static class FullModeConfig {
@Bean
MessageRepository messageRepository() {
return new MessageRepository();
}
@Bean
MessageService messageService() {
// Intercepted: receives the managed MessageRepository bean.
return new MessageService(messageRepository());
}
}
@Configuration(proxyBeanMethods = false)
static class LiteModeConfig {
@Bean
MessageRepository messageRepository() {
return new MessageRepository();
}
@Bean
MessageService messageService(MessageRepository repository) {
// Injected by Spring; no cross-method call is needed.
return new MessageService(repository);
}
}
} true requires a runtime CGLIB subclass, so the configuration class and intercepted methods must be overridable. Prefer false with parameter injection when no direct calls are needed: it avoids the extra subclass processing and is AOT/native-image friendly. Keep true when configuration intentionally relies on calls between @Bean methods.
If multiple beans match a parameter, use @Primary for the default or @Qualifier for an explicit selection. Creation-order and circular-dependency failures are covered under Creation Order, Lazy Beans, and Circular Dependencies.
@Bean(name = "customName") can assign an explicit name, while initMethod and destroyMethod can declare lifecycle callbacks. Unlike @Component or @Service, which are placed on the class being registered, @Bean places the registration logic in a separate configuration class. This makes it especially useful for third-party classes that cannot be annotated.
Interview point:
proxyBeanMethodschanges the behavior of direct Java calls to@Beanmethods; it does not change the declared bean’s scope. Method-parameter injection avoids depending on proxy behavior.
See Java-based Container Configuration, Basic Concepts: @Bean and @Configuration, @Configuration#proxyBeanMethods, and the org.springframework.context.annotation package documentation.
Bootstrapping and Accessing the ApplicationContext
For a standalone application, AnnotationConfigApplicationContext can bootstrap the container from one or more configuration classes. A bean can then be retrieved by type or by name and type.
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Application {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
MessageService byType = context.getBean(MessageService.class);
MessageService byName =
context.getBean("messageService", MessageService.class);
System.out.println(byType == byName); // true: singleton is the default
byType.send("Hello Spring");
}
}
} Looking up a bean by type requires exactly one matching candidate. Missing and ambiguous candidates are described with the other injection failures.
Application code should normally receive dependencies through injection instead of repeatedly calling getBean(). Direct lookup is most appropriate at application boundaries, during bootstrapping, or when integrating with code outside Spring’s control.
See Instantiating the Spring Container.
Composing Configuration Classes
Large applications should split configuration by feature or layer. @Import combines configuration classes into one context while keeping each class focused.
Multiple configuration classes contribute bean definitions to the same application context.
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration(proxyBeanMethods = false)
@Import({DataConfig.class, ServiceConfig.class})
public class ApplicationConfig {
} import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class ServiceConfig {
@Bean
public MessageService messageService(MessageRepository repository) {
return new MessageService(repository);
}
} import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class DataConfig {
@Bean
public MessageRepository messageRepository() {
return new MessageRepository();
}
} The context can now start with ApplicationConfig.class. Alternatives include passing multiple classes directly to AnnotationConfigApplicationContext, registering them with context.register(...) before refresh(), or discovering them through @ComponentScan.
Interview point: Configuration classes are organizational boundaries, not separate containers. Imported classes contribute their bean definitions to the same
ApplicationContext.
See Composing Java-based Configurations.
Externalized Properties
Spring’s Environment abstraction provides a unified way to read configuration from property files, JVM system properties, operating-system environment variables, servlet configuration, JNDI, and custom PropertySource implementations. When the same key exists in multiple sources, the source with higher precedence wins.
@PropertySource adds a properties file to the Environment. Values can then be read programmatically or injected with @Value placeholders.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
@Configuration(proxyBeanMethods = false)
@PropertySource("classpath:payment.properties")
public class ExternalPropertiesConfig {
@Bean
public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public PaymentClient paymentClient(
@Value("${payment.base-url}") String baseUrl,
@Value("${payment.timeout:5000}") int timeout) {
return new PaymentClient(baseUrl, timeout);
}
} payment.base-url=https://payments.example.com
payment.timeout=3000 public class PaymentClient {
private final String baseUrl;
private final int timeout;
public PaymentClient(String baseUrl, int timeout) {
this.baseUrl = baseUrl;
this.timeout = timeout;
}
public void connect() {
System.out.println("Connecting to " + baseUrl + " with timeout " + timeout);
}
} ${property:default} supplies a fallback, and Spring converts strings to target types such as int. A PropertySourcesPlaceholderConfigurer is not required for ordinary @Value resolution because Spring provides a lenient default resolver; however, the configurer makes unresolved placeholders without defaults fail at startup instead of injecting the literal placeholder. It can also customize placeholder syntax and must be declared through a static @Bean method because it is a BeanFactoryPostProcessor that runs before ordinary beans and the configuration class are instantiated. The core framework does not assume an application.properties filename.
Interview point: Keep environment-specific values outside compiled code. Inject them at startup and fail early when required configuration is missing.
See the Environment and PropertySource abstractions and Using @Value.
Bean Definition Profiles
A profile is a named group of bean definitions that is registered only when that profile is active. Profiles are commonly used to select environment-specific infrastructure, such as an in-memory implementation for development and an external service for production.
@Profile can be placed on a @Configuration class, a component, or an individual @Bean method.
Profiles conditionally register bean definitions in the same ApplicationContext; they do not create separate containers. More than one profile can be active at the same time.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration(proxyBeanMethods = false)
@Profile("development")
public class DevelopmentConfig {
@Bean
public StorageClient storageClient() {
return key -> System.out.println("Stored locally: " + key);
}
} import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration(proxyBeanMethods = false)
@Profile("production & !test")
public class ProductionConfig {
@Bean
public StorageClient storageClient() {
return key -> System.out.println("Stored remotely: " + key);
}
} import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ProfileApplication {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext()) {
context.getEnvironment().setActiveProfiles("development");
context.register(DevelopmentConfig.class, ProductionConfig.class);
context.refresh();
context.getBean(StorageClient.class).store("report.pdf");
}
}
} public interface StorageClient {
void store(String key);
} Profiles can be activated programmatically before refresh(), through the spring.profiles.active property, or with @ActiveProfiles in integration tests. Multiple profiles can be active simultaneously.
Profile expressions support ! (NOT), & (AND), and | (OR). Parentheses are required when combining & and |. If no profile is explicitly active, Spring enables the profile named default; its name can be changed with spring.profiles.default or the Environment API.
Interview point:
@Profileconditionally registers bean definitions while the context is being built. Attempting to retrieve a bean whose profile is inactive results in no matching bean definition.
Spring Expression Language (SpEL)
The Spring Expression Language (SpEL) evaluates expressions against objects and the Spring application context at runtime. It supports property access, method calls, arithmetic and logical operators, bean references, collections, type references, and conditional operators.
Do not confuse its syntax with property placeholders:
${payment.timeout}resolves a value from theEnvironment.#{@pricingRules.bulkThreshold() * 2}evaluates a SpEL expression.@pricingRulesreferences a bean from within a SpEL expression.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class SpelConfig {
@Bean
public PricingRules pricingRules() {
return new PricingRules();
}
@Bean
public OrderPolicy orderPolicy(
@Value("#{@pricingRules.bulkThreshold() * 2}") int maximumItems,
@Value("#{systemProperties['user.timezone'] ?: 'UTC'}") String timeZone) {
return new OrderPolicy(maximumItems, timeZone);
}
} public class PricingRules {
public int bulkThreshold() {
return 25;
}
} public class OrderPolicy {
private final int maximumItems;
private final String timeZone;
public OrderPolicy(int maximumItems, String timeZone) {
this.maximumItems = maximumItems;
this.timeZone = timeZone;
}
} In the example, @pricingRules resolves the bean, the method call returns 25, and SpEL injects 50. The Elvis operator (?:) supplies UTC when the system property is null.
SpEL can also be used programmatically with SpelExpressionParser and an EvaluationContext. Avoid evaluating untrusted expression strings with a powerful evaluation context, because expressions may access methods, types, or beans exposed to that context.
Interview point: Property placeholders retrieve configuration; SpEL computes a value. They can be combined when a property must participate in an expression.
See Spring Expression Language, Expressions in Bean Definitions, and the SpEL Language Reference.
Component Scanning
Component Scanning and Constructor Injection
Component scanning lets Spring discover application classes and register them as beans without a separate @Bean method for each class. @ComponentScan scans the specified packages recursively for classes annotated with @Component or one of its specializations.
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@ComponentScan(basePackageClasses = InventoryService.class)
public class ComponentScanConfig {
} import org.springframework.stereotype.Service;
@Service
public class InventoryService {
private final InventoryRepository repository;
public InventoryService(InventoryRepository repository) {
this.repository = repository;
}
public boolean isAvailable(String productId) {
return repository.quantity(productId) > 0;
}
} import org.springframework.stereotype.Repository;
@Repository
public class InventoryRepository {
public int quantity(String productId) {
return "missing-product".equals(productId) ? 0 : 10;
}
} basePackageClasses is a type-safe alternative to package-name strings: Spring scans the package of each supplied class. If no base package is specified, scanning begins in the package containing the class with @ComponentScan.
Spring resolves annotated dependencies primarily by type. A bean with one constructor does not need @Autowired; Spring uses that constructor automatically. With multiple constructors, mark the intended one with @Autowired. Candidate selection follows the same @Primary and @Qualifier rules as Java configuration.
By default, a detected class named InventoryService receives the bean name inventoryService. An explicit stereotype value, such as @Service("inventory"), can override it.
Interview point:
@ComponentScanregisters bean definitions; annotations such as@Autowireddescribe how the resulting beans are connected.
See Classpath Scanning and Managed Components and Using @Autowired.
Choosing a Configuration Style
Spring supports several configuration styles, and they can be mixed in the same application:
| Choice | Best fit | Main trade-off |
|---|---|---|
| Component scanning | Application classes that you own | Concise, but bean registration is distributed across the codebase. |
Java configuration with @Bean | Third-party classes, explicit construction, or centralized wiring | More verbose, but dependencies and construction are easy to inspect. |
@Import | Explicitly composing configuration modules | Clear module boundaries without broad package scanning. |
| XML configuration | Legacy systems or configuration that must remain outside compiled classes | No source annotations required, but less type-safe and more verbose. |
Use stereotypes for ordinary controllers, services, repositories, and components. Use @Bean when construction requires logic, when the class cannot be annotated, or when the same type needs multiple differently configured instances. Restrict component scanning to deliberate root packages; scanning an overly broad package can register unintended components or duplicate configurations.
@ComponentScan supports include and exclude filters when the default stereotype-based rules are not sufficient. In most applications, clear package boundaries and explicit imports are easier to maintain than complex filters.
Interview point: Annotation-based configuration and Java configuration are complementary. Component scanning discovers classes;
@Beanexplicitly registers the object returned by a factory method.
See Container Configuration Metadata and Java-based Container Configuration.
Stereotypes and Composed Annotations
A stereotype annotation marks a class for a particular role while making it eligible for component scanning:
@Componentis the generic stereotype.@Serviceidentifies service-layer logic.@Repositoryidentifies persistence code and can participate in persistence-exception translation when the corresponding post-processor is configured.@Controlleridentifies a Spring MVC controller.@Configurationidentifies a source of bean definitions and is itself a component stereotype.
@Service, @Repository, and @Controller are meta-annotated with @Component. A meta-annotation is an annotation applied to another annotation. This mechanism lets an application create composed annotations that bundle a role with reusable semantics.
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Service;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
public @interface DomainService {
@AliasFor(annotation = Service.class, attribute = "value")
String value() default "";
} @DomainService("priceCalculator")
public class PriceCalculator {
public int total(int unitPrice, int quantity) {
return unitPrice * quantity;
}
} Because @DomainService is meta-annotated with @Service, component scanning treats PriceCalculator as a bean. @AliasFor explicitly forwards the custom annotation’s value to @Service, allowing it to supply the bean name. Explicit aliasing is preferred over relying on attribute-name conventions.
Composed annotations can also combine concerns. For example, Spring MVC’s @RestController combines @Controller and @ResponseBody. Custom annotations should remain meaningful domain concepts rather than opaque bundles of unrelated behavior.
Interview point: The specialized stereotypes are all components, but their semantic roles help framework processing, tooling, AOP pointcuts, and code readability.
See @Component and Stereotype Annotations and Meta-annotations and Composed Annotations.
Inside the Spring Container
Bean Scopes
A bean’s scope determines how many instances Spring creates and how long those instances remain associated with the container or web context.
| Scope | Meaning |
|---|---|
singleton | One instance per bean definition per Spring container; this is the default. |
prototype | A new instance for each injection or getBean() request. |
request | One instance per HTTP request. |
session | One instance per HTTP session. |
application | One instance per ServletContext. |
websocket | One instance per WebSocket session. |
The four web scopes require a web-aware ApplicationContext. Spring also supports custom scopes.
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
@Configuration(proxyBeanMethods = false)
public class ScopeConfig {
@Bean // singleton by default
public MessageRepository messageRepository() {
return new MessageRepository();
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public ReportBuilder reportBuilder() {
return new ReportBuilder();
}
} public class ReportBuilder {
private final StringBuilder content = new StringBuilder();
public ReportBuilder append(String value) {
content.append(value);
return this;
}
} import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;
@Component
@RequestScope
public class RequestDetails {
private String correlationId;
public String getCorrelationId() {
return correlationId;
}
public void setCorrelationId(String correlationId) {
this.correlationId = correlationId;
}
} Important lifecycle details:
- A Spring singleton is per bean and per container, not one instance per JVM.
- The container caches one instance for each bean definition, so repeated lookups of the same bean from the same
ApplicationContextreturn the same object. - Two bean definitions backed by the same Java class are two separate singletons.
- Two application contexts can each create their own instance.
- Singleton scope does not prevent application code from calling
new. - Because the managed instance may be shared by many callers and threads, a bean with mutable state must still be designed for thread safety.
- The container caches one instance for each bean definition, so repeated lookups of the same bean from the same
- Spring performs initialization callbacks for prototype beans but does not manage their complete destruction lifecycle.
- The container creates, configures, and hands each prototype instance to the caller, but does not continue tracking it.
- Consequently, Spring does not invoke callbacks such as
@PreDestroywhen the instance is no longer needed. The application code that obtains and owns each instance—for example, code callinggetBean()orObjectProvider#getObject()—must explicitly call itsclose()or other cleanup method. If another bean retains the prototype, that bean must arrange the cleanup.
- Injecting a prototype directly into a singleton resolves it only when the singleton is created.
- The singleton therefore keeps using that one injected prototype instance rather than receiving a new one for every method call.
- To obtain new prototype instances later, inject
ObjectProvider<T>and callgetObject()each time one is needed. Each call asks the container to create and configure a new prototype; this is usually the clearest option. - Alternatively, an
@Lookupmethod is a method that Spring overrides so each call asks the container for a new prototype. This avoids a directObjectProviderdependency but requires Spring to subclass the containing bean. - A prototype-scoped proxy can also be injected, but it creates a new target for each method call on the proxy. Use it only when one proxy call represents one complete use of the prototype.
- A shorter-lived web bean can be injected into a singleton through a scoped proxy.
- A singleton can live for the entire application, while a request-scoped bean must be different for each HTTP request. Injecting one real request bean would make the singleton keep using that same request’s data.
- Spring therefore injects a proxy with the same type as the dependency. The singleton keeps this stable proxy reference, but whenever it calls a method, the proxy finds or creates the real bean for the current request and delegates the call.
- Calls made during one request reach the same request-scoped instance; calls made during another request reach a different instance.
@RequestScopeenables this proxy behavior automatically and requires an active request in a web-awareApplicationContext.
See Bean Scopes.
Bean Initialization Lifecycle
When an ApplicationContext is refreshed, Spring first prepares the container and then creates its non-lazy singleton beans. The high-level sequence is:
- Read configuration and register
BeanDefinitionmetadata. - Run
BeanFactoryPostProcessorinstances, which can modify bean definitions before regular beans are instantiated. - Discover and register
BeanPostProcessorinstances. - Resolve constructor or factory-method dependencies, creating those dependency beans first when necessary.
- Instantiate the bean through its constructor or factory method.
- Populate properties and inject setter or field dependencies and configuration values.
- Invoke aware callbacks such as
BeanNameAwareandBeanFactoryAware, when implemented. - Run
BeanPostProcessor.postProcessBeforeInitialization(...)methods. - Invoke initialization callbacks:
@PostConstruct,InitializingBean.afterPropertiesSet(), and a configured custom init method, in that order when present. - Run
BeanPostProcessor.postProcessAfterInitialization(...)methods, which may return a proxy instead of the original object.
The resulting bean—or proxy—is fully initialized and ready for use by other beans and application code.
Container-wide definition processing happens first. The creation and initialization sequence then runs for each bean, recursively creating required dependencies when necessary.
For a bean that combines all three initialization mechanisms, Spring calls them in this order: @PostConstruct, InitializingBean.afterPropertiesSet(), and the configured custom init method. Combining all three is useful for demonstrating the order, but one lifecycle mechanism is normally enough in production code.
Choose a callback by its required timing. @PostConstruct prepares one bean after injection; SmartInitializingSingleton or ContextRefreshedEvent is appropriate for work that must wait until regular singleton creation finishes. Avoid long-running or broad cross-bean work inside a bean’s initialization callback.
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class LifecycleProbe implements InitializingBean, DisposableBean {
public LifecycleProbe() {
System.out.println("1. constructor");
}
@PostConstruct
public void postConstruct() {
System.out.println("2. @PostConstruct");
}
@Override
public void afterPropertiesSet() {
System.out.println("3. afterPropertiesSet");
}
public void customInit() {
System.out.println("4. custom init method");
}
public void execute() {
System.out.println("5. bean in use");
}
@PreDestroy
public void preDestroy() {
System.out.println("6. @PreDestroy");
}
@Override
public void destroy() {
System.out.println("7. DisposableBean.destroy");
}
public void customDestroy() {
System.out.println("8. custom destroy method");
}
} import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class InternalLifecycleConfig {
@Bean(initMethod = "customInit", destroyMethod = "customDestroy")
public LifecycleProbe lifecycleProbe() {
return new LifecycleProbe();
}
} Both configuration styles become BeanDefinition metadata. Spring later invokes an @Bean factory method or constructs a scanned component, then applies the relevant injection and lifecycle post-processors.
BeanFactoryPostProcessor works on definitions; BeanPostProcessor works on created objects. Features such as annotation injection, lifecycle annotations, and AOP proxies are implemented through post-processors.
Initialization callbacks run on the target bean before an AOP proxy is applied. The bean is not considered fully initialized until the callbacks return, so initialization methods should validate state or prepare local data rather than perform long-running work or broad container lookups.
Interview point: Instantiation creates the object; dependency injection populates it; initialization callbacks prepare it; post-processing may replace the exposed reference with a proxy.
See Container Extension Points and Combining Lifecycle Mechanisms.
Bean Use and Destruction
After initialization, callers use the object exposed by the container. That reference may be the original bean or a proxy created for features such as transactions, caching, security, or other AOP advice.
Spring AOP normally uses a JDK dynamic proxy when the target implements an interface and a CGLIB subclass proxy otherwise. Either type routes calls through interceptors before delegating to the target; proxy type can also be configured explicitly.
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ContainerUseDemo {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(
InternalLifecycleConfig.class)) {
LifecycleProbe probe = context.getBean(LifecycleProbe.class);
probe.execute();
} // close() starts orderly shutdown and invokes destruction callbacks
}
} The bean scope still controls identity and lifetime during use. Mutable singleton state must be safe for concurrent callers, while prototype instances do not receive container-managed destruction callbacks. Web-scoped instances follow their corresponding request, session, application, or WebSocket lifecycle.
During a regular context shutdown, Spring first stops participating Lifecycle beans and then destroys managed singleton beans. If a bean uses multiple destruction mechanisms with different method names, the callback order is @PreDestroy, DisposableBean.destroy(), and the configured custom destroy method.
Lifecycle and SmartLifecycle support coordinated start and stop operations. SmartLifecycle additionally supports automatic startup, phases for ordering, and asynchronous shutdown.
Dependencies remain available while their dependents are being destroyed. Therefore, a dependent singleton is destroyed before the singleton on which it depends. Closing a standalone context explicitly—or using try-with-resources as above—is required for reliable cleanup.
Interview point: The object returned by
getBean()is not guaranteed to be the raw class instance; it may be a container-created proxy. Destruction callbacks run only during an orderly container shutdown.
See Lifecycle and Destruction Callbacks and Shutting Down the Container Gracefully.
Creation Order, Lazy Beans, and Circular Dependencies
Spring derives bean creation order from the dependency graph, not from source-file order, component-scan order, or bean names. If OrderService requires OrderRepository, Spring fully creates and initializes the repository before injecting it into the service.
@DependsOn expresses an ordering relationship that is not represented by injection—for example, when one bean requires another bean’s static registration or side effect.
- Static registration: The first bean adds a driver, plugin, codec, or handler to a static, process-wide registry during initialization. The second bean reads that registry through static APIs rather than receiving the first bean as a dependency, so Spring cannot infer the relationship from injection.
- Side effect: The first bean changes state outside itself—for example, loading a native library, creating a required file, or initializing a third-party SDK. The second bean needs that action to finish but does not need a reference to the first bean.
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
@Component
@DependsOn("databaseDriver")
public class MigrationRunner {
private final SchemaRepository repository;
public MigrationRunner(SchemaRepository repository) {
this.repository = repository;
}
public void migrate() {
repository.updateSchema();
}
} import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component("databaseDriver")
public class DatabaseDriver {
@PostConstruct
public void register() {
System.out.println("Database driver registered");
}
@PreDestroy
public void deregister() {
System.out.println("Database driver deregistered");
}
} import org.springframework.stereotype.Repository;
@Repository
public class SchemaRepository {
public void updateSchema() {
System.out.println("Schema updated");
}
} Here, constructor injection creates SchemaRepository before MigrationRunner, while @DependsOn separately forces databaseDriver to initialize first. For singleton beans, @DependsOn also reverses the relationship during shutdown: MigrationRunner is destroyed before databaseDriver.
Common creation and injection problems include:
- Missing candidate: no bean matches the required type, causing
NoSuchBeanDefinitionException. - Ambiguous candidate: multiple beans match a single injection point, causing
NoUniqueBeanDefinitionException; resolve it with@Primary,@Qualifier, or more specific types. - Constructor cycle: bean A requires B while B requires A, causing
BeanCurrentlyInCreationException. Setter injection can technically expose an early reference in some cases, but redesigning the responsibilities is safer. - Unexpected eager creation: a lazy bean is still created at startup when a non-lazy singleton requires it. Use
ObjectProvider<T>or a lazy injection-point proxy when deferred lookup is intentional. - Bypassing the container: an object created with
newdoes not receive Spring injection, lifecycle callbacks, or proxy-based behavior.- Spring does not know about that object, so it does not process annotations such as
@Autowired,@Value, or@PostConstructon it. - The object is also not replaced with a Spring proxy, so features such as
@Transactional, caching, security, and@Asyncdo not apply. Obtain managed application objects through injection instead.
- Spring does not know about that object, so it does not process annotations such as
- Premature post-processor dependencies: beans pulled in while
BeanPostProcessorinstances are being created may miss later post-processing, including auto-proxying.- Spring creates and registers all
BeanPostProcessorinstances early because they must process ordinary beans. If creating one of those processors also creates an application bean, the complete processor chain is not yet available for that bean. - The early bean may therefore miss annotation processing or an AOP proxy—for example, its
@Transactionalmethods may not be intercepted. Keep post-processors’ dependencies minimal; declare a post-processor@Beanmethod asstaticand ideally dependency-free.
- Spring creates and registers all
@Order does not control singleton startup order. It orders supported collections, processors, listeners, and similar extension points. Use real dependency injection or @DependsOn for bean creation order.
Interview point: Prefer dependencies that describe real object relationships. Reserve
@DependsOnfor hidden ordering constraints, and treat circular dependencies as a design warning rather than a wiring technique.
See Dependency Resolution and Circular Dependencies, Using @DependsOn, and Lazy-initialized Beans.
Aspect Oriented Programming
Cross-cutting Concerns
Some behavior applies across many unrelated classes. Examples include transaction boundaries, authorization, logging, metrics, tracing, caching, retries, and exception translation. Implementing that behavior directly in every business method creates two problems:
- Scattering: the same infrastructure logic is repeated across many classes.
- Tangling: business logic becomes mixed with infrastructure concerns.
A single aspect centralizes one concern and can apply it to selected methods across otherwise unrelated services.
Aspect-oriented programming (AOP) extracts a cross-cutting concern into an aspect and applies it declaratively to selected method executions. The business class remains focused on its primary responsibility, while the aspect defines where and when the additional behavior runs.
AOP is most appropriate when behavior is systematic and orthogonal to the business operation. It should not hide essential domain decisions or replace ordinary decomposition, inheritance, or composition.
Spring uses AOP internally for features such as declarative transactions, caching, method security, and asynchronous method execution. These features commonly work by placing an interceptor chain around a Spring-managed bean.
Interview point: AOP centralizes cross-cutting behavior. It reduces duplication without forcing business classes to call infrastructure code explicitly.
See AOP Concepts and Spring AOP Capabilities and Goals.
Core AOP Concepts and the Proxy Model
Rather than learning the terms in isolation, follow one call through the example below. OrderService.placeOrder() contains the business logic, while PerformanceAspect measures selected method executions without changing OrderService.
- Spring creates the real
OrderServicebean, called the target. - Spring exposes a proxy in front of the target, so callers receive the proxy instead of calling the target directly.
- A call to
placeOrder()is a join point—a location where AOP behavior could run. Spring AOP supports method-execution join points. - The pointcut checks whether that join point should be intercepted. Here,
@annotation(Monitored)selects methods marked with@Monitored. - When the pointcut matches, the advice runs. The
measure()advice starts a timer, invokes the target method throughproceed(), and records the elapsed time afterward. - The aspect is the class that groups this selection rule and behavior. Connecting the aspect to matching target methods is called weaving; Spring AOP does it at runtime by creating proxies.
The call path is therefore: caller → proxy → advice → target method → advice → caller. If the pointcut does not match, the proxy delegates to the target without running that advice.
The caller knows only the exposed service type. The proxy checks the pointcut, executes matching advice, and delegates to the target when the advice calls proceed().
| Concept | In this example |
|---|---|
| Aspect | The PerformanceAspect class. |
| Join point | The execution of OrderService.placeOrder(). |
| Pointcut | @annotation(Monitored), which selects the method. |
| Advice | The measure() method that surrounds the selected execution. |
| Target | The real OrderService bean containing the business logic. |
| Proxy | The wrapper received by callers that runs advice and delegates to OrderService. |
| Weaving | Spring creating the runtime relationship between the proxy, aspect, and target. |
The @AspectJ style uses AspectJ annotations to declare the aspect, pointcut, and advice, but the runtime here is still proxy-based Spring AOP. @EnableAspectJAutoProxy enables proxy creation, and the aspect must also be registered as a Spring bean.
package com.example.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class PerformanceAspect {
@Around("@annotation(Monitored)")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
long startedAt = System.nanoTime();
try {
return joinPoint.proceed();
} finally {
long elapsed = System.nanoTime() - startedAt;
System.out.println(joinPoint.getSignature() + " took " + elapsed + " ns");
}
}
} package com.example.order;
import com.example.aop.Monitored;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Monitored
public String placeOrder(String productId) {
return "order-for-" + productId;
}
} package com.example.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Monitored {
} package com.example;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy
@ComponentScan("com.example")
public class AopConfig {
} ProceedingJoinPoint represents the intercepted method call. Calling proceed() continues to the target method and returns its result; if around advice does not call proceed(), the target method does not run. In this example, the finally block records the duration whether placeOrder() returns normally or throws an exception.
Spring uses a JDK dynamic proxy when the target implements an interface and a CGLIB subclass proxy otherwise. This proxy model creates important limitations:
- Spring AOP advises method executions on Spring-managed beans, not constructors or field access.
- Self-invocation through
thisbypasses the proxy and therefore bypasses advice. finalclasses cannot use CGLIB subclass proxies, andfinalorprivatemethods cannot be advised through CGLIB.- Objects created directly with
neware outside Spring’s proxying infrastructure.
Interview point: The caller must invoke the method through the proxy. A call from one target method to another method on the same
thisreference does not cross the proxy boundary.
See @AspectJ Support and Proxying Mechanisms.
Pointcut Expressions
Spring uses the AspectJ pointcut expression language to select method executions. The official shape of the most common designator is:
execution(
modifiers-pattern?
return-type-pattern
declaring-type-pattern?method-name-pattern(parameter-pattern)
throws-pattern?
)
The declaring type is the optional, fully qualified package-and-class pattern. When present, it ends with a . that joins it to the method-name pattern. A more readable mental model is:
execution([modifiers] return-type [package-and-class.]method-name(parameters) [throws ...])
For example, consider execution(public * com.example.order..*Service.*(..)):
publicmatches only public methods.- The first
*matches any return type. com.example.order..matches that package and any number of subpackage levels.*Servicematches declaring class names ending inService.- The second
*matches any method name. (..)matches zero or more parameters of any types.
The return-type, method-name, and parameter patterns are required; the modifier, declaring-type, and throws patterns are optional. In a name or type pattern, * is a wildcard within one pattern component. In a parameter list, (*) means exactly one parameter of any type, while (..) means zero or more parameters. Pointcuts can be combined with &&, ||, and ! and extracted into named @Pointcut methods for reuse.
package com.example.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class OrderPointcuts {
@Pointcut("execution(public * com.example.order..*Service.*(..))")
public void publicServiceOperation() {
}
@Pointcut("within(com.example.order..*)")
public void inOrderPackage() {
}
@Pointcut("@annotation(com.example.aop.Monitored)")
public void monitoredOperation() {
}
@Pointcut("bean(*Service)")
public void serviceBean() {
}
@Pointcut("publicServiceOperation() && monitoredOperation()")
public void monitoredServiceOperation() {
}
} Common supported designators include:
| Designator | Matches by |
|---|---|
execution | Method signature; the primary designator in Spring AOP. |
within | Declaring type or package. |
this | Type of the proxy exposed to the caller. |
target | Type of the underlying target object. |
args | Runtime types of method arguments. |
@annotation | Annotation on the executed method. |
@within / @target | Annotation on the declaring or runtime target type. |
bean | Spring bean name; this is a Spring-specific designator. |
args(String, ..) uses runtime argument types, while execution(* *(String, ..)) matches the method’s declared parameter types. This is a common interview distinction.
Spring AOP does not support AspectJ designators such as call, get, set, cflow, or constructor-initialization designators because its join points are limited to method execution on proxied Spring beans.
Interview point: Prefer a narrow
executionexpression combined with a package or annotation constraint. Broad pointcuts can advise infrastructure or framework methods unintentionally.
See Declaring a Pointcut.
Advice Types and Ordering
Spring AOP supports five main advice types:
| Advice | When it runs | Can control execution? |
|---|---|---|
@Before | Before the target method. | No, except by throwing an exception. |
@AfterReturning | After a normal return. | Can inspect, but not replace, the returned reference. |
@AfterThrowing | After the method throws a matching exception. | Observes the exception; it does not handle it automatically. |
@After | After either a normal return or exception, like finally. | No. |
@Around | Around the entire invocation. | Yes; it can proceed, skip, retry, change arguments, return another value, or throw. |
Solid arrows represent calls; dashed arrows represent returns or propagated exceptions. Only @Around controls whether and how the target executes through proceed().
package com.example.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AuditAspect {
private static final String OPERATIONS =
"com.example.aop.OrderPointcuts.monitoredServiceOperation()";
@Before(OPERATIONS)
public void before(JoinPoint joinPoint) {
System.out.println("Starting " + joinPoint.getSignature());
}
@AfterReturning(pointcut = OPERATIONS, returning = "result")
public void afterReturning(Object result) {
System.out.println("Returned " + result);
}
@AfterThrowing(pointcut = OPERATIONS, throwing = "error")
public void afterThrowing(Throwable error) {
System.out.println("Failed with " + error.getMessage());
}
@After(OPERATIONS)
public void afterFinally() {
System.out.println("Invocation finished");
}
@Around(OPERATIONS)
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startedAt = System.nanoTime();
try {
return joinPoint.proceed();
} finally {
System.out.println("Elapsed: " + (System.nanoTime() - startedAt));
}
}
} The names in returning and throwing must match advice method parameters. Their declared parameter types further restrict which return values or exceptions match the advice.
Around advice is the most powerful and the easiest to misuse. Its first parameter must be a ProceedingJoinPoint; the target executes only when proceed() is called, and the advice’s return value becomes the value seen by the caller. Use the least powerful advice type that satisfies the requirement.
When multiple aspects advise the same method, @Order or the Ordered interface can define aspect precedence. Lower order values have higher precedence on the way into the invocation; the order reverses on the way out.
Interview point:
@Aftermeans “finally,” not “after successful completion.” Use@AfterReturningfor success and@AfterThrowingfor exceptional completion.
See Declaring Advice.
Testing Spring Applications
Unit Testing with JUnit Jupiter
JUnit 5 separates test execution into three main parts:
- JUnit Platform: launches testing frameworks on the JVM and integrates with build tools and IDEs.
- JUnit Jupiter: provides the JUnit 5 programming and extension model used by new tests.
- JUnit Vintage: runs older JUnit 3 and JUnit 4 tests when its engine is present.
Use plain JUnit for a class that can be tested without Spring. Such a test is fast, isolates business logic, and does not pay the cost of creating an ApplicationContext.
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class ShippingCalculatorTest {
private ShippingCalculator calculator;
@BeforeEach
void setUp() {
calculator = new ShippingCalculator();
}
@Test
void calculatesShippingFee() {
assertAll(
() -> assertEquals(11, calculator.fee(3)),
() -> assertEquals(0, calculator.fee(0))
);
}
@Test
void rejectsNegativeQuantity() {
assertThrows(
IllegalArgumentException.class,
() -> calculator.fee(-1)
);
}
} public class ShippingCalculator {
public int fee(int itemCount) {
if (itemCount < 0) {
throw new IllegalArgumentException("item count must not be negative");
}
return itemCount == 0 ? 0 : 5 + (itemCount * 2);
}
} Common Jupiter annotations include @Test, @BeforeEach, @AfterEach, @BeforeAll, @AfterAll, @Nested, @ParameterizedTest, and @Disabled. @BeforeAll runs once before all tests in the class, while @BeforeEach runs before every test.
JUnit uses the PER_METHOD test-instance lifecycle by default. Its sequence is new test instance → @BeforeEach → test → @AfterEach, repeated for every test method. Because @BeforeAll and @AfterAll run only once and are not associated with any one of those instances, they must be static and cannot access instance fields.
Annotating the class with @TestInstance(PER_CLASS) changes the sequence to new test instance → @BeforeAll → all tests → @AfterAll. All lifecycle and test methods now use that one object, so @BeforeAll and @AfterAll may be non-static and may access instance fields.
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PerClassLifecycleTest {
private List<String> values;
@BeforeAll
void createSharedFixture() {
values = new ArrayList<>();
}
@BeforeEach
void resetFixture() {
values.clear();
}
@Test
void startsWithAnEmptyFixture() {
assertTrue(values.isEmpty());
}
@AfterAll
void releaseSharedFixture() {
values = null;
}
} With PER_CLASS, instance fields persist between tests and can accidentally make tests depend on execution order. Reset mutable state in @BeforeEach when test isolation is required. The test instance is shared only for that test class’s execution; it is not a JVM-wide singleton.
Assertions verify observable behavior. Prefer one behavioral reason for each test to fail, use descriptive method names, and use assertThrows for expected failures rather than catching exceptions manually.
Interview point: Loading Spring does not make a test better. Start with a unit test and add the Spring TestContext Framework only when the behavior depends on container configuration or infrastructure.
See the JUnit Jupiter User Guide and Spring Testing Support.
Spring TestContext Integration Tests
The Spring TestContext Framework loads and manages an ApplicationContext for a test. It supports dependency injection into the test instance, test execution listeners, transactions, SQL scripts, profiles, property sources, and context caching.
For JUnit Jupiter, SpringExtension connects JUnit to the TestContext Framework. @SpringJUnitConfig is the usual shortcut: it combines @ExtendWith(SpringExtension.class) and @ContextConfiguration.
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(ComponentScanConfig.class)
class InventoryServiceIntegrationTest {
private final InventoryService inventoryService;
@Autowired
InventoryServiceIntegrationTest(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
@Test
void resolvesServiceAndRepositoryFromTheContext() {
assertTrue(inventoryService.isAvailable("product-1"));
assertFalse(inventoryService.isAvailable("missing-product"));
}
} This test verifies more than InventoryService logic: it checks that component scanning registers the service and repository, that constructor injection succeeds, and that the configured beans collaborate correctly.
Spring caches contexts between tests that use the same configuration, profiles, properties, and other context-defining attributes. Reusing a cached context makes a suite much faster. Use @DirtiesContext only when a test genuinely changes the context or corrupts shared singleton state, because it evicts and rebuilds that context.
| Test type | Spring context? | Typical purpose |
|---|---|---|
| Unit test | No | One class with direct dependencies or test doubles. |
| Spring integration test | Yes | Wiring, configuration, proxies, events, transactions, and infrastructure. |
| End-to-end test | Usually a running application | Behavior across the complete external boundary. |
Interview point:
@SpringJUnitConfigdoes not merely inject mocks. It creates or reuses a real Spring context based on the supplied configuration.
See Spring JUnit Jupiter Testing Annotations, TestContext Framework, and Context Caching.
Database Integration Testing
@ActiveProfiles activates bean-definition profiles before the test context is loaded. This makes it useful for selecting test-specific infrastructure such as an embedded DataSource. Active profiles are part of Spring’s context-cache key, so tests with different profile sets generally use different contexts.
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.annotation.Transactional;
@SpringJUnitConfig(classes = {
DatabaseTestConfig.class,
CustomerRepository.class
})
@ActiveProfiles("test")
@Transactional
@Sql("/test-data.sql")
class CustomerRepositoryIntegrationTest {
@Autowired
CustomerRepository repository;
@Test
void savesCustomer() {
assertEquals(2, repository.count());
repository.save(3L, "Ada");
assertEquals(3, repository.count());
}
} import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration(proxyBeanMethods = false)
@Profile("test")
@EnableTransactionManagement
public class DatabaseTestConfig {
@Bean
public EmbeddedDatabase dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:/schema.sql")
.build();
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
} import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class CustomerRepository {
private final JdbcTemplate jdbcTemplate;
public CustomerRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void save(long id, String name) {
jdbcTemplate.update(
"insert into customer (id, name) values (?, ?)",
id,
name
);
}
public int count() {
return jdbcTemplate.queryForObject(
"select count(*) from customer",
Integer.class
);
}
} In this example, schema.sql creates the customer table and test-data.sql inserts two rows. @Sql runs the data script before each test method by default. It can also run scripts after a method or before and after a test class by setting executionPhase.
@Transactional on a test starts a test-managed transaction that is rolled back by default. A PlatformTransactionManager must exist in the test context. Use @Commit or @Rollback(false) only when a test intentionally needs committed changes.
TestTransaction provides programmatic transaction control. @BeforeTransaction and @AfterTransaction run outside the test-managed transaction, while @BeforeEach and @AfterEach run inside it.
Important database-testing details:
@BeforeEachand@AfterEachexecute inside a transactional test’s transaction;@BeforeAlland@AfterAlldo not.- Preemptive timeouts such as JUnit’s
assertTimeoutPreemptivelyrun code on another thread, outside Spring’s thread-bound test transaction, so database changes may commit instead of rolling back. - ORM tests should flush the persistence context before assertions when a database constraint or generated SQL must be verified; otherwise, a test can produce a false positive.
- Embedded databases are fast, but database-specific SQL, locking, and transaction behavior should also be tested against the production database engine when those differences matter.
@TestPropertySource can add test-only properties, while @DynamicPropertySource can publish values determined at runtime, such as an external test database’s generated port.
Interview point: Transactional test rollback isolates database state, but only work participating in the test-managed transaction is rolled back.
See Context Configuration with Profiles, Test-managed Transactions, and Executing SQL Scripts.
JDBC Simplification with JdbcTemplate
Why JdbcTemplate?
Traditional JDBC (Java Database Connectivity) requires the same infrastructure steps for nearly every operation:
- Obtain a
Connection. - Create a
StatementorPreparedStatement. - Bind parameters and execute SQL.
- Iterate through the
ResultSetand map columns. - Catch
SQLException. - Close the result set, statement, and connection in the correct order.
This boilerplate obscures the query’s intent and creates failure paths for leaked resources, incomplete cleanup, inconsistent exception handling, and incorrect transaction participation.
JdbcTemplate applies the template-method pattern to this workflow. Spring controls the repetitive JDBC lifecycle and calls application-provided callbacks for the variable parts.
| Spring handles | Application code handles |
|---|---|
| Obtaining and releasing connections | Configuring the DataSource |
| Creating and executing statements | Writing SQL and supplying parameters |
| Iterating result sets | Mapping each row or extracting the complete result |
Translating SQLException | Handling meaningful data-access failures |
| Participating in Spring transactions | Defining transaction boundaries |
JdbcTemplate does not generate SQL or provide object-relational mapping. It keeps JDBC explicit while removing resource-management and exception-handling boilerplate.
Interview point:
JdbcTemplateis not an ORM (object-relational mapping). It is a reusable JDBC workflow that lets application code focus on SQL, parameters, and result mapping.
See Data Access with JDBC and Using JdbcTemplate.
Queries and Updates
A configured JdbcTemplate is thread-safe and should normally be shared as a singleton. Inject it into repository classes rather than creating a new template for every method call.
import java.util.List;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
@Repository
public class JdbcProductRepository {
private static final RowMapper<Product> PRODUCT_MAPPER = (rs, rowNum) ->
new Product(
rs.getLong("id"),
rs.getString("name"),
rs.getInt("quantity")
);
private final JdbcTemplate jdbcTemplate;
public JdbcProductRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Product findRequired(long id) {
String sql = """
SELECT
p.id,
p.name,
p.quantity
FROM product AS p
WHERE p.id = ?
""";
return jdbcTemplate.queryForObject(
sql,
PRODUCT_MAPPER,
id
);
}
public List<Product> findAll() {
String sql = """
SELECT
p.id,
p.name,
p.quantity
FROM product AS p
ORDER BY p.id
""";
return jdbcTemplate.query(
sql,
PRODUCT_MAPPER
);
}
public int insert(Product product) {
String sql = """
INSERT INTO product (
id,
name,
quantity
)
VALUES (?, ?, ?)
""";
return jdbcTemplate.update(
sql,
product.id(),
product.name(),
product.quantity()
);
}
public long count() {
String sql = """
SELECT COUNT(*)
FROM product AS p
""";
return jdbcTemplate.queryForObject(
sql,
Long.class
);
}
} import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration(proxyBeanMethods = false)
public class JdbcConfig {
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
} public record Product(long id, String name, int quantity) {
} The most common operations are:
query(...)for multiple rows.queryForObject(...)for exactly one row or scalar value.update(...)forINSERT,UPDATE, andDELETE; it returns the affected-row count.batchUpdate(...)for repeated writes using one prepared statement.execute(...)for lower-level or arbitrary JDBC work.
Use ? placeholders and bound arguments instead of concatenating untrusted values into SQL. queryForObject expects exactly one result: zero rows cause EmptyResultDataAccessException, and multiple rows cause IncorrectResultSizeDataAccessException.
NamedParameterJdbcTemplate supports named placeholders such as :productId. Newer Spring applications can also use JdbcClient, a fluent facade that delegates to JdbcTemplate and NamedParameterJdbcTemplate.
Interview point:
JdbcTemplateis thread-safe once configured, but the mutable state inside repository callbacks or domain objects still follows normal Java thread-safety rules.
See Running Queries and Updates and JDBC Batch Operations.
ResultSet Callbacks
JdbcTemplate offers three main callback styles for reading a ResultSet:
| Callback | Use when |
|---|---|
RowMapper<T> | Each row independently maps to one result object. This is the usual choice. |
ResultSetExtractor<T> | The entire result set must be combined into one object graph or aggregate. |
RowCallbackHandler | Each row triggers a side effect and no returned collection is required. |
The basic repository above uses a reusable RowMapper<Product>. The following repository demonstrates the other two callback types.
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class OrderReportRepository {
private final JdbcTemplate jdbcTemplate;
public OrderReportRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<OrderSummary> findOrdersWithLines() {
String sql = """
SELECT
o.id AS order_id,
o.customer_name,
l.id AS line_id,
l.product_name
FROM customer_order AS o
LEFT JOIN order_line AS l
ON l.order_id = o.id
ORDER BY o.id, l.id
""";
return jdbcTemplate.query(sql, rs -> {
Map<Long, OrderSummary> orders = new LinkedHashMap<>();
while (rs.next()) {
long orderId = rs.getLong("order_id");
OrderSummary order = orders.get(orderId);
if (order == null) {
order = new OrderSummary(
orderId,
rs.getString("customer_name"),
new ArrayList<>()
);
orders.put(orderId, order);
}
Long lineId = rs.getObject("line_id", Long.class);
if (lineId != null) {
order.lines().add(new OrderLine(
lineId,
rs.getString("product_name")
));
}
}
return List.copyOf(orders.values());
});
}
public void forEachProductName(Consumer<String> consumer) {
String sql = """
SELECT p.name
FROM product AS p
ORDER BY p.id
""";
jdbcTemplate.query(
sql,
rs -> consumer.accept(rs.getString("name"))
);
}
} import java.util.List;
public record OrderSummary(
long id,
String customerName,
List<OrderLine> lines
) {
} public record OrderLine(long id, String productName) {
} The ResultSetExtractor lambda controls iteration with while (rs.next()) because it processes the complete joined result. It groups many rows into one OrderSummary per order. The RowCallbackHandler lambda is invoked once for each row by JdbcTemplate and performs a side effect through the supplied Consumer.
RowMapper and RowCallbackHandler callbacks must not call next(); Spring has already positioned the cursor on the current row. None of these callbacks should close the ResultSet, statement, or connection—JdbcTemplate owns those resources.
Interview point: Use
RowMapperfor one object per row,ResultSetExtractorfor one result built from the whole result set, andRowCallbackHandlerfor per-row processing without a return value.
See RowMapper, ResultSetExtractor, and RowCallbackHandler.
DataAccessException Translation
JDBC exposes checked SQLException objects containing vendor codes and SQL states. JdbcTemplate catches them and uses an SQLExceptionTranslator to throw a consistent, unchecked DataAccessException subtype. The original SQLException remains available as the cause.
Common translated exceptions include:
| Exception | Meaning |
|---|---|
BadSqlGrammarException | Invalid SQL syntax or an invalid database object. |
DataIntegrityViolationException | A constraint or data-integrity rule was violated. |
DuplicateKeyException | A primary-key or unique constraint was violated. |
EmptyResultDataAccessException | An operation expected at least one result but found none. |
CannotGetJdbcConnectionException | A database connection could not be obtained. |
CannotAcquireLockException | A required database lock could not be acquired. |
Catch an exception only when the current layer can recover or translate it into a meaningful domain failure. Otherwise, let it propagate so transaction infrastructure can roll back appropriately.
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
@Service
public class CustomerRegistrationService {
private final JdbcCustomerRepository repository;
public CustomerRegistrationService(JdbcCustomerRepository repository) {
this.repository = repository;
}
public void register(long id, String email) {
try {
repository.insert(id, email);
} catch (DuplicateKeyException ex) {
throw new CustomerAlreadyExistsException(email, ex);
}
}
} import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class JdbcCustomerRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcCustomerRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void insert(long id, String email) {
jdbcTemplate.update(
"insert into customer (id, email) values (?, ?)",
id,
email
);
}
} public class CustomerAlreadyExistsException extends RuntimeException {
public CustomerAlreadyExistsException(String email, Throwable cause) {
super("Customer already exists: " + email, cause);
}
} Spring’s exception hierarchy is independent of JDBC, which lets higher layers respond to categories such as integrity, connectivity, or concurrency failure without interpreting vendor-specific codes. A custom SQLExceptionTranslator can be configured when the default translation is insufficient.
Interview point:
DataAccessExceptionis unchecked by design. Do not wrap every call in a generic catch block; catch specific subtypes only when you can add domain meaning or recover.
See DAO Exception Translation and Using SQLExceptionTranslator.
Transaction Management with Spring
Transaction Fundamentals
A transaction groups multiple operations into one logical unit of work. A transfer that debits one account and credits another must not leave only one update committed.
Database transactions are commonly described by ACID:
- Atomicity: all operations commit or all are rolled back.
- Consistency: successful work preserves application and database invariants.
- Isolation: concurrent transactions do not observe prohibited intermediate states.
- Durability: committed changes survive failures according to the database’s guarantees.
JDBC supports a local transaction on one Connection. Application code disables auto-commit, executes all statements on that connection, calls commit() on success, and calls rollback() on failure. JDBC also exposes savepoints and transaction-isolation levels.
The same TransferService workflow has different atomicity depending on its connection scope.
On the left, no transaction surrounds transfer(). Each JdbcTemplate call can obtain and release a separate connection. With auto-commit enabled, debit() may become permanent before credit() runs, so a later failure can leave a partial transfer.
On the right, a transactional interceptor asks JdbcTransactionManager to begin a transaction and bind one connection to the current thread. JdbcTemplate obtains connections through Spring’s transaction-aware DataSourceUtils, so both repository calls reuse that connection. The interceptor commits after a successful return or rolls back after a qualifying failure, then releases the connection. The service and repository code do not manage the connection directly.
The standard read anomalies are:
- Dirty read: a transaction reads changes made by another transaction before they are committed. If the other transaction rolls back, the first transaction has observed data that never became permanent.
- Non-repeatable read: a transaction reads the same row twice and gets different values because another transaction committed an update or deletion between the reads.
- Phantom read: a transaction repeats a query with the same condition and gets a different set of matching rows because another transaction committed an insert, deletion, or relevant update.
| Isolation level | Prevents under the SQL standard | Why |
|---|---|---|
READ_UNCOMMITTED | None of the standard read anomalies. | The transaction may observe changes that concurrent transactions have not committed. |
READ_COMMITTED | Dirty reads. | Each statement can see only committed data, but a later statement may use a newer committed view. |
REPEATABLE_READ | Dirty and non-repeatable reads; phantom behavior can vary by database. | A stable transaction view or retained locks keep previously read rows consistent, although the SQL standard may still allow new rows to match a repeated query. |
SERIALIZABLE | Dirty, non-repeatable, and phantom reads. | The database permits only outcomes equivalent to running the transactions one at a time, blocking or aborting conflicting work when necessary. |
PostgreSQL implements these guarantees with multiversion concurrency control (MVCC). READ_UNCOMMITTED behaves like READ_COMMITTED; READ_COMMITTED takes a new snapshot for each statement; and REPEATABLE_READ keeps one transaction snapshot, which also prevents phantom reads in PostgreSQL. SERIALIZABLE adds conflict detection and may abort a transaction with a serialization failure, so applications must be prepared to retry it.
Stronger isolation can reduce concurrency or require retries, and exact locking or multiversion behavior is database-specific. Java Transaction API (JTA, now Jakarta Transactions) provides coordination when a transaction must span multiple transactional resources. Spring does not replace the underlying transaction system; it provides one programming model over JDBC, JTA, JPA, Hibernate, and other technologies.
Interview point: A JDBC transaction belongs to a connection. All participating operations must use the connection associated with the current transaction.
See Transaction Management, Understanding the Transaction Abstraction, Controlling Database Connections, and PostgreSQL Transaction Isolation.
Declarative Transaction Management
PlatformTransactionManager is Spring’s central imperative transaction strategy. Implementations adapt the abstraction to a resource: JdbcTransactionManager for one JDBC DataSource, JpaTransactionManager for JPA, and JtaTransactionManager for JTA transactions. Reactive applications use ReactiveTransactionManager instead.
Declarative transaction management places a TransactionInterceptor around a Spring bean through an AOP proxy. @Transactional supplies metadata such as propagation, isolation, timeout, read-only status, transaction-manager qualifier, and rollback rules.
The interceptor adds transaction behavior around the service invocation. The target TransferService contains only business operations and does not call the transaction manager directly.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TransferService {
private final AccountRepository accounts;
public TransferService(AccountRepository accounts) {
this.accounts = accounts;
}
@Transactional
public void transfer(long fromId, long toId, int amount) {
accounts.debit(fromId, amount);
accounts.credit(toId, amount);
}
@Transactional(readOnly = true)
public int balance(long accountId) {
return accounts.balance(accountId);
}
} import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class AccountRepository {
private final JdbcTemplate jdbcTemplate;
public AccountRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void debit(long accountId, int amount) {
int updated = jdbcTemplate.update(
"update account set balance = balance - ? "
+ "where id = ? and balance >= ?",
amount,
accountId,
amount
);
if (updated != 1) {
throw new InsufficientFundsException(accountId);
}
}
public void credit(long accountId, int amount) {
jdbcTemplate.update(
"update account set balance = balance + ? where id = ?",
amount,
accountId
);
}
public int balance(long accountId) {
return jdbcTemplate.queryForObject(
"select balance from account where id = ?",
Integer.class,
accountId
);
}
} import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.support.JdbcTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new JdbcTransactionManager(dataSource);
}
} public class InsufficientFundsException extends RuntimeException {
public InsufficientFundsException(long accountId) {
super("Insufficient funds in account " + accountId);
}
} If credit() fails, the unchecked exception leaves transfer(), and the interceptor rolls back the debit and credit together. JdbcTemplate automatically participates in the connection bound to the current Spring transaction.
The default @Transactional settings are REQUIRED propagation, database-default isolation, read-write mode, the underlying system’s default timeout, and rollback for RuntimeException or Error.
readOnly = true is an optimization hint to the transaction manager and database driver, not a portable guarantee that writes are impossible.
Important proxy implications:
@Transactionalis metadata;@EnableTransactionManagementor equivalent infrastructure activates it.- In the default proxy mode, only calls entering through the proxy are intercepted. Self-invocation does not start a new transaction or apply different transactional settings.
- Imperative transactions are normally bound to the current thread and do not automatically cross newly created threads.
- Place transaction boundaries at service methods that represent complete units of work.
For dynamic workflows, TransactionTemplate provides programmatic transaction demarcation. Direct use of PlatformTransactionManager is lower level; declarative @Transactional is usually the clearest choice for service-layer operations.
Interview point: The transaction manager controls the resource transaction; the interceptor decides when to begin, commit, or roll it back around a proxied method call.
See Using @Transactional and Declarative Transaction Implementation.
Transaction Propagation
Propagation defines how a transactional method behaves when its caller already has a transaction.
| Propagation | Behavior |
|---|---|
REQUIRED | Join the current transaction or create one if none exists; this is the default. |
SUPPORTS | Join a current transaction, but run without one if none exists. |
MANDATORY | Require a current transaction or throw an exception. |
REQUIRES_NEW | Suspend the current transaction and create an independent physical transaction. |
NOT_SUPPORTED | Suspend the current transaction and run non-transactionally. |
NEVER | Run non-transactionally and throw an exception if a transaction exists. |
NESTED | Use a savepoint inside one physical transaction so the nested scope can roll back partially. |
Read each timeline from left to right. The shaded interval is the called method; tx1 is the caller or first transaction, while tx2 is an independent transaction. Gaps show suspension, and the original transaction resumes after the method returns.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CheckoutTransactionService {
private final StockReservationService stock;
private final AuditLogService audit;
public CheckoutTransactionService(
StockReservationService stock,
AuditLogService audit) {
this.stock = stock;
this.audit = audit;
}
@Transactional
public void checkout(long orderId, long productId) {
stock.reserve(productId);
audit.record("Checkout attempted for order " + orderId);
// A later failure rolls back the reservation but not the audit entry.
}
} import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AuditLogService {
private final JdbcTemplate jdbcTemplate;
public AuditLogService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void record(String message) {
jdbcTemplate.update("insert into audit_log (message) values (?)", message);
}
} import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class StockReservationService {
private final JdbcTemplate jdbcTemplate;
public StockReservationService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Transactional(propagation = Propagation.MANDATORY)
public void reserve(long productId) {
jdbcTemplate.update(
"update product set quantity = quantity - 1 where id = ?",
productId
);
}
} The calls cross bean boundaries, so their transactional proxies apply. StockReservationService must join the checkout transaction. AuditLogService suspends it and commits its audit record independently.
Important propagation edge cases:
- With
REQUIRED, inner and outer logical scopes share one physical transaction. If an inner scope marks it rollback-only and the outer scope still attempts to commit, Spring throwsUnexpectedRollbackException. REQUIRES_NEWusually needs another database connection while the outer connection remains bound. Heavy use can exhaust the connection pool or deadlock threads waiting for extra connections.NESTEDnormally relies on JDBC savepoints and is not equivalent to an independent transaction. If the outer transaction rolls back, all nested work rolls back too.- Propagation applies only when a call is intercepted. Calling a differently annotated method through
thisbypasses the transactional proxy.
Interview point:
REQUIRES_NEWcreates an independent physical transaction;NESTEDnormally creates a savepoint within the existing physical transaction.
Rollback Rules
By default, an unhandled RuntimeException or Error marks a transaction for rollback, while a checked exception does not. rollbackFor adds rollback rules, and noRollbackFor adds commit rules. Type-based rules are safer than class-name patterns because patterns use substring matching.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class PaymentSettlementService {
private final JdbcTemplatePaymentRepository payments;
public PaymentSettlementService(JdbcTemplatePaymentRepository payments) {
this.payments = payments;
}
@Transactional(
rollbackFor = PaymentDeclinedException.class,
noRollbackFor = ReceiptDeliveryException.class
)
public void settle(
long paymentId,
boolean declined,
boolean receiptDeliveryFailed)
throws PaymentDeclinedException, ReceiptDeliveryException {
payments.markSettled(paymentId);
if (declined) {
throw new PaymentDeclinedException("Payment was declined");
}
if (receiptDeliveryFailed) {
throw new ReceiptDeliveryException("Receipt could not be delivered");
}
}
} import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class JdbcTemplatePaymentRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcTemplatePaymentRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void markSettled(long paymentId) {
jdbcTemplate.update(
"update payment set status = 'SETTLED' where id = ?",
paymentId
);
}
} public class PaymentDeclinedException extends Exception {
public PaymentDeclinedException(String message) {
super(message);
}
} public class ReceiptDeliveryException extends Exception {
public ReceiptDeliveryException(String message) {
super(message);
}
} When several rollback rules match, the strongest and most specific rule wins. Avoid catching an exception inside a transactional method unless it can be handled completely; swallowing it prevents the interceptor from applying exception-based rollback rules. Programmatic setRollbackOnly() is available but couples business code to Spring’s transaction API.
The TestContext Framework interprets @Transactional on test methods separately to provide automatic isolation. That behavior, including default rollback and thread-bound limitations, is covered in Database Integration Testing.
Interview point: Unchecked exceptions and
Errortrigger rollback by default; checked exceptions require an explicit rollback rule. Catching and swallowing the exception prevents rule-based rollback.