Spring & Hibernate Interview Questions: What Really Happens Under the Hood?
After my previous blog on Java & Spring internals, one thing became pretty clear:
Knowing the annotation is easy.
Explaining what happens because of that annotation is where the real interview begins.
You can write:
@Transactional
But do you know how Spring actually starts the transaction?
You can add a Spring Boot dependency.
But do you know why Spring suddenly creates a bunch of beans?
You can modify a JPA entity without explicitly calling save().
But do you know why Hibernate still sends an UPDATE query?
And here's the one I really like:
Two users try to buy the last item in stock at exactly the same time. What happens?
These are the kinds of questions that move an interview from "Do you know Spring?" to "Do you actually understand Spring?"
So, let's go under the hood again.
🔹 Spring & Spring Boot
1. @Bean vs @Component — What's Actually Different?
At first glance, both seem to do the same thing:
"They create Spring beans."
But the way Spring discovers them is different.
@Component
@Component
public class PaymentService {
}
Spring discovers this through component scanning.
When Spring scans the package, it finds the class annotated with @Component and registers it as a bean.
The specialized annotations are built on the same idea:
@Component
├── @Service
├── @Repository
└── @Controller
So:
@Service
class PaymentService {
}
is essentially telling Spring:
"Discover this class during component scanning and manage it as a bean."
@Bean
With @Bean, you explicitly tell Spring what object to register.
@Configuration
public class AppConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
Here Spring doesn't need to discover ObjectMapper as a component.
Instead, it processes the configuration class and registers the object returned by the @Bean method.
So the simplest way I remember it:
@Component→ Spring discovers the class.@Bean→ You explicitly provide the object.
When would I use which?
For classes that I own and control, @Component / @Service is usually convenient.
For third-party classes:
ObjectMapper
RestClient
SomeExternalLibrary
I can't simply add @Component to their source code.
That's where:
@Bean
becomes useful.
Interview answer
"
@Componentis discovered through component scanning, whereas@Beanis explicitly declared in a configuration class and registers the object returned by the method. I generally use component scanning for application classes and@Beanwhen I need to configure or register objects, especially third-party classes."
2. How Does Spring Execute @Transactional Without You Writing Transaction Code?
This one sounds like magic until you understand AOP and proxies.
You write:
@Transactional
public void placeOrder() {
// database operations
}
You didn't write:
beginTransaction();
commit();
rollback();
So who is doing it?
Spring's proxy infrastructure.
Conceptually:
Your Code
|
v
Spring Proxy
|
+---- Begin / Join Transaction
|
v
Actual Bean
|
+---- placeOrder()
|
v
Commit / Rollback
When another bean calls:
orderService.placeOrder();
the call can go through a Spring-generated proxy.
The proxy's transaction interceptor essentially does:
1. Start or join transaction
2. Call actual method
3. If successful → commit
4. If appropriate exception → rollback
This is one of the practical uses of AOP.
3. So What Exactly Is AOP?
AOP stands for Aspect-Oriented Programming.
The basic idea is:
Some behavior is needed across many unrelated parts of an application.
For example:
Logging
Security
Transactions
Metrics
Auditing
Tracing
Imagine 100 service methods.
Do we really want:
log();
startTransaction();
actualBusinessLogic();
commitTransaction();
log();
inside every method?
Obviously not.
Instead, we can separate the cross-cutting concern.
Aspect
|
+---------+---------+
| | |
Service A Service B Service C
The aspect can intercept method execution and apply additional behavior.
That's why things like:
@Transactional
@Cacheable
@PreAuthorize
can work without us manually putting infrastructure code into every business method.
A real-world example
Suppose every payment operation needs auditing.
Instead of:
public void payment() {
audit();
// business logic
}
in every method, we can create an aspect that intercepts payment-related operations and performs the audit automatically.
Interview answer
"AOP separates cross-cutting concerns such as logging, security, transactions and auditing from business logic. Spring commonly implements AOP using proxies, allowing additional behavior to execute before, after or around method invocation."
4. Bean A Needs Bean B. Bean B Needs Bean A. What Happens?
Consider:
@Service
class A {
private final B b;
A(B b) {
this.b = b;
}
}
and:
@Service
class B {
private final A a;
B(A a) {
this.a = a;
}
}
Now Spring tries to create:
A
↓
needs B
↓
B
↓
needs A
↓
A
↓
...
We have a circular dependency.
Does Spring always solve it?
No.
And this is where the distinction between constructor injection and other forms of injection becomes important.
Constructor-based circular dependencies generally cannot be resolved simply because Spring needs both objects to be fully constructed before either constructor can complete.
You can end up with an error such as:
BeanCurrentlyInCreationException
Spring has historically been able to resolve some circular dependencies involving setter/field injection by exposing an early reference to a bean.
But relying on circular dependencies is generally a bad design.
Better solution?
Usually, rethink the design.
If:
A → B
B → A
exists, there may be a missing abstraction.
For example:
A → C ← B
or an event-based design may remove the direct dependency.
Interview answer
"Spring can resolve some circular dependencies, particularly with certain forms of property injection by exposing early bean references, but constructor-based circular dependencies generally fail because both objects need to be created before either constructor can complete. In practice, I'd treat circular dependencies as a design smell and refactor the dependency graph."
5. You Added One Dependency. Why Did Spring Suddenly Create Dozens of Beans?
This is Spring Boot auto-configuration.
Suppose you add:
spring-boot-starter-web
and suddenly Spring configures things related to:
Spring MVC
Jackson
Embedded Tomcat
Message converters
DispatcherServlet
Did Spring randomly decide to create them?
No.
Spring Boot uses conditional configuration.
Conceptually:
Classpath
|
v
What dependencies are available?
|
v
What configuration exists?
|
v
Which conditions match?
|
v
Create appropriate beans
For example, a configuration may effectively say:
If a particular class exists and a particular bean doesn't already exist, configure this bean.
This is why conditions such as:
@ConditionalOnClass
@ConditionalOnMissingBean
@ConditionalOnProperty
are so important in Spring Boot.
And this is the important part:
Auto-configuration doesn't mean Spring always creates everything.
It is conditional.
If you provide your own bean, auto-configuration can often back off.
That's why you can customize Spring Boot instead of fighting it.
Interview answer
"Spring Boot auto-configuration uses conditional configuration based on the classpath, existing beans and application properties. It provides sensible defaults and backs off when the application provides its own configuration in applicable cases."
6. application.properties vs application.yml — What's Actually Happening?
These two:
application.properties
application.yml
are just different ways of expressing configuration.
For example:
server.port=8081
spring.datasource.url=...
can be represented in YAML as:
server:
port: 8081
spring:
datasource:
url: ...
But here's the important question:
How does that configuration reach my Java objects?
Spring Boot loads configuration from its environment/property sources.
Those properties become available through Spring's configuration infrastructure.
You can access them using:
@Value("${server.port}")
or, for structured configuration, preferably:
@ConfigurationProperties
For example:
@ConfigurationProperties(prefix = "payment")
public class PaymentProperties {
private String url;
private int timeout;
}
Now configuration can be bound to a typed object.
This becomes particularly useful when applications have many configuration values.
Practical takeaway
For simple values:
@Value
can be fine.
For groups of related configuration:
@ConfigurationProperties
is generally cleaner and type-safe.
And externalized configuration means we don't need to hardcode environment-specific values.
The same application can run with:
Development → DB A
Testing → DB B
Production → DB C
without changing the application code.
7. ApplicationContext vs BeanFactory
Both are Spring containers.
But:
BeanFactory
↓
basic IoC container
ApplicationContext
↓
BeanFactory +
more enterprise/application features
ApplicationContext builds on BeanFactory functionality and provides additional capabilities such as:
event publication
message/source support
integration with Spring's application infrastructure
more complete support for annotation-based configuration
In typical Spring Boot applications, you interact with:
ApplicationContext
rather than directly using BeanFactory.
Interview answer
"
BeanFactoryis the basic IoC container, whileApplicationContextextends that functionality with features such as application events, message resolution and broader application integration. In normal Spring Boot applications, ApplicationContext is the container we generally work with."
8. How Would You Secure a REST API Using JWT and OAuth2?
Now let's move from internals to architecture.
Imagine:
Client
|
v
REST API
We need:
authentication
authorization
token expiry
secure endpoints
role-based access
A common architecture is:
Client
|
| authenticate
v
Authorization Server
|
| Access Token
v
Client
|
| Bearer Token
v
Resource Server
The access token can be a JWT.
A JWT typically contains claims such as:
sub
iss
exp
roles/scopes
The resource server validates the token before allowing access.
For example:
Authorization: Bearer <token>
The important distinction is:
OAuth2 is an authorization framework/protocol.
JWT is a token format.
They aren't interchangeable concepts.
You can use OAuth2 with JWT access tokens, but OAuth2 itself isn't synonymous with JWT.
Interview answer
"I'd typically use OAuth2 for the authorization flow and configure the API as an OAuth2 Resource Server. The client sends an access token, commonly a JWT, in the Authorization header. Spring Security validates the token, checks its claims/scopes or authorities, and then allows or denies access to the endpoint."
🔹 JPA / Hibernate
Now things get more interesting.
Because Hibernate is one of those technologies where:
Your Java code can look simple while the database is doing a lot more work than you realize.
9. You Requested an Entity That Doesn't Exist. Why Can get() and load() Behave Differently?
Historically, Hibernate provided:
session.get(User.class, id);
session.load(User.class, id);
The key conceptual difference:
get()
get() attempts to retrieve the actual entity.
If it doesn't exist:
null
is returned.
load()
load() can return a proxy without immediately fetching the row.
Conceptually:
load(User.class, 10)
|
v
User Proxy
|
access property
|
v
Database query
If the row doesn't exist, accessing the proxy can eventually result in an object-not-found exception.
Important modern note
In newer Hibernate versions, Session.load() has been deprecated in favor of APIs such as getReference() for obtaining a reference/proxy.
So if an interviewer asks about get() vs load(), I'd explain the underlying concept and mention the modern equivalent.
Interview answer
"
get()retrieves the entity and returns null if it doesn't exist. The olderload()API could return a proxy without immediately hitting the database, with the existence check potentially happening when the proxy is accessed. In modern Hibernate,getReference()is the preferred API for obtaining a reference."
10. Why Can Hibernate Return an Entity Without Hitting the Database?
This brings us to caching.
Hibernate has a first-level cache associated with the persistence context.
Suppose:
User user1 = entityManager.find(User.class, 10L);
User user2 = entityManager.find(User.class, 10L);
You might expect two SQL queries.
But within the same persistence context, Hibernate can return the same managed entity from its first-level cache.
Conceptually:
Persistence Context
ID 10
|
+---- User object
First request:
Database → Entity → Persistence Context
Second request:
Persistence Context → Entity
No second database query is required.
What about second-level cache?
That's different.
The first-level cache belongs to the persistence context/session.
The second-level cache is associated with the broader Hibernate SessionFactory and can be shared across sessions when configured with a supported cache provider.
Conceptually:
Session A ──┐
|
Session B ──┼──> Second-Level Cache
|
Session C ──┘
Interview answer
"The first-level cache is tied to the persistence context and is enabled by default. It prevents repeated database access for the same entity within that context. The second-level cache is shared across sessions and is optional; it must be explicitly configured and is useful for suitable read-heavy data."
11. You Changed an Entity Without Calling save(). Why Did Hibernate Still Execute UPDATE?
Dirty checking.
This is one of the most important Hibernate concepts to understand.
Suppose:
@Transactional
public void updateUser(Long id) {
User user = entityManager.find(User.class, id);
user.setName("Saurabh");
}
Notice:
No save().
Yet Hibernate can still execute:
UPDATE user
SET name = 'Saurabh'
WHERE id = ?
Why?
Because the entity is managed.
When Hibernate loads the entity, it keeps track of its state.
Conceptually:
Original state
name = "Rahul"
↓
Application changes entity
↓
Current state
name = "Saurabh"
↓
Transaction flush
↓
Hibernate compares state
↓
UPDATE
This is dirty checking.
At flush time, Hibernate detects that the managed entity has changed and generates the appropriate SQL.
This is why save() isn't synonymous with "make Hibernate update the database."
In Spring Data JPA, save() delegates to persistence operations based on whether the entity is considered new, but managed entities can be dirty-checked without explicitly calling save().
Interview answer
"Hibernate performs dirty checking on managed entities. It keeps track of their state within the persistence context and, during flush, detects changes and generates the required SQL. That's why a managed entity can be updated without explicitly calling save()."
12. Entity Lifecycle: Transient → Persistent → Detached → Removed
An entity doesn't simply exist in one state.
Understanding these states explains a lot of Hibernate behavior.
1. Transient
You create an object:
User user = new User();
Hibernate doesn't know about it yet.
Java object
|
X
Persistence Context
That's transient.
2. Persistent / Managed
You persist it:
entityManager.persist(user);
Now Hibernate manages it.
Persistence Context
|
v
User
Changes to the entity can be tracked.
3. Detached
The entity was managed, but is no longer attached to the current persistence context.
For example, the persistence context ends.
Persistence Context closes
|
v
User
|
Detached
Changes made while detached aren't automatically tracked by that context.
4. Removed
You mark it for deletion:
entityManager.remove(user);
The entity enters the removed state, and the corresponding SQL DELETE is generally executed when the persistence context is flushed.
The lifecycle
new
|
v
Transient
|
persist()
|
v
Persistent
/ \
/ \
detach() remove()
| |
v v
Detached Removed
This lifecycle is incredibly useful when debugging JPA behavior.
13. Two Users Buy the Last Product. How Does JPA Prevent Both From Succeeding?
Now let's take a real scenario.
Database:
Product
id = 101
stock = 1
Two users arrive simultaneously.
User A → stock = 1
User B → stock = 1
Without proper concurrency control:
A reads 1
B reads 1
A → writes 0
B → writes 0
Both orders might succeed.
But we only had one product.
This is a classic concurrency problem.
JPA gives us locking mechanisms to address this.
14. Optimistic Locking vs Pessimistic Locking
Optimistic Locking
We assume conflicts are relatively uncommon.
Add a version field:
@Version
private Long version;
Suppose:
Product
stock = 1
version = 5
Two transactions read it.
Both see:
version = 5
User A updates first:
version 5 → 6
User B then tries to update using the old version:
UPDATE product
SET stock = 0,
version = 6
WHERE id = 101
AND version = 5
But version 5 no longer exists.
The update affects zero rows.
Hibernate detects the conflict and throws an optimistic locking exception.
Conceptually:
A → version 5 → SUCCESS → version 6
B → version 5 → CONFLICT ❌
This is excellent when conflicts are possible but not constant.
Pessimistic Locking
Here we assume conflicts are likely enough that we want the database to lock the row.
For example:
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Product> findById(Long id);
Conceptually:
Transaction A
|
v
Lock Product
|
v
Update stock
|
v
Commit
|
v
Unlock
Transaction B
|
v
WAIT
The database controls the lock.
This can prevent concurrent transactions from modifying the same row simultaneously, but it can reduce concurrency and increase the risk of lock contention/deadlocks if used carelessly.
So which one should we choose?
A simple rule:
Optimistic locking
→ conflicts are relatively infrequent
→ high concurrency desired
→ retry/conflict handling is acceptable
Pessimistic locking
→ conflicts are frequent or critical
→ holding a database lock is acceptable
→ we need stronger immediate serialization around the resource
15. Scenario: Designing an Order System with Independent Services
Let's make this more realistic.
Suppose we have:
Order
Inventory
Payment
Invoice
When an order is placed:
Order Created
|
+----> Inventory Update
|
+----> Payment
|
+----> Invoice
We don't necessarily want the Order service to directly know the implementation details of all three services.
That's where events can help.
For example:
public record OrderCreatedEvent(Long orderId) {}
After the order is created, we can publish an event.
Listeners can react:
Order Service
|
| OrderCreatedEvent
|
+----> Inventory
|
+----> Payment
|
+----> Invoice
This reduces direct coupling.
Where does AOP fit?
AOP is useful for cross-cutting behavior around these operations:
Logging
Metrics
Security
Auditing
Transactions
Tracing
For example, an aspect can measure:
Payment processing time
Inventory processing time
Invoice generation time
without putting timing code inside every service.
But there's an important production consideration.
If these are genuinely independent services, Spring's in-process application events are not enough for reliable cross-service communication.
For distributed systems, we'd generally look at something such as:
Kafka
RabbitMQ
or another messaging mechanism.
And if database state and event publishing must remain reliable together, patterns such as the Transactional Outbox become important.
That's the kind of distinction that separates a basic Spring answer from a production architecture answer.
The Interview Pattern I Keep Coming Back To
There is a common pattern across almost all of these questions.
The interviewer doesn't really want you to recite:
"Dirty checking means Hibernate automatically detects changes."
They want to know whether you can reason through:
Entity loaded
↓
Persistence Context
↓
Entity modified
↓
Transaction flush
↓
Dirty checking
↓
SQL generated
Similarly, they don't just want:
"Spring Boot has auto-configuration."
They want:
Dependency added
↓
Classpath detected
↓
Conditions evaluated
↓
Auto-configuration applied
↓
Beans created
And for @Transactional:
Method called
↓
Spring Proxy
↓
Transaction Interceptor
↓
Target method
↓
Commit / Rollback
Once you start thinking in these flows, Spring and Hibernate become much easier to reason about.
Final Takeaway
For me, the biggest shift in preparing for Java/Spring interviews has been this:
Don't stop at:
"What does this annotation do?"
Ask:
"Who processes this annotation?"
Then:
"When does that happen?"
And finally:
"What happens internally because of it?"
Because:
@Transactional
is easy to remember.
Understanding Spring proxies and transaction interceptors is what makes the answer interesting.
@Version
is easy to remember.
Understanding how Hibernate prevents lost updates is what makes the answer useful.
And:
@Autowired
is easy to use.
Understanding how Spring builds and manages the dependency graph is what makes you comfortable debugging a real application.
That's the difference I'm trying to focus on with these posts:
Don't just know the framework.
Understand what the framework is doing for you.
👍
ReplyDelete