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.