Spring interview questions
Table of Contents
How does @Autowired work?
At a high level, @Autowired is part of Spring's dependency injection mechanism. Its job is to tell Spring that a class depends on another object, and that Spring should automatically provide that dependency at runtime instead of the developer manually creating it.
During startup, Spring performs a component scan over the configured packages. It looks for classes annotated with @Component, @Service, @Repository, and @Controller. Each of these annotations signals to Spring that the class should be managed by the framework.
For every discovered component, Spring creates an object called a bean and stores it inside the ApplicationContext. You can think of the ApplicationContext as a container that holds all fully-initialized, ready-to-use objects that Spring manages for the application.
When Spring encounters the @Autowired annotation, such as on a field, constructor, or setter, it recognizes that the current bean has a dependency that needs to be satisfied. For example, if Spring sees a field of type UserService annotated with @Autowired, it understands that this class requires a UserService instance.
Rather than creating a new object itself, Spring searches inside the ApplicationContext for an existing bean that matches the required type. Spring always resolves dependencies by type first. If there is exactly one matching bean, Spring injects it automatically. If there are multiple beans of the same type, Spring then attempts to disambiguate by name or additional metadata, such as @Qualifier. If Spring cannot uniquely determine which bean to inject, it fails fast and throws an error to prevent ambiguous behavior.
Once the correct bean is identified, Spring performs the injection. The preferred and recommended approach is constructor injection, because it makes dependencies explicit and ensures the object is always created in a valid state. Field injection and setter injection are also supported, but they are generally considered less explicit and harder to test.
In essence, @Autowired allows Spring to take a fully managed object from its container and seamlessly plug it into another object that depends on it. This decouples object creation from object usage, making applications easier to maintain, test, and extend.
@Transactional in Spring Boot
Spring Boot leverages proxy-based Aspect-Oriented Programming (AOP) to handle cross-cutting concerns such as transaction management, logging, and security. Instead of embedding these concerns directly into business logic, Spring separates them by wrapping beans with proxy objects.
When you annotate a class or method with@Transactional, Spring creates a proxy around the target bean. This proxy intercepts method calls and applies transactional behavior transparently.
During method execution, the proxy begins a transaction before the method runs, commits the transaction if the method completes successfully, and rolls it back if an exception occurs.
This mechanism relies on method interception. Only external method calls— those made through the proxy—are intercepted. Internal method calls within the same class (self-invocation) bypass the proxy and will not trigger transactional behavior.
By default, Spring rolls back transactions only for unchecked exceptions (subclasses of RuntimeException). Checked exceptions do not trigger a rollback unless explicitly configured.
Isolation Levels
Isolation levels define how transactions interact with each other when executed concurrently. They control the visibility of changes made by one transaction to others, directly impacting both data consistency and system performance. Choosing an appropriate isolation level involves balancing correctness guarantees against throughput and latency.
There are three primary anomalies that isolation levels aim to prevent. Dirty reads occur when a transaction reads data that has been modified but not yet committed by another transaction. Non-repeatable reads happen when the same query within a transaction returns different values read because the underlying data was modified by another transaction. Phantom reads occur when a query returns additional rows in a result set due to inserts made by another transaction during its execution.
The READ_UNCOMMITTED level provides the weakest isolation. Transactions can see uncommitted changes from others, which allows dirty reads and can lead to inconsistent or invalid data being processed.
The READ_COMMITTED level prevents dirty reads by ensuring that only committed data is visible. However, non-repeatable reads can still occur, as data may change between multiple reads within the same transaction.
The REPEATABLE_READ level guarantees that rows read within a transaction remain consistent, preventing both dirty reads and non-repeatable reads. However, phantom reads are still possible because new rows may be inserted by other transactions.
The SERIALIZABLE level provides the strongest isolation by ensuring full transaction isolation, as if transactions were executed sequentially. It prevents dirty reads, non-repeatable reads, and phantom reads, but typically comes with a significant performance cost due to increased locking or coordination.
In practice, READ_COMMITTED and REPEATABLE_READ are the most commonly used levels, offering a good balance between consistency and performance for most applications.
Difference Between @Bean and @Component
In the Spring Framework, both @Component and @Bean are used to register objects (beans) in the Spring IoC container. However, they differ in how beans are defined, discovered, and managed.
@Component is a class-level annotation. When applied to a class, it indicates that the class should be automatically detected through classpath scanning and registered as a bean. This approach is declarative and requires minimal configuration, making it ideal for application classes that you control.
In contrast, @Bean is a method-level annotation used within a configuration class (typically annotated with @Configuration). The method explicitly constructs and returns an object, and Spring registers the returned instance as a bean in the application context. This provides fine-grained control over object creation.
Unlike @Component, which relies on automatic scanning,@Bean requires explicit declaration. This makes it particularly useful when you need to configure complex initialization logic or integrate classes from third-party libraries where you cannot modify the source code to add annotations.
For example, when working with a third-party class, you can define a bean manually using a configuration class:
1@Configuration
2public class AppConfig {
3
4 @Bean
5 public MyBean myBean() {
6 return new MyBean();
7 }
8}
9
10public class MyBean {
11 // ...
12}In this example, the myBean() method explicitly creates and returns an instance of MyBean. Spring registers this instance as a managed bean, even though the class itself is not annotated.
In practice,@Component is preferred for most application-level classes due to its simplicity, while @Bean is used when explicit control over bean creation is required.
@Cacheable
The @Cacheable annotation in Spring Boot is used to transparently cache the results of method calls. When a method is invoked with a specific set of parameters, Spring first checks whether a cached result already exists. If a cached value is found, it is returned immediately without executing the method. Otherwise, the method is executed and its result is stored in the cache for subsequent calls.
Spring Boot automatically configures a CacheManager based on the caching libraries available on the classpath. By default, a simple in-memory implementation backed by a ConcurrentHashMap is used. For production systems, more robust solutions such as Caffeine or Redis are commonly used to support features like eviction policies, time-based expiration, and distributed caching. Other supported providers include JSR-107 (JCache), Ehcache, Hazelcast, and Infinispan.
Caching behavior is implemented using Spring's proxy-based AOP mechanism. This means that method calls must go through the proxy in order for caching to be applied. As a result, self-invocation (a method within the same class calling another @Cacheable method) will bypass the cache and execute the method directly.
Additionally, only public methods are eligible for proxy-based caching. Non-public methods cannot be intercepted, and therefore will not benefit from @Cacheable.
In practice, @Cacheable is most effective for read-heavy operations where method results are deterministic and expensive to compute, such as database queries or external API calls.
Spring AOP with @Aspect
@Aspect is a Spring Framework annotation used to implement Aspect-Oriented Programming (AOP). AOP allows you to separate cross-cutting concerns—such as logging, security, caching, transaction management, rate limiting, and idempotency—from your business logic.
Instead of duplicating the same code across multiple controllers or services, you can place the shared logic inside an aspect. Spring then automatically intercepts matching method calls and executes your aspect before, after, or around the target method.
A common use case is implementing idempotency for REST APIs. By creating a custom annotation such as @Idempotent, an aspect can transparently validate an idempotency key before the business method executes, preventing duplicate requests without modifying the controller itself.
1@Target(ElementType.METHOD)
2@Retention(RetentionPolicy.RUNTIME)
3public @interface Idempotent {
4 String headerName() default "X-Idempotency-Key";
5}The annotation can then be applied to any endpoint that should reject duplicate requests.
1@PostMapping("/execute")
2@Idempotent
3public CompletableFuture<ResponseEntity<String>> executeOrder(
4 @RequestBody OrderRequest request) {
5 return executionService
6 .executeMarketOrder(request)
7 .thenApply(ResponseEntity::ok);
8}The aspect intercepts every method annotated with @Idempotent, checks whether the request has already been processed, and either returns the cached response or allows the request to continue.
1@Aspect
2@Component
3public class IdempotencyAspect {
4
5 @Around("@annotation(idempotent)")
6 public Object enforceIdempotency(
7 ProceedingJoinPoint joinPoint,
8 Idempotent idempotent) throws Throwable {
9
10 String headerName = idempotent.headerName();
11
12 boolean duplicate = checkRedisForDuplicateKey(headerName);
13
14 if (duplicate) {
15 return getCachedResponse(headerName);
16 }
17
18 Object result = joinPoint.proceed();
19
20 saveResponseToRedis(headerName, result);
21
22 return result;
23 }
24
25 private boolean checkRedisForDuplicateKey(String header) {
26 return false;
27 }
28
29 private Object getCachedResponse(String header) {
30 return "Cached Response";
31 }
32
33 private void saveResponseToRedis(String header, Object response) {
34 }
35}How does it work?
When the Spring application starts, it scans for classes annotated with @Aspect. For beans that match the defined pointcuts, Spring creates a proxy object that wraps the original bean.
When a client invokes a controller or service method, the call is made to the proxy rather than directly to the target object. The proxy determines whether any aspects should execute for that method.
The @Around advice runs before the target method. It reads the configuration from the @Idempotent annotation, retrieves the idempotency key from the request, and checks Redis (or another data store) to determine whether the request has already been processed.
If the request is a duplicate, the aspect immediately returns the cached response without executing the business logic. Otherwise, it calls joinPoint.proceed(), which invokes the original controller or service method.
After the method completes successfully, the aspect stores the response in Redis using the idempotency key so that future duplicate requests can be served directly from the cache.
Common use cases
Logging request execution time, method parameters, and exceptions.
Authentication, authorization, rate limiting, and idempotency checks.
Transaction management, caching, metrics collection, and audit logging without polluting business logic.
@Async
@Async is a Spring annotation that allows a method to execute asynchronously. Instead of running on the caller's thread, the method is submitted to a TaskExecutor (typically backed by a thread pool), allowing the caller to continue executing immediately without waiting for the task to finish.
Asynchronous execution is useful for long-running or I/O-bound operations that do not need to block the request thread, such as sending emails, processing uploaded files, generating reports, or calling external APIs.
Before using @Async, asynchronous processing must be enabled by adding @EnableAsync to a configuration class.
1@SpringBootApplication
2@EnableAsync
3public class DemoApplication {
4 public static void main(String[] args) {
5 SpringApplication.run(DemoApplication.class, args);
6 }
7}An asynchronous method can either return a CompletableFuture if the caller needs the result later, or return void for fire-and-forget operations.
1@Service
2public class ReportService {
3 @Async
4 public CompletableFuture<String> generateComplexReport() {
5 return CompletableFuture.completedFuture(
6 "Data Report Content");
7 }
8}1@Service
2public class EmailService {
3 @Async
4 public void sendNotificationEmail(String userEmail) {
5 System.out.println(
6 "Running on: " +
7 Thread.currentThread().getName());
8 // Send email...
9 }
10}How does it work?
Similar to Spring AOP, Spring creates a proxy around beans containing @Async methods. When another bean invokes the method, the proxy intercepts the call and submits it to a TaskExecutor instead of executing it immediately.
The TaskExecutor selects a worker thread from its thread pool to execute the method, allowing the original thread to continue processing other work. If the method returns a CompletableFuture, the caller can continue executing and retrieve the result later.
Common use cases
Sending emails, SMS messages, or push notifications after a request has completed.
Calling slow third-party REST APIs or microservices without blocking the request thread.
Processing uploaded files, generating reports, or performing background data synchronization.
Things to watch out for
@Async only works when the method is invoked through the Spring proxy. Calling an @Async method from another method within the same class (self-invocation) bypasses the proxy, causing the method to execute synchronously.
By default, Spring uses a shared TaskExecutor. For production applications, it is recommended to configure a custom thread pool to control the number of worker threads, queue capacity, and rejection policy.
@Async is best suited for I/O-bound work. It does not automatically improve the performance of CPU-intensive computations, which may simply compete for CPU resources with other threads.