Skip to main content

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", 10);
map.put("B", 20);

The problem isn't simply that both threads are writing.

The bigger issue is that the internal structure of the map can be modified concurrently, and without proper synchronization, another thread may observe an inconsistent state.

So naturally, the next question is:

Why not just synchronize the entire HashMap?

We could.

But then every operation would effectively wait for every other operation.

That's where ConcurrentHashMap comes in.

Java 7 vs Java 8

This is an important interview distinction.

Older implementations of ConcurrentHashMap used segments.

Java 8 moved away from the segment-based design.

In Java 8, the structure is broadly:

ConcurrentHashMap
       |
       v
   Node[] table
       |
       +---- Node
       |
       +---- Node
       |
       +---- TreeBin

The table contains buckets, similar to HashMap.

Each bucket can contain:

  • a linked list of nodes

  • or a tree structure when collisions become large

So where is the concurrency?

This is the interesting part.

ConcurrentHashMap does not lock the entire map for every operation.

For many reads, it uses non-blocking techniques.

For updates, synchronization is performed at a much smaller granularity, typically around the affected bucket/bin.

Conceptually:

Thread 1 -> Bucket 3 -> lock/update
Thread 2 -> Bucket 7 -> lock/update

Both can proceed

Instead of:

Thread 1 -> lock entire map
Thread 2 -> WAIT

This dramatically improves concurrency.

CAS also plays a major role

Java 8's implementation makes heavy use of CAS (Compare-And-Swap).

For example, if a bucket is empty, a thread can attempt to place a new node there atomically.

Conceptually:

if bucket == null
    CAS(bucket, null, newNode)

If another thread changed the bucket first, the CAS fails and the operation retries using the updated state.

So the implementation combines techniques such as:

  • CAS

  • volatile

  • synchronized blocks on individual bins when necessary

  • special nodes for resizing/tree structures

What about reads?

One of the biggest advantages is that reads generally don't require locking.

That's extremely useful when your application has:

1000 readers
100 writers

instead of making every reader fight for a global lock.

One important interview point

ConcurrentHashMap does not allow null keys or null values.

Why?

Because in a concurrent map, null can create ambiguity.

Consider:

map.get("A")

If the result is null, does that mean:

1. Key doesn't exist

or:

2. Key exists and its value is null

In a concurrent environment, that ambiguity becomes problematic, so ConcurrentHashMap simply doesn't allow nulls.

Interview answer

If I had to answer this in an interview:

"ConcurrentHashMap provides thread-safe access without synchronizing the entire map. In Java 8, it uses a bucket-based structure with CAS for some operations and synchronized locking at the bin level for updates when required. Reads are generally non-blocking, which gives much better concurrency than synchronizing a complete HashMap."

That is usually a much stronger answer than:

"ConcurrentHashMap is a thread-safe HashMap."


2. HashMap vs ConcurrentHashMap: Why Do We Need It?

Let's make the difference practical.

Suppose we have:

Map<String, Integer> map = new HashMap<>();

If multiple threads modify this map concurrently, there is no guarantee of thread safety.

You might think:

"Fine, I'll just use Collections.synchronizedMap()."

That works for basic thread safety, but it uses synchronization around map operations.

Conceptually:

Thread A
   |
   v
[ LOCK ]
   |
HashMap
   |
[UNLOCK]

Thread B
   |
   v
WAIT

With ConcurrentHashMap, the implementation allows much greater concurrency.

Quick comparison

FeatureHashMapConcurrentHashMap
Thread-safeNoYes
Null keyYesNo
Null valuesYesNo
Concurrent readsUnsafe contractSupported
Concurrent writesUnsafeSupported
LockingNoneFine-grained/internal concurrency mechanisms
Typical useSingle-threaded/local dataShared concurrent data

The important thing isn't:

"Which one is faster?"

The important question is:

"Do multiple threads access and modify this data?"

If yes, thread safety becomes part of the design.


3. Fail-Fast vs Fail-Safe Iterators

This question sounds simple until someone asks:

"What actually makes an iterator fail-fast?"

Let's look at ArrayList.

Internally, ArrayList maintains a modification count:

modCount

When the collection is structurally modified, the count changes.

An iterator captures the expected modification count:

ArrayList
   |
modCount = 5

Iterator
   |
expectedModCount = 5

Now imagine:

List<Integer> list = new ArrayList<>();

list.add(10);
list.add(20);

Iterator<Integer> iterator = list.iterator();

list.add(30);

The collection's:

modCount = 6

But the iterator still has:

expectedModCount = 5

When the iterator performs its next operation, it checks something conceptually similar to:

if (modCount != expectedModCount) {
    throw new ConcurrentModificationException();
}

That's the basic idea behind fail-fast behavior.

But there is an important catch

Fail-fast is best effort.

It is not a thread-safety mechanism.

You should never say:

"ArrayList is safe because it throws ConcurrentModificationException."

No.

The exception is basically there to help detect unexpected structural modification during iteration.


What is fail-safe then?

"Fail-safe" is a commonly used interview term, although the Java documentation doesn't formally define every concurrent iterator under that exact category.

The idea is that the iterator doesn't directly depend on a structure that is being modified in the same way.

For example:

CopyOnWriteArrayList<Integer> list =
        new CopyOnWriteArrayList<>();

When the list is modified, a new underlying array can be created.

Existing iterators continue operating on the snapshot they already obtained.

So conceptually:

Original array
[10, 20, 30]
       |
       +---- Iterator sees this

Modification
       |
       v

New array
[10, 20, 30, 40]

The iterator doesn't suddenly start walking the new array.

Interview answer

"Fail-fast iterators generally detect structural modification using a modification count and may throw ConcurrentModificationException. Fail-safe is commonly used to describe iterators such as CopyOnWriteArrayList's iterator, which can iterate over a snapshot and therefore doesn't fail in the same way when the collection is modified."


4. Functional Interface: Can It Have Default and Static Methods?

A functional interface is an interface with exactly one abstract method.

For example:

@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}

Now we can do:

Calculator addition = (a, b) -> a + b;

That's where lambdas come in.

But here's the interview trap.

Someone asks:

"Can a functional interface contain other methods?"

Yes.

It can contain:

  • exactly one abstract method

  • multiple default methods

  • multiple static methods

For example:

@FunctionalInterface
interface Calculator {

    int calculate(int a, int b);

    default void print() {
        System.out.println("Calculator");
    }

    static void info() {
        System.out.println("Utility method");
    }
}

This is still a functional interface because it has only one abstract method.

Why?

Because default and static methods already have implementations.

The lambda only needs to provide the implementation for the single abstract method.

Common examples

Java provides many functional interfaces:

Predicate<T>
Function<T, R>
Consumer<T>
Supplier<T>

For example:

Predicate<Integer> isEven =
        n -> n % 2 == 0;

This becomes extremely useful with Streams.


5. Try-With-Resources: What Happens When Both Try and Close Throw?

This is one of those questions where the interviewer is checking whether you actually understand Java exception handling.

Consider:

try (MyResource resource = new MyResource()) {
    throw new RuntimeException("Try failed");
}

And suppose:

resource.close();

also throws:

RuntimeException("Close failed");

Which exception do we get?

The exception from the try block is the primary exception.

The exception from close() becomes a suppressed exception.

Conceptually:

Primary Exception
      |
      +--- suppressed: Close Exception

You can retrieve suppressed exceptions using:

exception.getSuppressed();

Why was this designed this way?

Because the original operation is usually more important.

Imagine:

Database operation failed
+
Connection cleanup failed

The actual business failure shouldn't disappear just because cleanup also failed.

What does try-with-resources require?

The resource must implement:

AutoCloseable

or:

Closeable

For example:

try (BufferedReader reader =
         new BufferedReader(new FileReader("data.txt"))) {

    // use resource
}

Java automatically calls:

reader.close();

even if an exception occurs.

And if multiple resources exist, they are closed in reverse order.

try (
    ResourceA a = ...;
    ResourceB b = ...
) {
}

Closing order:

B
A

6. Major Java 8 Features and Where We Actually Use Them

Java 8 was a pretty big shift in how Java code was written.

The features I would be ready to discuss in an interview are:

Lambda Expressions

Before:

Collections.sort(list, new Comparator<User>() {
    @Override
    public int compare(User a, User b) {
        return a.getAge() - b.getAge();
    }
});

After:

list.sort((a, b) -> a.getAge() - b.getAge());

Useful when:

  • passing behavior

  • callbacks

  • collection operations

  • streams


Functional Interfaces

They provide the target type for lambdas.

Predicate<User> adult =
        user -> user.getAge() >= 18;

Stream API

Instead of manually iterating:

List<String> names = new ArrayList<>();

for (User user : users) {
    if (user.getAge() > 18) {
        names.add(user.getName());
    }
}

We can express the operation as:

List<String> names = users.stream()
        .filter(user -> user.getAge() > 18)
        .map(User::getName)
        .toList();

The key idea isn't:

"Streams are shorter."

The real advantage is that they allow us to express what we want to do rather than manually controlling every iteration step.


Optional

Optional was introduced to represent the possible absence of a value.

Instead of:

User user = repository.findById(id);

if (user != null) {
    ...
}

we can have:

Optional<User> user =
        repository.findById(id);

And:

user.map(User::getName)
    .orElse("Unknown");

It doesn't magically eliminate every NullPointerException, but it can make absence explicit in APIs.


Default and Static Interface Methods

Default methods allowed interfaces to evolve without forcing every existing implementation to immediately implement a new method.

interface Vehicle {

    default void start() {
        System.out.println("Starting");
    }
}

This was especially important for evolving Java's collection APIs.


New Date and Time API

Instead of the older problematic date APIs:

Date
Calendar

Java 8 introduced:

LocalDate
LocalDateTime
Instant
ZonedDateTime
Duration
Period

Example:

LocalDate today = LocalDate.now();

Much cleaner and generally safer because the new API is immutable and better designed.


7. What Happens When @SpringBootApplication Is Executed?

This is where Spring Boot starts becoming interesting.

We write:

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

And somehow...

Spring creates the application.

Finds beans.

Injects dependencies.

Starts Tomcat.

Loads configuration.

Runs our application.

But what actually happened?

Let's break it down.


Step 1: main() executes

Java starts here:

SpringApplication.run(Application.class, args);

Spring Boot creates a SpringApplication instance.


Step 2: Application context is created

Spring creates the appropriate ApplicationContext.

For a web application, this eventually becomes a web-aware application context.

Think of the ApplicationContext as the container responsible for managing our Spring beans.


Step 3: Configuration is discovered

This annotation:

@SpringBootApplication

is effectively a combination of:

@Configuration
@EnableAutoConfiguration
@ComponentScan

That's a very common interview question.

@Configuration

Tells Spring:

This class can provide bean definitions.

@ComponentScan

Tells Spring to scan packages for components such as:

@Component
@Service
@Repository
@Controller
@RestController

@EnableAutoConfiguration

This is where Spring Boot becomes powerful.

Spring Boot looks at:

  • classpath dependencies

  • configuration

  • conditions

and automatically configures appropriate beans.

For example, if Spring MVC dependencies are present, Spring Boot can configure the infrastructure required for a web application.


Step 4: Bean definitions are created

Spring identifies beans and registers their definitions in the container.

For example:

@Service
public class UserService {
}

Spring knows:

Bean name -> userService
Bean type -> UserService
Scope -> singleton by default

Step 5: Dependencies are resolved

Suppose:

@Service
class UserService {

    private final UserRepository repository;

    UserService(UserRepository repository) {
        this.repository = repository;
    }
}

Spring sees:

UserService
      |
      v
UserRepository

It resolves the dependency and creates the object graph.


Step 6: Embedded server starts

For a Spring Boot web application, an embedded server such as Tomcat is started.

So instead of manually deploying a WAR into an external server, your application can start with:

java -jar application.jar

and the embedded server comes up as part of application startup.


8. How Does Spring Dependency Injection Actually Work?

Let's say we have:

@Service
class OrderService {

    private final PaymentService paymentService;

    OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

We didn't write:

new PaymentService();

So who created it?

Spring.

The basic process is:

Application starts
       |
       v
Component scanning
       |
       v
Bean definitions
       |
       v
Dependency resolution
       |
       v
Bean creation
       |
       v
Dependency injection

Spring's BeanFactory / ApplicationContext infrastructure manages this process.

Constructor injection

When Spring needs to create OrderService, it sees that its constructor requires:

PaymentService

Spring checks its container.

If a suitable bean exists:

PaymentService bean
       |
       v
OrderService constructor

Spring passes that bean into the constructor.

This is why constructor injection is generally preferred.

The dependencies become explicit, and the object can be created only when all required dependencies are available.


9. @Transactional and Spring Proxies

This one is probably responsible for more interview confusion than almost anything else in Spring.

You write:

@Transactional
public void transferMoney() {
    ...
}

And Spring magically manages:

BEGIN TRANSACTION
       |
method()
       |
COMMIT

But how?

The answer is:

A proxy.

Conceptually:

Your code
   |
   v
Spring Proxy
   |
   +-- Begin transaction
   |
   v
Actual Service
   |
   +-- execute method
   |
   v
Commit / Rollback

Spring creates a proxy around the bean.

When another bean calls:

service.transferMoney();

the call goes through the proxy.

The proxy's transaction interceptor detects @Transactional.

Then:

1. Start transaction
2. Invoke actual method
3. If successful -> commit
4. If appropriate exception -> rollback
5. End transaction

The famous self-invocation problem

Consider:

public void methodA() {
    methodB();
}

@Transactional
public void methodB() {
}

If methodA() and methodB() belong to the same object, the call:

methodB();

is effectively:

this.methodB()

It does not go through the Spring proxy.

Therefore, the transactional interceptor may never get a chance to intercept that call.

This is why self-invocation can cause @Transactional to appear as if it isn't working.

Interview answer

"Spring generally implements declarative transactions using proxies and transaction interceptors. The proxy intercepts calls to transactional methods, starts or joins a transaction, invokes the target method, and then commits or rolls back based on the outcome. A common limitation is self-invocation because an internal this.method() call bypasses the proxy."

That answer shows actual understanding.


10. Spring Bean Lifecycle

Here's another question where memorizing annotations isn't enough.

Suppose Spring creates:

@Service
public class PaymentService {
}

What happens?

Conceptually:

Bean Definition
      |
      v
Instantiate
      |
      v
Populate dependencies
      |
      v
Aware callbacks
      |
      v
BeanPostProcessor - Before
      |
      v
@PostConstruct
      |
      v
BeanPostProcessor - After
      |
      v
Bean Ready
      |
      v
Application running
      |
      v
@PreDestroy
      |
      v
Bean destroyed

Let's simplify it into something interview-friendly.

1. Instantiation

Spring creates the object.

new PaymentService(...)

conceptually.

2. Dependency injection

Spring provides required dependencies.

3. Aware interfaces

If the bean implements interfaces such as:

BeanNameAware
ApplicationContextAware

Spring can provide corresponding container information.

4. BeanPostProcessor

Spring gives post-processors a chance to modify or wrap the bean.

This is extremely important because Spring uses this infrastructure for many features.

5. @PostConstruct

After dependency injection, initialization logic can execute:

@PostConstruct
public void init() {
}

6. Bean is ready

The bean can now be used by the application.

7. Destruction

When the application context shuts down:

@PreDestroy
public void cleanup() {
}

can be called for applicable beans.


11. Spring Bean Scopes

The default scope is:

singleton

But singleton here means:

One bean instance per Spring ApplicationContext.

It does not necessarily mean one object for the entire JVM.

Common scopes include:

Singleton

@Scope("singleton")

One instance per container.

Prototype

@Scope("prototype")

A new instance is created each time the bean is requested from the container.

Request

One instance per HTTP request.

Useful in web applications.

Session

One instance per HTTP session.

Application

One instance per ServletContext.

WebSocket

One instance per WebSocket lifecycle.

The important interview distinction is:

Singleton and prototype describe Spring bean lifecycle management, not the Gang of Four Singleton design pattern.


12. JPA N+1 Query Problem

Now let's move to something that can quietly destroy application performance.

Suppose we have:

@Entity
class Department {

    @OneToMany
    private List<Employee> employees;
}

And we fetch:

List<Department> departments =
        departmentRepository.findAll();

Suppose we have 100 departments.

You might expect:

1 query

But then accessing:

department.getEmployees()

for each department can trigger:

1 query -> fetch departments

100 queries -> fetch employees

Total:

101 queries

That's the N+1 problem.

And here's the dangerous part:

Your Java code looks completely innocent.

for (Department department : departments) {
    department.getEmployees();
}

But underneath, Hibernate may be firing query after query.


How do we solve it?

There isn't one universal solution.

The right solution depends on the use case.

1. JOIN FETCH

For example:

@Query("""
    SELECT d
    FROM Department d
    JOIN FETCH d.employees
""")
List<Department> findDepartmentsWithEmployees();

This tells Hibernate to fetch the relationship in the same query.

Conceptually:

Before:

Department query
      |
      +--> Employee query
      +--> Employee query
      +--> Employee query
      +--> ...

After:

Department JOIN Employee

2. Entity Graph

You can use:

@EntityGraph(attributePaths = {"employees"})

This can be cleaner when you want to control fetching without writing custom JPQL for every case.

3. Batch fetching

Hibernate can fetch related entities in batches rather than one at a time.

Instead of:

1 + 100 queries

you may reduce the number significantly by fetching groups of relationships.

4. DTO projections

And this is one I particularly like when building APIs.

If your endpoint only needs:

Department ID
Department Name
Employee Count

why load the entire entity graph?

Use a projection/DTO that retrieves exactly what the API needs.

That's not just an N+1 solution.

It's often a better architectural decision.


The Bigger Picture

Notice something interesting.

These questions look unrelated:

ConcurrentHashMap
Try-with-resources
Functional interfaces
Spring DI
@Transactional
Bean lifecycle
JPA N+1

But they're actually testing the same thing.

Do you understand what your code is causing underneath?

When you write:

map.put()

what happens inside the data structure?

When you write:

@Transactional

who actually intercepts your method?

When you write:

@Autowired

who creates the object?

When you access:

department.getEmployees()

how many SQL queries are actually going to the database?

That's the level at which framework knowledge starts becoming engineering knowledge.


A Simple Interview Strategy

One thing that has helped me is answering these questions in layers.

Don't immediately dump everything you know.

Start with the simple answer.

Then explain the internal mechanism.

Then mention the important edge case.

For example:

Interviewer:

"How does @Transactional work?"

Weak answer:

"It manages transactions automatically."

Better:

"Spring uses proxies and transaction interceptors to manage declarative transactions."

Stronger:

"When a transactional method is invoked through the Spring proxy, the transaction interceptor starts or joins a transaction, invokes the target method, and commits or rolls back depending on the outcome. Self-invocation is a common limitation because internal calls bypass the proxy."

That progression shows something much more valuable than memorization.

It shows that you understand what happens behind the annotation.

And honestly, that's where most framework interviews eventually go.

Because knowing:

@Transactional

is easy.

Understanding why it sometimes doesn't work?

That's the real interview question.

Comments

Popular posts from this blog

Physics wallah ~ The legend

  "Helloooo Bachchooo.....! If you can feel these words, then you already have a glimpse of what this blog in particular talks about. Yup! It's about our beloved and most revolutionary teacher Alakh Pandey Aka Physics wallah. In case if you're not yet familiar...WE FOUND YOU!😌 Who is Physics Wallah? Currently, Physics wallah is an organisation which aims at providing FREE or Very cost effective qualitative education in regards with competitive exams like JEE mains, JEE Advanced and NEET.  Motive of this organisation is to make quality education available to all sections of society so that everybody gets equal opportunities irrespective of economic differences. Early Phase  As per the current status quo digitalization in education system is setting up its roots but but but let's take a dive into 2016 when idea of digital learning was so novel and unprocessed.  At that time a person named Alakh Pandey comes with the idea of providing FREE YouTube content in accordance...

Are we happy enough?

 Hey Mitra! how's everything? What was that hilarious thing that made you laugh absurdly today? Who was the person who filled your day with all the love in the world just by a small act of kindness? Let me reverse the question how many lives did you fill with positivity or humbleness today ? If the answer is none, then this blog may have something to add  to your life. Let me share with you an interesting event that just took place. Two children were playing a game which they called "How many". Rules were simple, one had to ask the questions by adding "how many" like, " how many planets are there in our solar system ?" Another had to answer. The game was quite interesting  and suddenly a question appeared "how many muscles does it take to smile?" The child seeking the answer smiled curiously and tried to locate all the muscles involved in smiling right then at the moment and his friend helped him too in a manner like," ohh look, there...

Be friend with yoga

Our lives had already been messed up and tired while corona virus added fuel to fire. In this global pandemic and amidst all the tensions and worries don't forget to revive yourself  up. What could be better occasion then international yoga day itself to make your life less troublesome and actually get a life! What is yoga? Yoga is very ancient practice in itself and it truly explains that it comes from Sanskrit Language. Yoga simply means unification of mental, physical and spiritual elements of life. It aims at connecting us with nature and establish peace and calmness within ourselves. The whole theory of Yoga is to go beyond the mind.  Swami Vivekananda But rather than just being a practice, it is a habit which had been followed from like some thousand years in India. It's quite unfortunate that when people across the world are accepting the merits of Yoga, netizens of India seem to lack interest in the heritage of their own nation. Ironic?! Yeah. Yoga and its origin Nam...