Skip to main content

Spring & Hibernate Interview Questions: What Really Happens Under the Hood?

 

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

"@Component is discovered through component scanning, whereas @Bean is explicitly declared in a configuration class and registers the object returned by the method. I generally use component scanning for application classes and @Bean when 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

"BeanFactory is the basic IoC container, while ApplicationContext extends 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 older load() 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.

Comments

Post a Comment

Popular posts from this blog

Java & Spring Interview Questions: What Really Happens Under the Hood

  Java & Spring Interview Questions: What Really Happens Under the Hood There is a phase in every developer's career where you can use something without actually understanding what is happening underneath. You know how to use ConcurrentHashMap . You know what @Transactional does. You know that Spring injects dependencies. You know @SpringBootApplication starts your application. And then the interviewer asks: "Okay, but what actually happens internally?" Suddenly, knowing the syntax isn't enough. That is exactly what this blog is about. I'm going to break down some of the Java and Spring concepts that are frequently asked in interviews, but more importantly, I'll try to understand why they work the way they do . Let's start with one of my favourites. 1. ConcurrentHashMap in Java 8: What Actually Happens? Let's first establish the problem. A normal HashMap is not thread-safe. Imagine two threads doing this at the same time: map.put("A",...

Think You Know Java? These 5 Questions Say Otherwise

  Hello everyone! Hope you’re sipping your coffee just right—because I’ve brewed something equally strong for your brain. Welcome to a blog that dives into 5 deceptively tricky Java interview questions —the kind that test not just what you know, but how well you understand Java under the hood. If you, like me, share a love-hate relationship with this language, this blog might just hit home. So, let's get started... Question 1: What will be the output of the following code? Why? class Parent { static void staticMethod() { System.out.println("Static method in Parent"); } private void privateMethod() { System.out.println("Private method in Parent"); } void show() { staticMethod(); privateMethod(); } } class Child extends Parent { static void staticMethod() { System.out.println("Static method in Child"); } private void privateMethod() { System.out.println("Pr...