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:

  1. Reading configuration metadata from annotated classes, @Configuration classes and @Bean methods, or XML.
  2. Creating the required beans.
  3. Resolving and injecting their dependencies.
  4. Managing lifecycle callbacks and making the configured beans available to the application.

The Spring container creates and connects application beans from application classes and configuration metadata.

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 @Component that 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);
	}
}

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:

AnnotationApplied toPurpose
@ConfigurationA classMarks the class as a source of bean definitions. Spring processes its @Bean methods when it builds the application context.
@BeanA methodTells 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);
	}
}

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.

SettingDirect call to another @Bean methodBest 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: proxyBeanMethods changes the behavior of direct Java calls to @Bean methods; 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.

Spring configuration split across multiple focused configuration classes.

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 {
}

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);
	}
}

${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.

A Spring application context containing always-available beans and beans grouped under JPA, JDBC, production, development, and test profiles.

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);
	}
}

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: @Profile conditionally registers bean definitions while the context is being built. Attempting to retrieve a bean whose profile is inactive results in no matching bean definition.

See Bean Definition Profiles.

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 the Environment.
  • #{@pricingRules.bulkThreshold() * 2} evaluates a SpEL expression.
  • @pricingRules references 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);
	}
}

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 {
}

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: @ComponentScan registers bean definitions; annotations such as @Autowired describe 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:

ChoiceBest fitMain trade-off
Component scanningApplication classes that you ownConcise, but bean registration is distributed across the codebase.
Java configuration with @BeanThird-party classes, explicit construction, or centralized wiringMore verbose, but dependencies and construction are easy to inspect.
@ImportExplicitly composing configuration modulesClear module boundaries without broad package scanning.
XML configurationLegacy systems or configuration that must remain outside compiled classesNo 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; @Bean explicitly 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:

  • @Component is the generic stereotype.
  • @Service identifies service-layer logic.
  • @Repository identifies persistence code and can participate in persistence-exception translation when the corresponding post-processor is configured.
  • @Controller identifies a Spring MVC controller.
  • @Configuration identifies 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 "";
}

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.

ScopeMeaning
singletonOne instance per bean definition per Spring container; this is the default.
prototypeA new instance for each injection or getBean() request.
requestOne instance per HTTP request.
sessionOne instance per HTTP session.
applicationOne instance per ServletContext.
websocketOne 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();
	}
}

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 ApplicationContext return 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.
  • 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 @PreDestroy when the instance is no longer needed. The application code that obtains and owns each instance—for example, code calling getBean() or ObjectProvider#getObject()—must explicitly call its close() 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 call getObject() 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 @Lookup method is a method that Spring overrides so each call asks the container for a new prototype. This avoids a direct ObjectProvider dependency 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. @RequestScope enables this proxy behavior automatically and requires an active request in a web-aware ApplicationContext.

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:

  1. Read configuration and register BeanDefinition metadata.
  2. Run BeanFactoryPostProcessor instances, which can modify bean definitions before regular beans are instantiated.
  3. Discover and register BeanPostProcessor instances.
  4. Resolve constructor or factory-method dependencies, creating those dependency beans first when necessary.
  5. Instantiate the bean through its constructor or factory method.
  6. Populate properties and inject setter or field dependencies and configuration values.
  7. Invoke aware callbacks such as BeanNameAware and BeanFactoryAware, when implemented.
  8. Run BeanPostProcessor.postProcessBeforeInitialization(...) methods.
  9. Invoke initialization callbacks: @PostConstruct, InitializingBean.afterPropertiesSet(), and a configured custom init method, in that order when present.
  10. 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.

Spring loads and processes bean definitions before creating each bean, injecting its dependencies, invoking initialization callbacks, and applying bean post-processors.

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");
	}
}

Java configuration and component scanning both register bean definitions before Spring creates and injects the resulting beans.

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.

A JDK dynamic proxy implements the target's interface, while a CGLIB proxy subclasses the target class; both apply advice before delegating to the target.

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();
	}
}

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 new does 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 @PostConstruct on it.
    • The object is also not replaced with a Spring proxy, so features such as @Transactional, caching, security, and @Async do not apply. Obtain managed application objects through injection instead.
  • Premature post-processor dependencies: beans pulled in while BeanPostProcessor instances are being created may miss later post-processing, including auto-proxying.
    • Spring creates and registers all BeanPostProcessor instances 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 @Transactional methods may not be intercepted. Keep post-processors’ dependencies minimal; declare a post-processor @Bean method as static and ideally dependency-free.

@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 @DependsOn for 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.

Security, transaction, and logging aspects apply cross-cutting behavior to method executions in BankService, CustomerService, and ReportingService.

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.

  1. Spring creates the real OrderService bean, called the target.
  2. Spring exposes a proxy in front of the target, so callers receive the proxy instead of calling the target directly.
  3. A call to placeOrder() is a join point—a location where AOP behavior could run. Spring AOP supports method-execution join points.
  4. The pointcut checks whether that join point should be intercepted. Here, @annotation(Monitored) selects methods marked with @Monitored.
  5. When the pointcut matches, the advice runs. The measure() advice starts a timer, invokes the target method through proceed(), and records the elapsed time afterward.
  6. 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.

Spring creates an AOP proxy that intercepts calls to OrderService, runs matching advice from PerformanceAspect, invokes the target method, and returns the result through the proxy.

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().

ConceptIn this example
AspectThe PerformanceAspect class.
Join pointThe execution of OrderService.placeOrder().
Pointcut@annotation(Monitored), which selects the method.
AdviceThe measure() method that surrounds the selected execution.
TargetThe real OrderService bean containing the business logic.
ProxyThe wrapper received by callers that runs advice and delegates to OrderService.
WeavingSpring 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");
		}
	}
}

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 this bypasses the proxy and therefore bypasses advice.
  • final classes cannot use CGLIB subclass proxies, and final or private methods cannot be advised through CGLIB.
  • Objects created directly with new are 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 this reference 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.*(..)):

  • public matches only public methods.
  • The first * matches any return type.
  • com.example.order.. matches that package and any number of subpackage levels.
  • *Service matches declaring class names ending in Service.
  • 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:

DesignatorMatches by
executionMethod signature; the primary designator in Spring AOP.
withinDeclaring type or package.
thisType of the proxy exposed to the caller.
targetType of the underlying target object.
argsRuntime types of method arguments.
@annotationAnnotation on the executed method.
@within / @targetAnnotation on the declaring or runtime target type.
beanSpring 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 execution expression 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:

AdviceWhen it runsCan control execution?
@BeforeBefore the target method.No, except by throwing an exception.
@AfterReturningAfter a normal return.Can inspect, but not replace, the returned reference.
@AfterThrowingAfter the method throws a matching exception.Observes the exception; it does not handle it automatically.
@AfterAfter either a normal return or exception, like finally.No.
@AroundAround the entire invocation.Yes; it can proceed, skip, retry, change arguments, return another value, or throw.

Sequence diagrams showing when Before, AfterReturning, AfterThrowing, After, and Around advice execute relative to the Spring proxy and target method.

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: @After means “finally,” not “after successful completion.” Use @AfterReturning for success and @AfterThrowing for 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)
		);
	}
}

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 typeSpring context?Typical purpose
Unit testNoOne class with direct dependencies or test doubles.
Spring integration testYesWiring, configuration, proxies, events, transactions, and infrastructure.
End-to-end testUsually a running applicationBehavior across the complete external boundary.

Interview point: @SpringJUnitConfig does 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());
	}
}

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:

  • @BeforeEach and @AfterEach execute inside a transactional test’s transaction; @BeforeAll and @AfterAll do not.
  • Preemptive timeouts such as JUnit’s assertTimeoutPreemptively run 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:

  1. Obtain a Connection.
  2. Create a Statement or PreparedStatement.
  3. Bind parameters and execute SQL.
  4. Iterate through the ResultSet and map columns.
  5. Catch SQLException.
  6. 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 handlesApplication code handles
Obtaining and releasing connectionsConfiguring the DataSource
Creating and executing statementsWriting SQL and supplying parameters
Iterating result setsMapping each row or extracting the complete result
Translating SQLExceptionHandling meaningful data-access failures
Participating in Spring transactionsDefining 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: JdbcTemplate is 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
		);
	}
}

The most common operations are:

  • query(...) for multiple rows.
  • queryForObject(...) for exactly one row or scalar value.
  • update(...) for INSERT, UPDATE, and DELETE; 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: JdbcTemplate is 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:

CallbackUse 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.
RowCallbackHandlerEach 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"))
		);
	}
}

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 RowMapper for one object per row, ResultSetExtractor for one result built from the whole result set, and RowCallbackHandler for 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:

ExceptionMeaning
BadSqlGrammarExceptionInvalid SQL syntax or an invalid database object.
DataIntegrityViolationExceptionA constraint or data-integrity rule was violated.
DuplicateKeyExceptionA primary-key or unique constraint was violated.
EmptyResultDataAccessExceptionAn operation expected at least one result but found none.
CannotGetJdbcConnectionExceptionA database connection could not be obtained.
CannotAcquireLockExceptionA 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);
		}
	}
}

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: DataAccessException is 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.

Without a transaction boundary, debit and credit can use separate JDBC connections; with Spring transaction management, both repository operations share one transaction-bound connection.

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 levelPrevents under the SQL standardWhy
READ_UNCOMMITTEDNone of the standard read anomalies.The transaction may observe changes that concurrent transactions have not committed.
READ_COMMITTEDDirty reads.Each statement can see only committed data, but a later statement may use a newer committed view.
REPEATABLE_READDirty 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.
SERIALIZABLEDirty, 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.

A Spring AOP proxy uses TransactionInterceptor and JdbcTransactionManager to begin a transaction before calling TransferService, then commit on success or roll back after a qualifying failure.

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);
	}
}

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:

  • @Transactional is metadata; @EnableTransactionManagement or 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.

PropagationBehavior
REQUIREDJoin the current transaction or create one if none exists; this is the default.
SUPPORTSJoin a current transaction, but run without one if none exists.
MANDATORYRequire a current transaction or throw an exception.
REQUIRES_NEWSuspend the current transaction and create an independent physical transaction.
NOT_SUPPORTEDSuspend the current transaction and run non-transactionally.
NEVERRun non-transactionally and throw an exception if a transaction exists.
NESTEDUse a savepoint inside one physical transaction so the nested scope can roll back partially.

Timelines comparing all Spring transaction propagation modes when a caller transaction exists and when no transaction exists.

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.
	}
}

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 throws UnexpectedRollbackException.
  • REQUIRES_NEW usually needs another database connection while the outer connection remains bound. Heavy use can exhaust the connection pool or deadlock threads waiting for extra connections.
  • NESTED normally 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 this bypasses the transactional proxy.

Interview point: REQUIRES_NEW creates an independent physical transaction; NESTED normally creates a savepoint within the existing physical transaction.

See Transaction Propagation.

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");
		}
	}
}

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 Error trigger rollback by default; checked exceptions require an explicit rollback rule. Catching and swallowing the exception prevents rule-based rollback.

See Rolling Back a Declarative Transaction.