0% found this document useful (0 votes)
6 views50 pages

Java Backend Interview Answers

This document is a Java Backend Interview Answer Guide containing 90 high-probability interview questions tailored for candidates with 3-4 years of experience. It provides natural spoken answers to key Java concepts such as OOP, SOLID principles, Java 8 features, and the Stream API, emphasizing understanding over memorization. The guide aims to prepare candidates for interviews by explaining complex topics in a clear and concise manner.

Uploaded by

raaj
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
6 views50 pages

Java Backend Interview Answers

This document is a Java Backend Interview Answer Guide containing 90 high-probability interview questions tailored for candidates with 3-4 years of experience. It provides natural spoken answers to key Java concepts such as OOP, SOLID principles, Java 8 features, and the Stream API, emphasizing understanding over memorization. The guide aims to prepare candidates for interviews by explaining complex topics in a clear and concise manner.

Uploaded by

raaj
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Java Backend

Interview Answer Guide


90 High-Probability Questions — Interview-Ready Spoken Answers
Curated for 3–4 Years Experience | 2025–2026

All answers are written as natural spoken prose — the way you would actually explain them in an
interview. Avoid memorising word-for-word; understand the concept and speak in your own voice.
1. OOP & Core Java

Q: What is Polymorphism? Difference between overloading and overriding?


Almost always asked
Polymorphism literally means 'many forms', and in Java it lets us treat objects of different types
through a common interface. There are two kinds. The first is compile-time polymorphism, also
called method overloading, where you have multiple methods in the same class with the same
name but different parameter lists. The compiler decides at compile time which version to call
based on the argument types. The second is runtime polymorphism, also called method
overriding, where a subclass provides its own implementation of a method defined in the parent
class. Here, the decision of which method to call is made at runtime by the JVM based on the
actual object type, not the reference type. A classic example is when you have an Animal
reference pointing to a Dog object — when you call speak(), the JVM calls Dog's
implementation. This is the foundation of how frameworks like Spring use proxy objects to
intercept method calls for things like transactions and caching.

Q: Abstract class vs Interface — when do you use which?


Almost always asked
Both are used to achieve abstraction, but they serve different design purposes. An interface
defines a contract — it says 'any class that implements me must be able to do these things.'
Before Java 8, interfaces could only have abstract methods. After Java 8, they can have default
and static methods too, which lets library authors add new methods without breaking existing
implementations. An abstract class, on the other hand, is a partial implementation. It can have
constructors, instance variables, and fully implemented methods alongside abstract ones. I use
an interface when I want to define a capability that can be shared across completely unrelated
classes — for example, Serializable, Runnable, or Comparable. I use an abstract class when I
want to provide a base implementation with shared state and some template methods, and I
expect subclasses to be of the same 'family'. A common real-world example is HttpServlet — it's
an abstract class because all servlets share lifecycle behavior. The rule I follow is: if there's no
shared state and you just want to define what a class can do, use an interface. If you need
shared code and state, use an abstract class.

Q: What are SOLID principles? Explain each.


Almost always asked
SOLID is an acronym for five design principles that make software more maintainable and
flexible. The Single Responsibility Principle says a class should have only one reason to change
— meaning it should do one thing well. If your OrderService is also sending emails and
generating reports, that's a violation. The Open-Closed Principle says your code should be open
for extension but closed for modification — you should be able to add new behavior without
changing existing tested code, typically achieved through interfaces and polymorphism. The
Liskov Substitution Principle says a subclass should be substitutable for its parent without
breaking the program — if your code works with an Animal, it should work with any subclass like
Dog or Cat without surprises. The Interface Segregation Principle says don't force a class to
implement methods it doesn't need — prefer many small, specific interfaces over one fat
interface. And the Dependency Inversion Principle says high-level modules should depend on
abstractions, not concrete implementations. This is exactly what Spring's dependency injection
enforces — your service depends on a repository interface, not a specific JPA implementation.

Q: Inheritance vs Composition — which do you prefer and why?


Almost always asked
Inheritance models an 'is-a' relationship while composition models a 'has-a' relationship. In my
experience, composition is almost always the better choice, and there's a well-known principle in
design: 'favor composition over inheritance.' The problem with inheritance is that it creates tight
coupling between the parent and child. If the parent class changes, all subclasses are affected.
It also violates encapsulation because subclasses can depend on internal implementation
details of the parent. With composition, you embed the behavior you need as a field and
delegate to it. This gives you much more flexibility — you can change the composed object at
runtime, swap implementations, and test things independently. For example, instead of
extending a Logger class, your service just holds a Logger reference and calls it. The one case
where inheritance genuinely makes sense is when the relationship is truly an 'is-a' and you want
to take advantage of polymorphism — like Spring's controller hierarchy. But even then, I keep
inheritance shallow, rarely more than one level deep.

Q: How to make a class immutable in Java?


Very likely
To make a class truly immutable you need to follow a few rules consistently. First, declare the
class as final so it cannot be subclassed — a subclass could potentially override methods and
break immutability. Second, make all fields private and final so they can only be assigned once,
in the constructor. Third, don't provide any setter methods. Fourth, and this is the subtle part, if
any field is a mutable object — like a List or a Date — you must perform a defensive copy when
accepting it in the constructor and when returning it from a getter. If you just store the reference,
external code can still mutate the object through that reference. The classic example is String,
which is immutable in Java. Every operation like substring or concat returns a new String rather
than modifying the original. Immutability is extremely valuable in concurrent environments
because immutable objects are inherently thread-safe — multiple threads can read them
simultaneously without any synchronization.

Q: What is Encapsulation and how does it help?


Very likely
Encapsulation is the practice of bundling data and the methods that operate on that data into a
single unit — the class — and restricting direct access to the internal state from outside. In
practice this means making fields private and exposing them through public getter and setter
methods. The real value of encapsulation goes beyond just hiding fields. It lets you control what
goes in and comes out — a setter can validate input before assigning it, ensuring the object is
always in a consistent state. It also lets you change the internal implementation without breaking
the external API. If your class stores a date as a String internally but you later switch to
LocalDate, none of the callers need to change because they only interact through the getter. It's
the foundational principle that makes code maintainable at scale.
Q: What is Abstraction? How is it different from Encapsulation?
Very likely
These two are closely related but address different concerns. Abstraction is about hiding
complexity — showing only what is necessary and hiding the 'how' behind a clean interface.
When you call [Link](), you don't need to know which sorting algorithm is used internally. That's
abstraction. Encapsulation is about hiding data — protecting internal state from unauthorized
access and modification. A good way to remember the difference: abstraction is about design
and what something does, encapsulation is about implementation and how data is protected. In
Java, abstraction is achieved through interfaces and abstract classes, while encapsulation is
achieved through access modifiers and the getter-setter pattern. In practice they work together
— you abstract the behavior through an interface and encapsulate the state inside the
implementing class.

Q: Why can't an interface have a constructor?


Very likely
A constructor exists to initialize the state of an object — specifically, to set up instance variables
when an object is created. Interfaces cannot have instance variables in the traditional sense;
before Java 8 they only had public static final constants. Since there's no instance state to
initialize, there's no need for a constructor. More fundamentally, you cannot instantiate an
interface directly — you can only instantiate a concrete class that implements the interface.
When that concrete class is instantiated, its own constructor runs. The interface itself is never
brought to life as an object, so giving it a constructor would be meaningless.

Q: What is method hiding vs method overriding?


Very likely
Method overriding happens when an instance method in a subclass has the same signature as
one in the parent class. At runtime, the JVM uses the actual object type to decide which version
runs — this is dynamic dispatch, the foundation of runtime polymorphism. Method hiding, on the
other hand, happens with static methods. If a subclass defines a static method with the same
signature as one in the parent, it hides the parent's method rather than overriding it. The key
difference is that the version that runs depends on the reference type at compile time, not the
actual object type at runtime. So if you call a static method through a parent class reference, you
get the parent's version even if the actual object is a subclass. This is why static methods cannot
participate in polymorphism — they're bound at compile time, not runtime.

Q: Why were default methods added to interfaces in Java 8?


Very likely
The primary motivation was backward compatibility. Before Java 8, if you owned a widely-used
interface and wanted to add a new method, every single class implementing that interface would
break — they'd all need to implement the new method. This was a massive problem for the Java
Collections API when the team wanted to add stream-related methods like forEach and stream
to the Collection interface. The solution was default methods — methods with a concrete
implementation inside the interface itself. Any class that doesn't override the default gets the
interface's implementation for free, so existing code doesn't break. It's a pragmatic addition, not
a design purity feature. The one nuance to understand is the diamond problem — if a class
implements two interfaces that both have a default method with the same signature, the class
must explicitly override that method to resolve the ambiguity.

Q: What are the types of inner classes in Java?


Good to know
Java has four types of inner classes. A member inner class is a non-static class defined directly
inside another class — it has access to all members of the outer class including private ones,
but it requires an instance of the outer class to be instantiated. A static nested class is declared
with the static keyword — it doesn't have access to the instance members of the outer class and
can be instantiated without an outer class instance. A local inner class is defined inside a
method — it's scoped to that method and can access effectively final local variables. An
anonymous inner class is a one-time-use class defined and instantiated in a single expression
— very common in event handling and before lambdas were introduced in Java 8. Today, most
use cases for anonymous inner classes that implement a functional interface are better handled
with lambdas.

Q: Can we override a static method in Java?


Good to know
No, you cannot override a static method in Java. Static methods belong to the class, not to any
instance, so they don't participate in the polymorphism mechanism. If you define a static method
in a subclass with the same signature as one in the parent, it's called method hiding, not
overriding. The distinction matters because when you call a static method through a reference,
the method that gets called is determined by the declared type of the reference at compile time,
not by the actual object type at runtime. Override resolution happens at runtime through the
virtual method table — static methods are resolved at compile time and don't go through that
mechanism.

Q: What is the order of execution: static initializer → instance initializer →


constructor?
Good to know
The JVM follows a strict order. When a class is first loaded, static initializers and static variable
assignments run in the order they appear in the source code. This happens once per class, not
per object. When you create an instance, the order is: first, the parent class constructor is called
(via the super() call, implicit or explicit), then instance variable initializers and instance initializer
blocks run top to bottom, and finally the body of the constructor runs. So if class B extends class
A, creating a B object results in: A's static init (if not already done), B's static init, A's instance init
and constructor, then B's instance init and constructor. Understanding this order is important
when debugging initialization issues, especially in frameworks that do a lot of class loading and
object creation.

Q: What is a Classloader in Java?


Good to know
A classloader is the component responsible for loading class files into the JVM at runtime. Java
uses a hierarchical delegation model with three built-in classloaders. The Bootstrap classloader
is the topmost one — it loads core Java classes from the [Link] like [Link]. The
Extension classloader loads classes from the ext directory of the JRE. The Application
classloader loads classes from the application's classpath. When a class needs to be loaded,
the request is always delegated upward first. Only if the parent classloader cannot find the class
does the child attempt to load it. This delegation model is called the parent-first delegation model
and it prevents malicious code from replacing core Java classes. Custom classloaders are used
in application servers to support hot deployment and class isolation between different web
applications.
2. Java 8 / Streams / Functional Interfaces

Q: What are the key features of Java 8?


Almost always asked
Java 8 was one of the most significant releases in the language's history. The headline feature is
Lambda expressions, which let you treat behavior as data — passing functions around just like
objects. Closely related are Functional Interfaces, which are interfaces with exactly one abstract
method, like Runnable or Comparator, that can be the target of a lambda. The Stream API was
introduced to enable functional-style operations on collections — filtering, mapping, reducing —
in a declarative, pipeline-based style. The Optional class was added to represent a value that
may or may not be present, providing a cleaner alternative to returning null. Method references
gave us a shorthand for lambdas that simply call an existing method. The Date-Time API was
completely reworked with [Link], replacing the error-prone Calendar and Date classes. And
the introduction of default and static methods in interfaces was what made all of this work
without breaking backward compatibility with existing code.

Q: What is a Functional Interface? Name the built-in ones.


Almost always asked
A functional interface is an interface that has exactly one abstract method. The
@FunctionalInterface annotation is optional but acts as a compile-time check to ensure nobody
accidentally adds a second abstract method. The single-abstract-method constraint is what
makes them compatible with lambda expressions — a lambda is essentially an inline
implementation of that one method. The four main built-in functional interfaces in
[Link] are: Predicate, which takes an input and returns a boolean — useful for
filtering; Function, which takes an input of one type and returns a value of another type — useful
for mapping or transforming; Consumer, which takes an input but returns nothing — useful for
side effects like printing or saving; and Supplier, which takes no input but returns a value —
useful for lazy initialization or factory-style creation. There are also variants like BiFunction,
BiPredicate, UnaryOperator, and BinaryOperator for common use cases.

Q: Explain Lambda expressions.


Almost always asked
A lambda expression is essentially an anonymous function — a block of code you can pass
around as a value without needing to create a full class or anonymous inner class. The syntax is:
parameters, then an arrow, then the body. For example, (x, y) -> x + y is a lambda that takes two
parameters and returns their sum. Before lambdas, if you wanted to sort a list with a custom
comparator, you'd write an anonymous inner class with five or six lines of boilerplate. With a
lambda it becomes one line: [Link]((a, b) -> [Link]().compareTo([Link]())). The
reason lambdas are called 'anonymous' is that they have no name and no explicit type
declaration — the type is inferred from the context, specifically from the functional interface
they're being assigned to or passed as. One important nuance is that lambdas can capture
variables from their enclosing scope, but those variables must be effectively final — either
declared final or never reassigned after the lambda is defined.
Q: What is the Stream API? Intermediate vs Terminal operations.
Almost always asked
The Stream API is a way to process collections of data in a declarative, functional style. A
stream is not a data structure — it doesn't store data. It's a pipeline of operations over a source,
which can be a collection, array, or I/O channel. Operations on streams are either intermediate
or terminal. Intermediate operations are lazy — they return a new stream and don't execute
anything until a terminal operation is called. Examples are filter, map, flatMap, sorted, distinct,
limit, and peek. Terminal operations trigger the actual execution of the whole pipeline and
produce a result or a side effect. Examples are collect, forEach, reduce, count, findFirst,
anyMatch, and toList. The lazy evaluation of intermediate operations is a key performance
feature — if you're filtering a million elements and only want the first three that match, the
pipeline stops after finding three matches. It doesn't process all million elements first.

Q: When do you use map() vs flatMap()?


Almost always asked
map is used when you want to transform each element in a stream into exactly one other
element — it's a one-to-one transformation. For example, mapping a list of strings to their
lengths, or mapping a list of users to their email addresses. flatMap is used when the
transformation produces zero or more elements for each input element — it's a one-to-many
transformation that then flattens all the results into a single stream. The classic example is when
each element is itself a collection. If you have a list of orders and each order has a list of items,
and you want a single flat stream of all items across all orders, you use flatMap:
[Link]().flatMap(order -> [Link]().stream()). If you used map instead, you'd get a
Stream of Lists, not a Stream of items. Another common real-world use is with Optional —
flatMap prevents wrapping Optional in Optional when chaining optional-returning methods.

Q: What is Optional? How do you use it to avoid NPE?


Almost always asked
Optional is a container object introduced in Java 8 that may or may not contain a non-null value.
Its primary purpose is to make the possibility of a missing value explicit in the API contract,
rather than using null which silently propagates and causes NullPointerExceptions far from
where the problem originated. Instead of returning null from a method when no result is found,
you return [Link](). The caller is then forced to handle both cases. The most idiomatic
way to use Optional is through its functional methods. orElse gives you a default value if empty.
orElseGet takes a supplier for lazy default computation. ifPresent lets you execute a consumer
only if a value is present. map lets you transform the value inside if present. orElseThrow throws
an exception if empty. What you should avoid is calling get() without first checking isPresent() —
that defeats the purpose. Also, Optional is not meant to be used as a field type or method
parameter — it's specifically designed as a return type for methods that might legitimately return
no result.

Q: What are method references? What are the 4 types?


Very likely
A method reference is a shorthand notation for a lambda expression that simply calls an existing
method. Instead of writing x -> [Link](x), you can write [Link]::println. The four
types are: static method reference, written as ClassName::staticMethod — for example,
Integer::parseInt instead of s -> [Link](s); instance method reference on a particular
object, written as instance::method — for example, myString::toUpperCase; instance method
reference on an arbitrary object of a particular type, written as ClassName::instanceMethod —
for example, String::toLowerCase, where the first parameter of the lambda becomes the object
the method is called on; and constructor reference, written as ClassName::new — for example,
ArrayList::new, which is used when a supplier or factory is expected. Method references are
purely a readability improvement — they produce exactly the same bytecode as the equivalent
lambda.

Q: What are Collectors in Java 8? Give examples.


Very likely
Collectors is a utility class that provides implementations of the Collector interface, used as the
argument to the collect terminal operation on streams. The most commonly used one is
[Link](), which collects all stream elements into a List. Similarly, toSet() collects into a
Set, and toMap() collects into a Map where you specify key and value extractors. joining() is
extremely useful for concatenating strings, optionally with a delimiter, prefix, and suffix.
groupingBy() is powerful — it groups elements by a classifier function, producing a Map where
the keys are the group values and the values are lists of elements. For example, grouping
employees by department. counting() counts the elements in each group when used as a
downstream collector. partitioningBy() splits elements into two groups — those that match a
predicate and those that don't. And toUnmodifiableList() is the thread-safe variant introduced in
Java 10 that produces an immutable collection.

Q: What is the difference between sequential and parallel streams?


Very likely
A sequential stream processes elements one after another in a single thread, in encounter
order. A parallel stream splits the data into chunks and processes them simultaneously using
multiple threads from the [Link](). You create a parallel stream either by
calling parallelStream() on a collection or by calling parallel() on an existing stream. The intuition
is that parallel streams should be faster, but that's not always true. The overhead of splitting the
work, managing threads, and combining results can outweigh the benefit for small datasets or
fast operations. Parallel streams work best when the dataset is large, the individual operation
per element is expensive, and the operation is stateless and side-effect-free. They can cause
problems with shared mutable state, ordering-sensitive operations, and operations that have
side effects. In production I've seen cases where switching to parallel actually made things
slower because the overhead dominated. My rule is: measure first, parallelize if there's a proven
bottleneck.

Q: Explain reduce() with an example.


Very likely
The reduce terminal operation combines all elements of a stream into a single result by
repeatedly applying a binary function. It takes two forms. The simpler form takes an identity
value and an accumulator function: for example, [Link](0, Integer::sum) starts with 0
and adds each element to a running total. The identity value is both the starting point and the
value returned for an empty stream. The second form returns an Optional because without an
identity there's no sensible return value for an empty stream. Under the hood, reduce is what
makes the Stream API composable with parallel execution — because the operation is
associative, the runtime can split the stream into chunks, reduce each chunk independently, and
then combine the partial results. This is analogous to MapReduce in distributed computing. For
most collection tasks, Collectors handles the common cases more readably, but reduce is the
right tool when you need a custom aggregation that doesn't fit a standard collector.

Q: How do you debug a stream pipeline?


Very likely
The challenge with debugging streams is that the pipeline is lazy and executes as a single chain
— you can't easily set a breakpoint in the middle like you would with a for loop. The primary tool
is the peek intermediate operation, which lets you observe each element at any point in the
pipeline without consuming it. I insert peek calls between operations to log what's happening:
[Link](...).peek(e -> [Link]('After filter: {}', e)).map(...). This gives me visibility into the
intermediate state. Another approach is to break the stream into multiple statements — assign
the result of each intermediate operation to a local variable — so I can step through with a
debugger. For complex pipelines, I often write a test that uses a small, controlled dataset where I
can predict the exact output at each stage. In production, if I see unexpected stream behavior,
the first thing I check is whether there are any side effects or shared state in the lambda
functions, which is a common source of bugs especially with parallel streams.

Q: What are new String methods in Java 11?


Good to know
Java 11 added several useful String utility methods that reduce the need for third-party libraries
like Apache Commons. isBlank() returns true if the string is empty or contains only whitespace,
unlike isEmpty() which only checks for empty. strip(), stripLeading(), and stripTrailing() are
Unicode-aware alternatives to trim() — trim() only removes ASCII space characters, while strip
uses [Link]() which covers all Unicode whitespace. lines() returns a Stream of
lines from the string, split by line terminators, which is very handy for processing multi-line text.
repeat(n) returns the string repeated n times. These might seem minor but they eliminate a lot of
boilerplate and improve readability, particularly isBlank which I use constantly in validation logic.

Q: What are sealed classes in Java 17?


Good to know
Sealed classes give you explicit control over which classes can extend or implement a type. You
declare a class or interface as sealed and then specify its permitted subclasses using the
permits keyword. For example, a Shape sealed class might permit Circle, Rectangle, and
Triangle. Any other class that tries to extend Shape will get a compile-time error. The benefit is
that it gives the compiler — and you as the developer — a complete, closed view of the type
hierarchy. This is especially powerful with pattern matching and switch expressions, because the
compiler can verify exhaustiveness: if you switch over a sealed type and handle all permitted
subtypes, no default case is needed. It's a way of expressing domain models more precisely —
if your payment method can only ever be CreditCard, BankTransfer, or Crypto, a sealed
interface makes that explicit and enforced.

Q: What are Record classes in Java 17?


Good to know
Records are a concise way to create immutable data carrier classes. Before records, if you
wanted a simple class to hold a name and an age, you'd write the class, private final fields, a
constructor, getters, equals, hashCode, and toString — probably 30 to 40 lines. A record does
all of that in one line: record Person(String name, int age) {}. The compiler automatically
generates the canonical constructor, accessors (accessed as name() and age(), not getName()),
equals and hashCode based on all components, and a sensible toString. Records are implicitly
final and their components are implicitly final, making them inherently immutable. They're perfect
for DTOs, value objects, response models, and any place where you just need to shuttle data
around. I used them extensively in Java 17 migration to replace hundreds of lines of boilerplate
DTO code.

Q: What are virtual threads in Java 21 and when should you use them?
Good to know
Virtual threads, introduced as a stable feature in Java 21 through Project Loom, are lightweight
threads managed by the JVM rather than the operating system. A traditional platform thread
maps one-to-one with an OS thread, and OS threads are expensive — you typically can't have
more than a few thousand without running into memory and context-switching problems. Virtual
threads can be created by the millions because they're essentially just objects on the heap,
scheduled by the JVM onto a small pool of carrier OS threads. When a virtual thread blocks —
say, on a database call or an HTTP request — the JVM parks it and uses the carrier thread for
something else, automatically resuming the virtual thread when the I/O completes. The best use
case is high-throughput I/O-bound applications where threads spend most of their time waiting.
Virtual threads let you write simple synchronous-looking code and get the concurrency benefits
that previously required reactive programming. They're not the right tool for CPU-bound tasks
where you genuinely need parallelism across cores.
3. Collections & HashMap

Q: How does HashMap work internally? Before and after Java 8.


Almost always asked
A HashMap internally uses an array of buckets, where each bucket is a linked list of key-value
pairs called Entry objects. When you put a key-value pair in, Java first computes the hash of the
key by calling its hashCode() method and then applies a secondary hash function to spread
values more evenly. This final hash value is used to compute the bucket index using a bitwise
AND with the array length minus one. If multiple keys hash to the same bucket — a hash
collision — they're stored in a linked list at that bucket. When you get a value, Java computes
the bucket index again and then traverses the linked list at that bucket, comparing keys using
equals(). Before Java 8, this linked list could degrade to O(n) performance in the worst case if
many keys collided. After Java 8, when a bucket's linked list grows beyond a threshold of 8
entries, it's converted to a balanced red-black tree, reducing worst-case lookup from O(n) to
O(log n). The default initial capacity is 16 and the load factor is 0.75, meaning when 75% of the
buckets are occupied, the map is resized to double capacity and all entries are rehashed.

Q: Can we use a custom object as a key? What contracts must it follow?


Almost always asked
Yes, you can use any object as a HashMap key, but you must correctly implement both equals()
and hashCode() and honor the contract between them. The contract is: if two objects are equal
according to equals(), they must return the same hashCode. If you violate this, keys will be
stored correctly but retrieval will fail — get will return null even though the key was put in,
because the hash function directs to a different bucket than where the entry was stored. The
second rule is that the hash code of a key should not change while it's stored in the map. This
means you should only use fields that don't change as part of the hash computation. If you use a
mutable field in hashCode and then mutate that field after inserting the key, the key becomes
'lost' in the map. In practice I always use an IDE to generate equals and hashCode, or use
[Link]() and [Link]() to avoid manual mistakes. The safest keys are immutable
objects, which is exactly why String is such a popular key.

Q: Explain equals() and hashCode() contract with an example.


Almost always asked
The contract between equals and hashCode is foundational to how all hash-based collections
work. The rules are: first, two objects that are equal according to equals must return the same
hashCode — this is a strict requirement. Second, two objects with the same hashCode do not
have to be equal — this is just a hash collision and is handled by the linked list or tree in the
bucket. Third, hashCode must be consistent — calling it multiple times on the same object must
return the same value. A common mistake is overriding equals without overriding hashCode.
Consider a Person class where you override equals to compare by name and id. If you forget to
override hashCode, two Person objects with the same name and id will be equal but have
different hash codes, causing HashMap to put them in different buckets. Now the map appears
to have two entries for the same logical key, and retrieval fails. The fix is to always override both,
and to use the same fields in both methods.
Q: What is ConcurrentHashMap? How does it differ from HashMap and Hashtable?
Almost always asked
HashMap is not thread-safe — concurrent reads and writes from multiple threads can corrupt its
internal structure, causing infinite loops or data loss. Hashtable is thread-safe but achieves this
by synchronizing every method on the entire map, which creates a bottleneck because only one
thread can read or write at a time. ConcurrentHashMap takes a much smarter approach. Before
Java 8, it used segment-based locking, dividing the map into 16 segments and locking only the
affected segment during a write, allowing up to 16 concurrent writers. After Java 8, the
implementation was redesigned to use compare-and-swap operations at the bucket level — it
locks only the individual bucket being modified, not a segment, which provides even finer
granularity and much better throughput. Reads in ConcurrentHashMap are entirely lock-free.
Another important difference is that ConcurrentHashMap does not allow null keys or values,
while HashMap permits one null key and multiple null values. For any multi-threaded scenario,
ConcurrentHashMap is the right choice — it gives you safety without the severe performance
penalty of Hashtable.

Q: Fail-fast vs Fail-safe iterators.


Very likely
When you iterate over a collection, the behavior when another thread modifies the collection
mid-iteration depends on the type of iterator. Fail-fast iterators, used by ArrayList, HashMap, and
most non-concurrent collections, detect concurrent modification by tracking a modCount — a
counter that increments on every structural change. If the modCount changes during iteration,
the iterator throws ConcurrentModificationException immediately. This is a 'fail loudly and early'
approach — better to throw an exception than silently produce wrong results. Fail-safe iterators,
used by collections like CopyOnWriteArrayList and ConcurrentHashMap, don't throw that
exception. CopyOnWriteArrayList makes a copy of the underlying array on every write, so
existing iterators continue iterating over the snapshot they started with. The trade-off is memory
overhead and the fact that writes made after iteration began are not visible to the iterator.
ConcurrentHashMap's iterator is weakly consistent — it may or may not reflect modifications
made after iteration started, but it will never throw ConcurrentModificationException.

Q: Why is String a good HashMap key?


Very likely
String is an ideal HashMap key for several reasons. First and most importantly, String is
immutable — once created, its value never changes. This means its hashCode will never
change after it's been used as a key, so the key can always be found in the same bucket it was
stored in. Second, String caches its hashCode — the first time hashCode() is called, the result is
stored in a private field, and subsequent calls return the cached value without recomputing. This
is a significant performance optimization since HashMap calls hashCode frequently. Third,
String has a well-tested, well-distributed hashCode implementation that minimizes collisions.
And fourth, its equals() method is correct and consistent. Any mutable object used as a key is
risky because if the object changes after insertion, it becomes unfindable. This is why the
general advice is to use immutable objects as map keys whenever possible.

Q: How does HashMap handle null keys?


Very likely
HashMap is specifically designed to handle one null key. Since you can't call hashCode() on
null, HashMap special-cases it: a null key is always placed in bucket 0. On retrieval, it checks if
the key is null and goes directly to bucket 0 to look it up. This is a deliberate design choice. For
values, HashMap allows multiple null values without any special handling — they're stored
normally under whatever key they're associated with. ConcurrentHashMap and Hashtable,
however, do not allow null keys or values. In ConcurrentHashMap, this was a deliberate design
decision — allowing null would create ambiguity in concurrent contexts. If get() returns null, you
can't tell whether the key was absent or whether someone explicitly stored null as the value. In a
single-threaded context with HashMap you can call containsKey to disambiguate, but in a
concurrent context that check-then-act pattern is not atomic.

Q: What is WeakHashMap?
Good to know
WeakHashMap is a special-purpose Map where the keys are held using weak references. In
Java's memory model, a weak reference doesn't prevent the garbage collector from collecting
the object. So in a WeakHashMap, if a key object has no other strong references pointing to it
anywhere in the application, the garbage collector can collect that key, and the corresponding
entry is automatically removed from the map. This makes WeakHashMap excellent for
implementing caches where you want entries to be automatically evicted when the cached
object is no longer needed by anyone else in the application. It prevents memory leaks that can
occur with regular HashMap caches where entries live as long as the map itself does. A practical
example is using it to associate metadata with objects — when the object is garbage collected,
the metadata automatically disappears too.
4. Multithreading & Concurrency

Q: What is volatile keyword and what problem does it solve?


Almost always asked
In a multi-core processor environment, each CPU core has its own cache. For performance,
threads often read variables from this cache rather than from main memory, and they write
updates to the cache before eventually flushing to main memory. This creates a visibility
problem — if thread A writes a value to a variable and thread B reads it, B might see a stale
cached value. The volatile keyword solves this by instructing the JVM that reads and writes to
this variable must always go directly to and from main memory, bypassing the CPU cache. This
guarantees visibility across threads. However, volatile does not guarantee atomicity. If two
threads both read a volatile counter, increment it, and write it back, you still have a race
condition because the read-modify-write is not atomic. For that you need AtomicInteger or
synchronized. The classic use case for volatile is a boolean flag used to signal a thread to stop
— the main thread sets the flag to true and the worker thread reads it in its loop condition.

Q: What is deadlock? How do you prevent it?


Almost always asked
A deadlock occurs when two or more threads are each waiting for a lock held by the other,
creating a circular dependency where no thread can make progress. For example, thread A
holds lock 1 and waits for lock 2, while thread B holds lock 2 and waits for lock 1. They wait
forever. Deadlock requires four conditions to occur simultaneously — mutual exclusion,
hold-and-wait, no preemption, and circular wait. The most practical prevention strategies target
the circular wait condition. The most reliable approach is lock ordering — always acquire locks in
a consistent global order. If every thread that needs both lock 1 and lock 2 always acquires lock
1 first, circular wait is impossible. Another strategy is to use tryLock with a timeout, available in
the ReentrantLock class, so a thread that can't acquire a lock within a time limit gives up and
releases what it holds. You can also use lock-free data structures from [Link] which
use compare-and-swap operations internally and avoid traditional locking altogether.

Q: synchronized vs Lock vs Semaphore — when to use each?


Almost always asked
The synchronized keyword is the simplest tool — it provides mutual exclusion at the method or
block level. The lock is acquired automatically when entering the block and released
automatically on exit, even if an exception is thrown. It's perfectly fine for straightforward mutual
exclusion but lacks flexibility. ReentrantLock from [Link] is a more powerful
alternative. It offers the same mutual exclusion but adds capabilities that synchronized lacks:
tryLock lets you attempt to acquire the lock without blocking, with an optional timeout;
lockInterruptibly lets a thread respond to interruption while waiting; and you can configure it as
fair, meaning threads acquire the lock in the order they requested it. You also get Condition
objects which let you have multiple wait-sets per lock, unlike the single wait-set on every Java
object. A Semaphore is a counting synchronizer — it maintains a count of permits. Unlike a lock
which allows only one thread, a semaphore can allow N threads to access a resource
simultaneously. The classic use case is connection pool management — you initialize the
semaphore with the pool size, and each thread acquires a permit before using a connection and
releases it after.

Q: What is the Executor framework? Difference between submit() and execute().


Almost always asked
The Executor framework, introduced in Java 5, provides a higher-level abstraction for managing
threads. Instead of creating and managing threads manually — which is error-prone and doesn't
scale — you submit tasks to an Executor which manages a pool of threads, task queuing, and
lifecycle. The core interface is Executor with a single execute method, and ExecutorService
extends it with lifecycle management and the ability to submit tasks that return results. The key
difference between submit and execute is in how they handle the result and exceptions. execute
accepts a Runnable and returns nothing — if the task throws an unchecked exception, it goes to
the thread's uncaught exception handler. submit accepts either a Runnable or Callable and
returns a Future. Even if you submit a Runnable, you get a Future back that lets you check if the
task completed and block until it does. For Callable tasks, [Link]() returns the computed
value or rethrows any exception the task threw, wrapped in an ExecutionException. In practice I
always use submit because the Future gives you control over the task lifecycle.

Q: What is CompletableFuture? Explain thenApply, thenCombine, thenCompose.


Almost always asked
CompletableFuture, introduced in Java 8, is a powerful class for asynchronous, non-blocking
programming. Unlike a regular Future which only lets you block and wait for a result,
CompletableFuture lets you chain operations, combine multiple async computations, and handle
errors without blocking. thenApply is the map of CompletableFuture — it takes the result of one
stage, applies a function to transform it, and produces a new CompletableFuture with the
transformed value. It's synchronous within the pipeline. thenCompose is flatMap — it's used
when the next operation itself returns a CompletableFuture. If you used thenApply in that case,
you'd get a CompletableFuture of CompletableFuture. thenCompose flattens that. thenCombine
lets you run two independent CompletableFutures in parallel and combine their results when
both are done. A real-world example: fetch user details from one service and account balance
from another, then combine them into a single response — you'd run both in parallel with
thenCombine rather than waiting for one before starting the other. The exceptionally method
handles errors gracefully, and thenApplyAsync, thenComposeAsync run the next stage on a
specified executor rather than the calling thread.

Q: What are atomic variables?


Very likely
Atomic variables, in the [Link] package, provide thread-safe operations on
single variables without explicit locking. Classes like AtomicInteger, AtomicLong,
AtomicBoolean, and AtomicReference wrap a value and expose operations that are guaranteed
to be atomic. The key operation is compareAndSet — it reads the current value, compares it to
an expected value, and only updates it if they match, all in one uninterruptible operation. This is
implemented using hardware-level compare-and-swap (CAS) instructions, which are much
cheaper than acquiring a lock. AtomicInteger is the canonical replacement for a synchronized
integer counter. Instead of synchronizing every increment, you call incrementAndGet() and the
underlying CAS loop handles concurrency. The advantage over synchronized is that CAS is
non-blocking — if two threads compete, one wins and the other simply retries, without either
being put to sleep. This makes atomic variables ideal for high-throughput counters, sequence
generators, and reference swapping.

Q: What is ThreadLocal? Give a real-world use case.


Very likely
ThreadLocal is a class that lets each thread have its own independent copy of a variable. When
you set a value on a ThreadLocal, it's stored in a map specific to the current thread. When you
get the value, you get back what was stored for that thread specifically. No other thread can see
or modify another thread's value. A classic real-world use case is storing the current user
context in a web application. When a request comes in, a filter reads the JWT token, extracts the
user, and stores it in a ThreadLocal. Any downstream service class can then read the current
user without having to pass it through every method signature. Spring Security's
SecurityContextHolder uses exactly this pattern. Another common use is database connections
or transactions — you store the connection for the current thread so that multiple methods in the
same transaction share one connection without explicitly passing it around. The critical thing to
remember with ThreadLocal in thread pool environments is to always clean up — call remove()
when the task is done. Because thread pools reuse threads, a stale ThreadLocal value from a
previous task will leak into the next task on the same thread.

Q: CachedThreadPool vs FixedThreadPool — when to use which?


Very likely
[Link](n) creates a pool with exactly n threads. If all threads are busy,
new tasks queue up and wait. The queue is unbounded, so in theory you can accept an
unlimited number of tasks without rejection, but memory will eventually run out if tasks pile up
faster than they're processed. FixedThreadPool is appropriate when you want predictable
resource consumption and your tasks are long-running or CPU-bound — you typically set n to
the number of CPU cores. [Link]() creates threads on demand and
reuses idle threads. Idle threads are kept alive for 60 seconds before being terminated. If tasks
arrive faster than threads can handle them, new threads are created — potentially thousands.
This is great for short-lived, bursty I/O tasks where threads spend most of their time waiting, but
dangerous for sustained high load because an unbounded number of threads can be created,
leading to OutOfMemoryError. In production systems, I prefer creating a custom
ThreadPoolExecutor where I explicitly set the core pool size, max pool size, keep-alive time, and
queue type, because the factory methods hide important defaults that can be surprising.

Q: What is CountDownLatch? How is it different from CyclicBarrier?


Very likely
Both are synchronization aids used to coordinate multiple threads, but they serve different
purposes. CountDownLatch is initialized with a count and provides two operations: countDown()
decrements the count by one, and await() blocks until the count reaches zero. It's a one-shot
tool — once the count reaches zero it cannot be reset. A common use case is a main thread that
launches multiple worker threads and waits for all of them to complete before proceeding. Each
worker calls countDown when done, and the main thread waits at await until all workers have
finished. CyclicBarrier is initialized with a number of parties and lets threads wait for each other
at a common barrier point. When the last thread calls await, all threads are released
simultaneously. It's 'cyclic' because it can be reused — after all threads are released, the barrier
resets to its initial state. The use case is iterative algorithms where a phase must complete
across all threads before the next phase can begin, like parallel matrix computations.

Q: What is false sharing?


Very likely
False sharing is a performance problem in multi-core systems where threads on different cores
unintentionally compete over the same cache line even though they're accessing different
variables. CPU caches work in units called cache lines, typically 64 bytes. If two threads each
write to different variables that happen to occupy the same cache line, every write by one thread
invalidates the cache line in the other thread's cache, forcing it to reload the entire line from
memory. This is expensive — it looks like a real cache miss but is caused by unrelated variables
being adjacent in memory. The fix is padding — adding unused bytes between the variables so
they land on different cache lines. Java 8 introduced the @Contended annotation in the
[Link] package to do this automatically, though it requires a JVM flag to enable.
ConcurrentHashMap and LongAdder in the JDK use this technique internally. In practice, false
sharing only matters in extremely hot code paths where threads are hammering adjacent
variables millions of times per second.

Q: How do virtual threads improve concurrency?


Very likely
Traditional Java threads map one-to-one with OS threads. Creating an OS thread is expensive
— each one has a fixed stack size of around 1MB by default — so thread pools cap the number
of threads, typically at a few hundred to a few thousand. When a thread blocks on I/O, that OS
thread is parked and unavailable, wasting resources. This is why reactive programming
frameworks emerged — they use async non-blocking I/O to avoid blocking threads. Virtual
threads flip this model. They're lightweight JVM-managed threads that are multiplexed onto a
small number of OS threads called carrier threads. When a virtual thread blocks — on a
database call, an HTTP request, a sleep — the JVM automatically unmounts it from the carrier
thread and mounts a different virtual thread onto it. When the I/O completes, the virtual thread is
remounted and continues. You can create millions of virtual threads because they're just heap
objects. The revolutionary part is that you can write straightforward synchronous-looking code
and get the concurrency benefits of async programming without the callback hell or reactive
operators.

Q: What is livelock vs deadlock?


Good to know
In a deadlock, threads are frozen — they're each waiting for a resource the other holds, and
nothing moves. In a livelock, threads are active but making no progress. They keep changing
their state in response to each other but never actually complete their work. The classic analogy
is two people in a hallway trying to pass each other — they both step to the same side
repeatedly, each responding to the other's move, but they never actually pass. In software, a
livelock might occur when two threads both detect a conflict, back off, wait a random amount of
time, and retry — but they keep picking the same back-off times and colliding again. Livelocks
are harder to detect than deadlocks because the threads appear to be doing something. The
solution typically involves randomization in the back-off strategy or a more sophisticated
coordination mechanism.

Q: What is the ABA problem?


Good to know
The ABA problem is a subtle issue that arises with compare-and-swap operations. CAS checks
if a variable's current value equals an expected value and only updates if they match. The ABA
problem occurs when a value changes from A to B and back to A between a thread's initial read
and its CAS attempt. The CAS succeeds because the value looks unchanged, but intermediate
changes might have invalidated assumptions. For example, in a lock-free stack: thread 1 reads
the top as A, then thread 2 pops A, pushes B, and pushes A back. Thread 1's CAS succeeds
because the top is still A, but the stack's structure may have changed in a meaningful way. The
solution is to attach a version stamp or timestamp to the reference. Java provides
AtomicStampedReference for exactly this purpose — you compare both the reference and the
stamp, so even if the value returns to A, the stamp has changed, preventing the false positive.
5. JVM, Memory & Garbage Collection

Q: Explain heap memory sections: Young Gen, Old Gen, Metaspace.


Almost always asked
The JVM heap is divided based on the observation that most objects are short-lived — the
generational hypothesis. The heap has two main regions: Young Generation and Old
Generation. The Young Generation is where all new objects are allocated. It's further divided
into Eden and two Survivor spaces (S0 and S1). When Eden fills up, a Minor GC runs — it
identifies live objects in Eden and the active Survivor space, copies them to the other Survivor
space, and discards everything else. Objects that survive a configured number of Minor GCs
(typically 15) are promoted to the Old Generation, also called Tenured space. The Old
Generation holds long-lived objects and is collected less frequently in a Major GC, which is
slower and causes longer pauses. Metaspace, introduced in Java 8 to replace PermGen, is not
part of the heap — it's native memory. It stores class metadata: class definitions, method
bytecode, and constant pools. Unlike PermGen which had a fixed maximum size, Metaspace
grows dynamically up to the system's available native memory, though you should set a max
with -XX:MaxMetaspaceSize to prevent unbounded growth.

Q: How does Garbage Collection work? Minor GC vs Major GC.


Almost always asked
Garbage collection is the automatic process of identifying objects that are no longer reachable
from any live thread or static reference and reclaiming their memory. The GC starts from a set of
root references — active threads, static fields, local variables in stack frames — and traces all
reachable objects. Anything not reached is garbage. A Minor GC runs when Eden space fills up.
It only collects the Young Generation and is typically fast — a few milliseconds — because most
new objects are garbage by the time GC runs, so there's relatively little to copy to the Survivor
space. A Major GC, also called Full GC, collects both the Young and Old Generation. It's much
slower because it has to scan a larger memory region and compact the heap. Full GC pauses,
historically called 'stop-the-world' pauses, can range from tens of milliseconds to several
seconds in large heaps, which is why GC tuning is critical for latency-sensitive applications.
Modern collectors like G1 and ZGC minimize these pauses by doing most of the work
concurrently while the application continues running.

Q: What are Strong, Soft, Weak, and Phantom references?


Very likely
Java's reference types give you fine-grained control over when objects can be garbage
collected. A strong reference is the normal reference you use every day — Object obj = new
Object(). As long as a strong reference exists, the GC will never collect the object. A soft
reference, created using SoftReference, tells the GC: collect this object if and only if memory is
low. It's perfect for memory-sensitive caches — your cache entries will stay in memory as long
as there's plenty of heap space, but will be evicted before an OutOfMemoryError. A weak
reference, created using WeakReference, tells the GC: collect this object at the next GC cycle
regardless of memory pressure. WeakHashMap uses these to hold keys. It's used for
canonicalization maps and associating metadata with objects without preventing their collection.
A phantom reference, created using PhantomReference, is the most exotic — get() always
returns null. You use it with a ReferenceQueue to be notified after an object has been finalized
and is about to be reclaimed, allowing you to perform cleanup actions more reliably than the
deprecated finalize() method.

Q: What causes a memory leak in Java? How do you detect one?


Very likely
A memory leak in Java is when objects that are no longer needed by the application are still
reachable through some reference, preventing the GC from collecting them. Common causes
include: static collections that accumulate objects and are never cleared; listeners or callbacks
that are registered but never deregistered; ThreadLocal variables that aren't removed in thread
pool environments, causing values to persist across requests; unclosed resources like streams
or connections that hold references to their underlying data; and incorrect equals/hashCode
implementations that cause duplicate entries to accumulate in HashSets or HashMaps. To
detect a memory leak, the first symptom is usually a steady increase in heap usage over time,
eventually leading to OutOfMemoryError. The diagnostic approach is to take heap dumps at
different points in time using jmap or a tool like VisualVM or JProfiler. You then compare the
dumps to identify which object types are growing. Once you know the object type, you look at
the reference chain holding it alive to find the leak source. In production, tools like the G1 GC
logs can also reveal patterns — if Old Gen keeps growing between Full GC cycles despite GC
running, that's a strong leak signal.

Q: What is Metaspace? How is it different from PermGen?


Very likely
Before Java 8, the JVM had a fixed-size memory region called Permanent Generation or
PermGen, which stored class metadata, method bytecode, interned strings, and static variables.
PermGen had a fixed maximum size controlled by -XX:MaxPermSize. The most common
production problem was [Link]: PermGen space, which typically occurred
in applications that loaded and unloaded many classes at runtime, like application servers
deploying multiple web apps or applications using byte-code generation libraries heavily. In Java
8, PermGen was replaced by Metaspace. The critical difference is that Metaspace uses native
memory — memory outside the Java heap — and by default has no fixed maximum size. It
grows automatically as needed, up to the system's available memory. This eliminated the
PermGen OOM problem for most applications. However, you should still set
-XX:MaxMetaspaceSize in production to prevent Metaspace from consuming all system
memory, which would destabilize the host machine. Interned strings were moved to the regular
heap in Java 7 and static variables followed in Java 8.

Q: What is OutOfMemoryError? What are its different types?


Very likely
OutOfMemoryError is thrown by the JVM when it cannot allocate an object because there's not
enough memory, and the garbage collector cannot free enough. There are several distinct types
with different causes. 'Java heap space' is the most common — the heap is full and GC cannot
reclaim enough. Causes include genuine memory leaks, insufficient heap size, or loading too
much data into memory at once. 'GC overhead limit exceeded' means the GC is spending more
than 98% of its time collecting and recovering less than 2% of heap space — the JVM gives up
rather than let the application limp along. 'Metaspace' happens when class metadata exceeds
the Metaspace limit. Common in applications that generate classes dynamically. 'Direct buffer
memory' is thrown when native off-heap memory allocated through [Link]() is
exhausted. This is common in Netty-based applications. 'Unable to create new native thread'
happens when the OS cannot create more threads — either you've hit the OS thread limit or
there's not enough native memory for stack allocation. Understanding which type you're seeing
is the first step to solving it.

Q: G1 GC vs ZGC.
Good to know
G1 GC, the default collector since Java 9, divides the heap into equally-sized regions rather than
fixed Young and Old generation areas. It's designed to provide predictable pause times — you
can set a pause time goal with -XX:MaxGCPauseMillis and G1 will try to meet it by collecting
only the most profitable regions. It's a good all-around collector for applications that need a
balance of throughput and latency, and it handles large heaps well. ZGC, introduced
experimentally in Java 11 and productionized in Java 15, has a radically different goal:
single-digit millisecond pause times regardless of heap size. It achieves this by doing almost all
its work concurrently while the application is running, using techniques like load barriers and
colored pointers. The stop-the-world pauses in ZGC are extremely short — typically under a
millisecond. The trade-off is that ZGC uses slightly more CPU and memory compared to G1. If
your application has strict latency requirements — real-time trading, gaming servers, API
gateways — ZGC is the right choice. For general-purpose backend services, G1 is usually
sufficient and better understood.
6. Spring Boot & Spring Framework

Q: What is Dependency Injection and Inversion of Control?


Almost always asked
Inversion of Control is a design principle where the control of object creation and dependency
wiring is transferred from the application code to a container or framework. Normally, if class A
needs an instance of class B, A creates it directly. With IoC, A declares what it needs and the
container creates and provides it. Dependency Injection is one specific way to implement IoC —
the container injects the dependencies into the class, rather than the class fetching them. There
are three forms: constructor injection, where dependencies are provided through the
constructor; setter injection, where they're provided through setter methods; and field injection,
where they're injected directly into fields using @Autowired. In Spring, the ApplicationContext is
the IoC container. It reads configuration — whether from annotations or XML — creates beans,
resolves dependencies, and wires everything together. The benefit is loose coupling — your
class doesn't depend on a specific implementation, just an interface. You can swap
implementations without changing the class, which makes testing trivial by injecting mock
objects.

Q: What is @Transactional and how does it work internally?


Almost always asked
@Transactional is a Spring annotation that demarcates transaction boundaries declaratively.
When you annotate a method with @Transactional, Spring wraps the bean in a proxy at startup
using Spring AOP. When the method is called, the proxy intercepts the call, begins a
transaction, calls the actual method, and then either commits the transaction if the method
returns normally or rolls it back if an unchecked exception is thrown. This proxy-based
mechanism has an important implication: if you call a @Transactional method from within the
same class — a self-invocation — the call bypasses the proxy and the transaction doesn't start.
The fix is to inject the bean into itself or refactor the code so the call goes through the proxy. By
default, @Transactional only rolls back for unchecked exceptions. If you need rollback for
checked exceptions, you must specify rollbackFor. The propagation attribute controls what
happens when a transactional method is called from within an existing transaction — for
example, REQUIRES_NEW suspends the current transaction and starts a fresh one.

Q: Explain Spring Bean scopes.


Almost always asked
Spring supports several bean scopes that control how many instances of a bean are created
and how long they live. Singleton is the default — one instance per Spring container, shared
across the entire application. Every time you inject that bean somewhere, you get the same
instance. Prototype creates a new instance every time the bean is requested — useful for
stateful beans that shouldn't be shared. Request scope creates one instance per HTTP request
— the bean is created when the request comes in and destroyed when it goes out. This is
available only in web-aware application contexts. Session scope creates one instance per HTTP
session. Application scope creates one instance per ServletContext, similar to singleton but in a
web context. The singleton scope is the most common and it's important to remember that
singleton beans must be stateless or thread-safe, because they're shared across all threads. If
you accidentally store request-specific data in a singleton bean field, you'll see data leaking
across requests.

Q: What is Spring Boot auto-configuration?


Almost always asked
Auto-configuration is the mechanism that makes Spring Boot opinionated and reduces
boilerplate setup. When you add a dependency like spring-boot-starter-data-jpa to your project,
Spring Boot's auto-configuration detects it on the classpath and automatically configures a
DataSource, EntityManagerFactory, and TransactionManager with sensible defaults. You don't
write any configuration XML or @Configuration classes for these. Internally,
@SpringBootApplication combines three annotations: @Configuration marks the class as a
configuration source; @ComponentScan tells Spring to scan the current package and
sub-packages for components; and @EnableAutoConfiguration triggers the auto-configuration
mechanism. Under the hood, Spring Boot uses a SpringFactoriesLoader to read [Link]
or spring/[Link] from all jars on the
classpath. Each entry is a @Configuration class annotated with @Conditional annotations — for
example, @ConditionalOnClass means this configuration only applies if a certain class is
present on the classpath. You can see what was auto-configured and why by enabling the
--debug flag.

Q: @Component vs @Service vs @Repository vs @Controller.


Almost always asked
All four are specializations of @Component, meaning they all cause Spring to detect and
register the class as a bean during component scanning. The behavioral difference is subtle but
meaningful. @Component is the generic stereotype for any Spring-managed bean. @Service is
used for service-layer classes — classes containing business logic. Functionally it behaves like
@Component, but it signals intent to other developers. @Repository is used for data access
objects. Beyond signaling intent, it has a functional difference: Spring automatically translates
persistence-specific exceptions — like Hibernate's HibernateException — into Spring's
DataAccessException hierarchy, which is an unchecked exception hierarchy. @Controller is
used for Spring MVC controllers — classes that handle HTTP requests. It signals that the class
is a web layer component. @RestController is a further specialization that combines
@Controller and @ResponseBody, meaning every method's return value is written directly to
the HTTP response body as JSON by default. Using the right stereotype makes code more
readable and enables better tooling support.

Q: Constructor vs setter vs field injection.


Almost always asked
Constructor injection is the recommended approach and what Spring itself recommends as best
practice. With constructor injection, dependencies are provided as constructor parameters,
making them mandatory. This means the object cannot be created in an invalid state — all
dependencies are guaranteed to be present when the constructor returns. It also makes the
class's dependencies immediately visible and makes the class easier to test without a Spring
container — you just call new with mock objects. Setter injection is useful for optional
dependencies, but it allows the object to exist in a partially initialized state, which can lead to
NullPointerExceptions if a dependency isn't set before use. Field injection using @Autowired
directly on fields is the most concise but the worst option. It hides dependencies — you can't see
them without reading the class internals. It makes testing harder because you can't inject mocks
through a constructor or setter. And it requires the Spring container to be running — you can't
instantiate the class normally in a test. The Spring documentation explicitly recommends
constructor injection, and when a class has too many constructor parameters, that's usually a
signal that it's violating the Single Responsibility Principle and should be split up.

Q: What is Spring Actuator?


Very likely
Spring Boot Actuator provides production-ready monitoring and management endpoints for your
application without you having to build them. When you include the actuator dependency, you
get HTTP endpoints that expose operational information. The health endpoint is the most
commonly used — it aggregates health indicators from all configured components like database
connections, disk space, and Kafka consumers, and returns UP or DOWN. This is what
Kubernetes or a load balancer uses to decide whether to route traffic to your instance. The
metrics endpoint exposes application and JVM metrics like heap usage, GC activity, thread
counts, and HTTP request rates, all in a Prometheus-compatible format when combined with
micrometer. The info endpoint exposes application version and build info. The env endpoint
shows all environment properties and their sources. The loggers endpoint lets you change log
levels at runtime without restarting — extremely useful for enabling debug logging on a specific
package during an incident. In production, you should secure the actuator endpoints using
Spring Security and only expose what's necessary, typically health and metrics.

Q: What is @ControllerAdvice and global exception handling?


Very likely
@ControllerAdvice is a Spring annotation that declares a class as a global exception handler,
applicable across all controllers in the application. Without it, you'd have to put
@ExceptionHandler methods in every controller. With @ControllerAdvice, you define exception
handlers in one central place. The typical pattern is to create a class annotated with
@RestControllerAdvice — which combines @ControllerAdvice and @ResponseBody — and
define methods annotated with @ExceptionHandler for each exception type you want to handle.
Each handler method returns a standardized error response object and is annotated with
@ResponseStatus to set the HTTP status code. For example, you'd handle
ResourceNotFoundException with a 404 status and return a JSON body with an error message
and timestamp. The framework matches the most specific exception handler — so a handler for
RuntimeException serves as a catch-all fallback if no more specific handler matches. This
pattern gives you consistent error response format across your entire API, which is important for
API consumers.

Q: Spring Transaction Propagation levels.


Very likely
Transaction propagation defines how a @Transactional method behaves when called from
within an existing transaction. REQUIRED is the default — if a transaction exists, join it; if not,
create a new one. Most operations should use this. REQUIRES_NEW always creates a new
transaction, suspending the existing one if there is one. This is useful when you want an
operation to commit independently — like logging an audit record that should persist even if the
outer transaction rolls back. SUPPORTS joins the existing transaction if one exists, and runs
without a transaction if there isn't one. MANDATORY requires an existing transaction and throws
an exception if there isn't one. NEVER requires no transaction and throws an exception if one
exists. NESTED creates a nested transaction within the existing one using savepoints. If the
nested transaction rolls back, it only rolls back to the savepoint, not the entire outer transaction.
The most commonly discussed pair in interviews is REQUIRED vs REQUIRES_NEW. A
practical example: an order service creates an order in a REQUIRED transaction, then calls a
notification service annotated with REQUIRES_NEW. If the notification fails and rolls back, the
order transaction is unaffected.

Q: What is BeanFactory vs ApplicationContext?


Very likely
BeanFactory is the root interface for Spring's IoC container. It provides basic dependency
injection support — the ability to retrieve beans by name and type. It uses lazy initialization by
default, meaning beans are only created when they're first requested. ApplicationContext
extends BeanFactory and adds enterprise-specific features. It eagerly initializes singleton beans
at startup, which is important for detecting configuration errors early. It provides built-in support
for internationalization through MessageSource. It publishes events to registered listeners
through ApplicationEventPublisher. It loads file resources through ResourceLoader. And it
integrates with Spring AOP, enabling features like @Transactional and @Async. In practice, you
always use ApplicationContext in a Spring application — BeanFactory is mostly a historical
artifact. The specific implementation you encounter in Spring Boot is
AnnotationConfigApplicationContext or, in a web context,
AnnotationConfigWebApplicationContext, created automatically by the [Link]()
call.

Q: What is @Async and how does async processing work?


Very likely
@Async is a Spring annotation that makes a method execute in a separate thread,
asynchronously. When you call an @Async method, Spring's proxy intercepts the call, submits
the method execution to a task executor, and returns immediately to the caller — either a Future,
CompletableFuture, or void, depending on the method's return type. To enable async
processing, you add @EnableAsync to a configuration class. By default, Spring uses a
SimpleAsyncTaskExecutor which creates a new thread for each task — fine for low volume but
not suitable for production. In production, you should define a custom ThreadPoolTaskExecutor
bean and configure core pool size, max pool size, and queue capacity based on your load. The
same self-invocation limitation from @Transactional applies here — calling an @Async method
from within the same class bypasses the proxy and runs synchronously. It's also important to
handle exceptions carefully — if an @Async method throws an unchecked exception and the
return type is void, the exception is silently swallowed by default. You should configure a custom
AsyncUncaughtExceptionHandler to log or handle it.

Q: How do you implement caching in Spring Boot?


Very likely
Spring Boot provides a cache abstraction through the @EnableCaching annotation and the
@Cacheable, @CachePut, and @CacheEvict annotations. @Cacheable on a method means:
before executing the method, check if the result is already in the cache for this set of arguments.
If yes, return the cached result without executing the method. If no, execute the method and
store the result in the cache. @CachePut always executes the method and updates the cache
— useful when you update an entity and want the cache to reflect the new value. @CacheEvict
removes entries from the cache — useful when you delete an entity. The cache abstraction is
provider-agnostic. By default, Spring uses a simple ConcurrentHashMap-based cache, which is
only suitable for single-instance applications. For distributed systems, you'd use Redis or
Hazelcast as the backing store. With Redis, you add spring-boot-starter-data-redis and
configure the connection, and Spring Boot auto-configures Redis as the cache manager. For
cache strategy, TTL-based expiration ensures data doesn't go stale indefinitely. For highly
dynamic data, event-driven eviction — invalidating the cache when data changes — is more
accurate.

Q: What is Spring Batch?


Good to know
Spring Batch is a framework for building robust batch processing applications — applications
that process large volumes of data in scheduled, repetitive jobs. The core concepts are Job,
Step, ItemReader, ItemProcessor, and ItemWriter. A Job is the overall batch process. A Step is
a phase within the job. ItemReader reads data from a source — a database, flat file, or message
queue. ItemProcessor transforms or filters each item. ItemWriter writes the processed items to a
destination. Spring Batch supports chunk-oriented processing — it reads a configurable number
of items, processes them, and writes them in one transaction. If a chunk fails, it rolls back just
that chunk. It has built-in support for restartability — if a job fails midway, it can be restarted from
where it left off rather than from the beginning. Real-world use cases include generating monthly
invoices, importing CSV files into a database, generating reports, and syncing data between
systems. In my projects, I've used Spring Batch for nightly data reconciliation jobs between our
service and third-party providers.

Q: How do you configure CORS in Spring Boot?


Good to know
CORS — Cross-Origin Resource Sharing — is a browser security mechanism that restricts web
pages from making requests to a different domain than the one that served the page. When your
React frontend running on localhost:3000 calls your Spring Boot API on localhost:8080, the
browser sends a preflight OPTIONS request first. If the server doesn't respond with the right
CORS headers, the browser blocks the actual request. In Spring Boot, you can configure CORS
globally in a WebMvcConfigurer bean by overriding addCorsMappings. You specify which
origins are allowed, which HTTP methods, which headers, and whether credentials like cookies
are allowed. In Spring Security, you need to configure CORS through the SecurityFilterChain as
well — specifically by calling cors() in the security configuration and providing a
CorsConfigurationSource. If you configure CORS only in WebMvc but not in Security, Spring
Security's filters will still block the preflight request before it reaches the MVC layer. For
production, you should never use allowedOrigins('*') with allowCredentials(true) — that's a
security vulnerability.

Q: How do you do API versioning in Spring Boot?


Good to know
There are four main strategies for API versioning. URL path versioning puts the version in the
URL: /api/v1/users vs /api/v2/users. It's the most explicit and easiest to test in a browser, but it
means URLs change and bookmarks break. Request parameter versioning uses a query
parameter: /api/users?version=1. It's flexible but mixing concerns in query parameters feels
wrong to many API designers. Header versioning uses a custom header like API-Version: 1.
This keeps the URL clean and is RESTful in spirit, but it's harder to test and document. Media
type versioning, also called content negotiation, uses the Accept header: Accept:
application/[Link].v1+json. This is the most REST-correct approach but the hardest to
work with in practice. In most teams I've worked with, URL path versioning wins for its clarity and
simplicity, even if it's not the 'purest' REST approach. The important operational consideration is
how long you maintain old versions — a clear deprecation timeline and sunset headers in
responses help API consumers plan migrations.
7. Spring Security & JWT

Q: How does JWT authentication work end-to-end?


Almost always asked
JWT stands for JSON Web Token and it's a compact, self-contained way to transmit
authentication information between parties. The flow works like this: the user sends their
credentials to the login endpoint. The server validates them and, if correct, generates a JWT.
The token has three parts separated by dots: a header that specifies the token type and signing
algorithm, a payload that contains claims — pieces of information like user ID, roles, and
expiration time — and a signature that's the base64-encoded hash of the header and payload,
signed with a secret key or private key. The server returns this token to the client. On
subsequent requests, the client includes the token in the Authorization header as 'Bearer '. The
server extracts the token, re-computes the signature using its secret key, and compares it to the
signature in the token. If they match, the token is authentic and hasn't been tampered with. The
server then reads the claims from the payload without any database lookup. This is the essence
of JWT's statelessness — the server doesn't need to store session data because all the
necessary information is in the token itself.

Q: Authentication vs Authorization.
Almost always asked
Authentication and authorization are two distinct steps in security. Authentication is about
identity — proving who you are. It answers the question 'who is this user?' The process involves
verifying credentials — typically a username and password, a token, a certificate, or biometrics.
When authentication succeeds, the system knows your identity. Authorization is about
permissions — determining what you're allowed to do. It answers the question 'what is this
authenticated user permitted to access?' Once the system knows who you are, it checks
whether you have the necessary permissions or roles to perform the requested action. In Spring
Security, these are two separate filter chains. Authentication is handled by
AuthenticationManager, and a successful authentication result is stored in the SecurityContext.
Authorization is then applied to requests through access control rules defined in the security
configuration — for example, requiring the ADMIN role to access /api/admin/** endpoints. The
order matters — authentication must succeed before authorization is evaluated.

Q: How is JWT stateless? Stateless vs stateful tradeoffs.


Almost always asked
JWT authentication is stateless because the server doesn't store any session information.
Everything needed to authenticate and authorize the user is encoded in the token itself. The
server only needs its secret key to verify the signature. Compare this to session-based
authentication: when a user logs in, the server creates a session, stores it in memory or a
session store like Redis, and gives the client a session ID cookie. On every request, the server
looks up the session by ID. That lookup is what makes it stateful. Stateless JWT has clear
advantages for distributed systems — any server in a cluster can verify a token without needing
to share session state. You don't need sticky sessions. It scales horizontally trivially. The
downsides are real though. You cannot invalidate a JWT before it expires. If a user logs out or
their account is compromised, the token remains valid until expiration. Mitigation strategies
include keeping token lifetimes short — 15 minutes for access tokens — combined with
longer-lived refresh tokens stored server-side in a database, allowing you to invalidate refresh
tokens and prevent issuance of new access tokens. You can also maintain a token blacklist in
Redis for immediate revocation, though that partially defeats the statelessness advantage.

Q: What are Spring Security Filters? How does the filter chain work?
Very likely
Spring Security is implemented as a chain of servlet filters that intercept every HTTP request
before it reaches your controllers. Each filter has a specific responsibility: some handle
authentication, some handle authorization, some handle CSRF protection, and some handle
session management. The chain is ordered and each filter decides whether to pass the request
to the next filter or short-circuit by writing a response directly. Key filters include
UsernamePasswordAuthenticationFilter which handles form-based login,
BasicAuthenticationFilter for HTTP Basic Auth, ExceptionTranslationFilter which translates
Spring Security exceptions into HTTP responses, and FilterSecurityInterceptor which enforces
access control rules. In a JWT setup, you write a custom filter — typically extending
OncePerRequestFilter — that reads the Authorization header, validates the token, and
populates the SecurityContext with the authenticated user. This filter is inserted before
UsernamePasswordAuthenticationFilter in the chain. If token validation fails, the filter simply
doesn't populate the SecurityContext and the request proceeds unauthenticated, which the
authorization filter then rejects with a 401.

Q: What is OAuth2? When and why to use it in microservices?


Very likely
OAuth2 is an authorization framework that allows a user to grant a third-party application limited
access to their resources without sharing their credentials. The Authorization Code flow, the
most common and secure flow, works like this: the user wants to log into your app using their
Google account. Your app redirects them to Google's authorization server. The user
authenticates with Google and grants permission. Google redirects back to your app with an
authorization code. Your app exchanges the code for an access token — this exchange
happens server-side, keeping the token out of the browser. In microservices, OAuth2 is valuable
because it provides a centralized authorization server. Instead of every microservice
implementing its own authentication, all services trust the authorization server. The API gateway
validates incoming tokens against the authorization server, and the resulting authentication
context is passed to downstream services via headers. This removes the burden of
authentication from each service. The access token — usually a JWT — carries scopes and
claims that services use for authorization decisions. It's more scalable and secure than requiring
each microservice to verify credentials independently.

Q: How do you handle JWT expiration and refresh tokens?


Very likely
Short-lived access tokens — typically 15 minutes — minimize the window of exposure if a token
is stolen, but they'd force users to re-login constantly. Refresh tokens solve this. When the user
first authenticates, they receive both a short-lived access token and a long-lived refresh token —
perhaps 7 or 30 days. The access token is kept in memory on the client side to prevent XSS
access. The refresh token is stored in an HttpOnly, Secure cookie, making it inaccessible to
JavaScript and protected over HTTPS. When the access token expires, the client silently sends
the refresh token to a dedicated refresh endpoint. The server validates the refresh token against
a stored record in the database — this is why refresh tokens must be stored server-side — and
issues a new access token. Storing refresh tokens in a database also enables revocation:
logging out means deleting the refresh token record, so no new access tokens can be issued.
Token rotation is a good security practice — on each refresh, invalidate the old refresh token
and issue a new one. This way, if an attacker steals a refresh token and uses it, the original
token is invalidated and the legitimate user will detect the forced logout.

Q: What is CSRF? How does Spring Security handle it?


Very likely
Cross-Site Request Forgery is an attack where a malicious website tricks an authenticated
user's browser into making a request to your server. Because browsers automatically include
cookies with requests to a domain, if the user is logged into your site and a malicious page
makes a POST request to it, the browser sends the session cookie along, and the server thinks
it's a legitimate request. Spring Security's default CSRF protection uses the synchronizer token
pattern: when you render a form, Spring includes a unique CSRF token. When the form is
submitted, the server validates that the CSRF token in the request matches the one it issued. An
attacker's malicious page can't read this token due to same-origin policy, so it can't forge a valid
request. For REST APIs using stateless JWT authentication in the Authorization header, CSRF
is typically disabled. The reason is that cookies are the attack vector — CSRF attacks exploit
automatic cookie transmission. If you're not using cookies for authentication — if the token is in
a header set by JavaScript — CSRF is not a risk because JavaScript can only read from the
same origin. In Spring Security's configuration, when you switch to JWT you typically call
csrf().disable().

Q: What is CORS? How do you configure it?


Very likely
CORS stands for Cross-Origin Resource Sharing and is a browser-enforced security
mechanism. The same-origin policy prevents a page at origin A from making requests to origin B
unless B explicitly allows it. The origin is the combination of protocol, domain, and port — so
localhost:3000 and localhost:8080 are different origins even on the same machine. When the
browser detects a cross-origin request, it may send a preflight OPTIONS request first, asking the
server what it allows. The server responds with CORS headers: Access-Control-Allow-Origin
specifies which origins are permitted, Access-Control-Allow-Methods lists allowed HTTP
methods, Access-Control-Allow-Headers lists allowed request headers, and
Access-Control-Allow-Credentials indicates whether cookies can be included. In Spring Boot,
you configure CORS through a WebMvcConfigurer, the @CrossOrigin annotation on a
controller, or through Spring Security's cors() configuration. Important security note: using
allowedOrigins(*) with credentials enabled is a misconfiguration and a security vulnerability. You
should always explicitly list trusted origins in production.
Q: How can you revoke a compromised JWT?
Very likely
This is the fundamental tension in JWT authentication: because tokens are self-contained and
verified without a database lookup, there's no built-in mechanism to invalidate one before it
expires. There are several practical solutions. The most common is a token blacklist in Redis.
When a token is revoked — because of logout, password change, or compromise — you store
the token's JTI claim — a unique token identifier — in Redis with an expiry matching the token's
remaining validity. On every request, after verifying the signature, you check Redis for the JTI. If
it's present, reject the token. This is fast because Redis lookups are O(1) and Redis is
in-memory. The downside is partial stateful behavior. Another approach is to keep access token
lifetimes very short — 5 minutes. At worst, a compromised token is valid for 5 minutes.
Combined with refresh token rotation and immediate refresh token revocation in the database,
the exposure window is minimal. A third approach, used in high-security systems, is to include
the user's password hash version in the token. When the password changes, all old tokens fail
validation automatically.

Q: What signing algorithms are used for JWT?


Good to know
JWT signing algorithms come in two families: symmetric and asymmetric. HS256 is the most
commonly used symmetric algorithm. HMAC-SHA256 uses a single secret key for both signing
and verification. It's fast and simple, but it means every service that needs to verify tokens must
share the same secret key. If any service is compromised, the secret is exposed. RS256 is an
asymmetric algorithm using RSA. A private key signs the token and a public key verifies it. The
authorization server holds the private key securely, and it publishes the public key through a
JWKS endpoint. Any service can download the public key and verify tokens independently,
without the risk of exposing the signing key. This is the standard approach in OAuth2 and
OpenID Connect. ES256 is similar but uses Elliptic Curve cryptography — it provides the same
security as RS256 but with shorter keys and faster computation. In microservices architectures,
RS256 or ES256 is the right choice because you can share the public key broadly without any
security risk.
8. Microservices

Q: Monolith vs Microservices — tradeoffs. When would you NOT use


microservices?
Almost always asked
A monolith is a single deployable unit where all the application's functionality is packaged
together. Microservices decompose the application into small, independently deployable
services, each owning its own data and communicating over the network. The monolith's
advantages are simplicity — there's no network overhead between components, transactions
are local, testing is straightforward, and debugging is simple with a single log stream.
Microservices offer independent deployability — you can update the order service without
touching the inventory service. Teams can work independently on different services. Services
can be scaled individually — you scale only the service under load, not the entire application.
Each service can use the technology best suited to its problem. But microservices introduce
significant complexity: distributed transactions, eventual consistency, network failures, service
discovery, distributed tracing, and the operational overhead of deploying and monitoring many
services. I would not choose microservices for a small team, a new product that's still finding its
domain boundaries, or when the overhead of distributed systems outweighs the benefits. The
advice I've seen work in practice is: start with a well-structured monolith and extract services
only when you have clear pain points — a bounded context that needs to scale independently,
or a team that needs to deploy independently.

Q: How do microservices communicate? Sync vs Async.


Almost always asked
Microservices communicate in two fundamental styles. Synchronous communication means the
caller waits for a response. REST over HTTP is the most common — straightforward, widely
understood, and easy to debug. gRPC is an increasingly popular alternative that uses Protocol
Buffers for serialization and HTTP/2 for transport, giving you better performance and strong
contract enforcement through protobuf schemas. The downside of synchronous communication
is temporal coupling — if the called service is down or slow, the caller is directly affected.
Asynchronous communication uses a message broker like Kafka or RabbitMQ as an
intermediary. The producer publishes a message and moves on without waiting. The consumer
processes it when it's ready. This decouples services in time — if the consumer is down, the
message waits in the queue. It also enables fan-out — one event can trigger multiple
consumers. Asynchronous is ideal for workflows where you don't need an immediate response,
like sending a confirmation email after an order is placed. In practice, most systems use both:
REST for queries and synchronous commands where the user needs immediate feedback, and
messaging for events and background processing.

Q: How do you handle distributed tracing across microservices?


Almost always asked
In a microservices system, a single user request might touch five or six services. When
something goes wrong — a slowdown or an error — you need to trace the entire journey of that
request across all services to diagnose the problem. Distributed tracing is the solution. The
standard is OpenTelemetry, which defines a model where every incoming request generates a
trace ID — a unique identifier for the entire journey. Each service-to-service call creates a span,
which represents a unit of work within the trace. Spans carry the trace ID and a parent span ID,
building a tree structure. The implementation in Spring Boot typically uses Micrometer Tracing
with an OpenTelemetry or Brave bridge, and the traces are sent to a backend like Jaeger or
Zipkin. The trace context is propagated in HTTP headers — typically W3C Trace Context
headers — so when service A calls service B, it includes the trace ID and span ID in the request
headers, and service B creates a child span. In practice I also include the trace ID in every log
statement using MDC — Mapped Diagnostic Context — so that logs across all services for a
single request can be correlated by searching for the trace ID in a log aggregation tool like ELK
or Grafana Loki.

Q: What is the Saga pattern?


Almost always asked
In a microservices architecture, a single business operation might span multiple services, each
with its own database. You can't use a traditional two-phase commit transaction across service
boundaries. The Saga pattern is the solution — it's a sequence of local transactions, one per
service, where each transaction publishes an event or message that triggers the next step.
There are two styles. Choreography-based saga has no central coordinator — each service
listens for events and reacts. When the order service creates an order, it publishes an
OrderCreated event. The payment service listens for it, charges the customer, and publishes
PaymentSucceeded. The inventory service listens for PaymentSucceeded and reserves stock. If
any step fails, that service publishes a failure event and the preceding services run
compensating transactions to undo their work — for example, a PaymentFailed event triggers
the order service to cancel the order. Orchestration-based saga uses a central coordinator,
called the saga orchestrator, that explicitly tells each service what to do and handles the failure
and compensation logic. The choreography approach is more decoupled but harder to track and
debug. Orchestration gives you a clearer view of the overall workflow but introduces a central
component that becomes a dependency. For complex workflows with many failure scenarios,
orchestration is usually more maintainable.

Q: How do you achieve data consistency in microservices?


Very likely
Strong consistency across microservices is very difficult and usually the wrong goal. The better
goal is eventual consistency — accepting that at any given moment some services might have
slightly stale data, but the system will converge to a consistent state. The Saga pattern handles
transactional consistency across services. For read-side consistency, the CQRS pattern —
Command Query Responsibility Segregation — separates write operations from read
operations. When a write happens in a service, it publishes a domain event. Other services
subscribe to these events and update their own read models. For example, when an order is
created, the reporting service gets a copy of the relevant order data to maintain its own view.
This is called event-driven architecture. The Outbox pattern solves the dual write problem — the
risk of writing to a database and then failing before publishing the event, leaving the system in
an inconsistent state. Instead of publishing to the broker directly, you write both the domain
record and the event to an outbox table in the same local transaction. A separate process then
reads the outbox and publishes events reliably.

Q: What is an API Gateway? How does it differ from a Load Balancer?


Very likely
A load balancer operates at the network layer and distributes traffic across multiple instances of
the same service. It's about availability and scalability — spreading load so no single instance is
overwhelmed. It doesn't understand the content of the requests. An API Gateway operates at
the application layer and understands HTTP semantics. It's a single entry point for all external
traffic into your microservices system. Beyond routing requests to the appropriate service, it
handles cross-cutting concerns: authentication and authorization — verifying JWT tokens once
at the gateway rather than in every service; rate limiting — preventing a single client from
overwhelming your system; request and response transformation; SSL termination; logging and
observability; and protocol translation. In Spring Cloud, Spring Cloud Gateway is a popular
choice. In larger systems, dedicated API gateway products like Kong, AWS API Gateway, or
Apigee are used. The conceptual difference is that a load balancer routes to instances of one
service, while an API gateway routes to different services based on request paths and applies
application-level policies.

Q: What is the Circuit Breaker pattern?


Very likely
The Circuit Breaker is a resilience pattern that prevents cascading failures in distributed
systems. Without it, if service B is slow or down, service A keeps sending requests that pile up,
eventually exhausting A's thread pool and taking A down too, spreading the failure through the
system. The circuit breaker sits between A and B and tracks the success and failure rate of calls.
It operates in three states. Closed is the normal state — requests flow through, failures are
tracked. If the error rate crosses a threshold — say 50% of requests in 10 seconds fail — the
circuit opens. Open means the circuit is broken — requests fail immediately without even
reaching service B. This gives B time to recover and prevents A from being overwhelmed with
blocked threads. After a configured wait time, the circuit transitions to Half-Open: a small
number of test requests are allowed through. If they succeed, the circuit closes again. If they fail,
it opens again. In the Java ecosystem, Resilience4j is the standard library for circuit breakers,
replacing the deprecated Hystrix. It integrates with Spring Boot through
spring-cloud-starter-circuitbreaker-resilience4j, and you can configure it declaratively with
annotations or programmatically.

Q: How do you achieve zero-downtime deployment?


Very likely
Zero-downtime deployment requires that at no point during a release are there no running
instances of your service. Several strategies achieve this. Rolling deployment gradually replaces
old instances with new ones — if you have 10 instances, it might update 2 at a time. The load
balancer only routes to healthy instances, so traffic continues to flow. The risk is that both
versions run simultaneously, so your API must be backward compatible. Blue-green deployment
maintains two identical environments. Blue is production, green is the new version. You deploy
to green, run tests, and then switch the load balancer to point to green. If something goes wrong,
you switch back to blue instantly. The downside is you need double the infrastructure. Canary
deployment routes a small percentage of traffic — say 5% — to the new version. You monitor
error rates and latency. If metrics look good, you gradually increase the percentage until the new
version handles 100% of traffic. This reduces risk because a bad deployment only affects a
small percentage of users. For database changes, zero-downtime requires that migrations are
backward compatible with the running version — expand-contract pattern: first add the new
column without removing the old one, deploy the new code that can work with both, then in a
subsequent release drop the old column.

Q: How do you monitor microservices in production?


Very likely
Monitoring a fleet of microservices requires observability across three pillars: metrics, logs, and
traces. For metrics, each Spring Boot service exposes a /actuator/metrics endpoint through
Micrometer, which formats data for Prometheus. Prometheus scrapes these endpoints and
stores the metrics in a time-series database. Grafana provides dashboards visualizing request
rates, error rates, latency percentiles, JVM heap usage, and GC activity. You configure alerts in
Grafana or AlertManager to notify on-call engineers when error rates spike or latency exceeds
SLO thresholds. For logs, each service writes structured JSON logs. A log aggregator like
Fluentd or Logstash collects logs from all containers and sends them to Elasticsearch or
Grafana Loki. Kibana or Grafana provides search and visualization. Because all logs include the
trace ID from the MDC, you can search for a specific trace ID and see all log entries from all
services for a single request. For traces, Jaeger or Zipkin collects spans and visualizes the call
graph for each request, showing exactly where latency is occurring. The combination of these
three tools gives you a complete picture of the system's behavior.

Q: What is Eureka Server?


Very likely
Eureka is a service discovery server developed by Netflix and integrated into the Spring Cloud
ecosystem. In a microservices architecture where services run on dynamic IP addresses —
especially in containerized environments — you can't hardcode the address of a service you
want to call. Service discovery solves this. When a microservice starts, it registers itself with the
Eureka server, providing its name, IP address, port, and health indicator URL. The Eureka
server maintains a registry of all registered services. When service A wants to call service B, it
asks Eureka for the instances of service B and gets back their addresses. Service A can then
load balance across those instances. Each service sends periodic heartbeats to Eureka. If a
heartbeat is missed for long enough, Eureka removes the instance from the registry. This
self-healing behavior means the registry stays accurate even as instances come and go. In
modern Kubernetes-based deployments, Kubernetes provides its own service discovery through
kube-dns, and Eureka's role is less relevant — you use Kubernetes Services to discover other
services. But in VM-based deployments or older Spring Cloud architectures, Eureka remains the
standard.

Q: What are contract-driven tests?


Good to know
Contract-driven testing, specifically Consumer-Driven Contract testing, is a testing approach for
microservices where the consumer of an API defines what it expects — the contract — and the
provider is tested against those expectations. The most popular tool for this in the Java
ecosystem is Spring Cloud Contract. The consumer team writes contracts specifying the
expected request format and response structure. These contracts are shared with the provider
team, who runs tests verifying their service fulfills them. This is a significant improvement over
integration tests that require both services to be running simultaneously. Because contracts are
code, they evolve alongside the API. If a provider wants to make a change that breaks a
contract, the contract tests fail immediately, before deployment. This catches breaking changes
early in the development cycle rather than in production. It also enables teams to develop and
test independently — the consumer can generate a stub from the contract and test against the
stub without the real provider being available.
9. Hibernate & JPA

Q: What is the N+1 problem in Hibernate? How do you fix it?


Almost always asked
The N+1 problem is one of the most common and impactful performance issues in Hibernate. It
occurs when fetching a list of N entities triggers N additional queries to fetch their associations,
resulting in N+1 total queries. Consider an Order entity with a collection of OrderItems mapped
as a lazy association. If you fetch 100 orders, Hibernate runs one query to load the orders. Then
when your code accesses the items of each order — perhaps in a loop — Hibernate fires a
separate query for each order's items. That's 100 additional queries, 101 total. This is often
invisible in development with small datasets but catastrophic in production. The primary solution
is to use JOIN FETCH in JPQL or the EntityGraph API to tell Hibernate to fetch the association
in the same query: SELECT o FROM Order o JOIN FETCH [Link] WHERE ... This produces a
single query with a join instead of 101 queries. Another solution is to use batch fetching —
@BatchSize(size = 25) on the association tells Hibernate to load the associations for 25 entities
at a time instead of one at a time, reducing 100 queries to 4. You can detect N+1 problems by
enabling SQL logging in development and looking for repeated similar queries, or by using a tool
like Hibernate Statistics or Datasource Proxy.

Q: First-level cache vs second-level cache in Hibernate.


Almost always asked
Hibernate has two levels of caching. The first-level cache is enabled by default and is scoped to
the Hibernate Session — it's essentially the Session's identity map. Within a single Session, if
you load an entity with a given ID, then load the same entity again, Hibernate returns the same
object reference without hitting the database the second time. This is transparent — you can't
disable it for a specific lookup within a session. When the Session is closed, the first-level cache
is cleared. The second-level cache is optional and scoped to the SessionFactory, meaning it's
shared across multiple Sessions and therefore multiple requests. If entity A is loaded in request
1 and cached in the second-level cache, request 2 can retrieve it from the cache without a
database hit. Common second-level cache providers are Ehcache and Infinispan. You enable it
with @Cache on your entity and configure the concurrency strategy — READ_ONLY for entities
that never change, READ_WRITE for entities that change occasionally. In a clustered
environment, you need a distributed cache implementation so all nodes share the same cache
view. The query cache, which can cache the results of specific queries, is a third related concept
that works in conjunction with the second-level cache.

Q: Lazy vs eager loading. What is LazyInitializationException?


Very likely
Eager loading means Hibernate loads associated entities immediately when the parent entity is
loaded, in the same query or a follow-up query. Lazy loading defers fetching associations until
they're first accessed in code. The default for @OneToMany and @ManyToMany is LAZY, and
for @ManyToOne and @OneToOne it's EAGER — though it's often recommended to change
@ManyToOne to LAZY as well for better control. LazyInitializationException is one of the most
common Hibernate errors. It occurs when you try to access a lazy-loaded association after the
Hibernate Session has been closed. In a Spring application, the Session is typically open only
for the duration of the service method annotated with @Transactional. If your controller calls a
service method, gets an entity back, and then the view or response serializer tries to access a
lazy association, the Session is already closed and you get the exception. Solutions include:
fetching the association eagerly for that specific query using JOIN FETCH or EntityGraphs;
enabling the Open Session in View pattern — though this is controversial and often considered
an antipattern for its implicit performance costs; or using DTOs to explicitly serialize what you
need within the transaction boundary.

Q: What is the difference between get() and load() in Hibernate?


Very likely
Both get() and load() retrieve an entity by its primary key, but they behave differently when the
entity doesn't exist and in how they interact with the database. get() hits the database
immediately and returns the actual object, or null if the entity doesn't exist. It's straightforward
and predictable. load() returns a proxy object without immediately hitting the database. The SQL
query is deferred until you first access a property of the entity. If the entity doesn't exist in the
database, load() won't fail immediately — the failure comes as an ObjectNotFoundException
when you later access the proxy. The use case for load() is when you're confident the entity
exists and you need a reference to it — for example, to set it as a foreign key on another entity.
If you have an Order and you want to set its associated Customer, you don't need to load all the
Customer's data — you just need a reference. load() gives you that proxy without a database
round trip. In modern Spring Data JPA, these are less directly relevant — you use findById
which returns Optional and uses get semantics.

Q: JPA vs Hibernate.
Very likely
JPA — Java Persistence API — is a specification, a set of interfaces and annotations that define
a standard way to map Java objects to relational database tables. It defines annotations like
@Entity, @Table, @Column, @OneToMany, and the EntityManager API for CRUD operations.
Hibernate is an ORM framework that implements the JPA specification. It's the most widely used
JPA provider, but there are others like EclipseLink and OpenJPA. In a Spring Boot application,
you typically program to the JPA interfaces — you use EntityManager or Spring Data JPA's
repository abstractions — and Hibernate provides the actual implementation underneath. The
benefit of coding to the JPA standard is portability — theoretically you could swap Hibernate for
another JPA provider. In practice, most teams use some Hibernate-specific features like
@BatchSize, SessionFactory, or specific query hints that go beyond the JPA spec. JPA gives
you the standard, Hibernate gives you the power and extensions beyond the standard.

Q: What JPA cascade types are there?


Very likely
Cascade types define what happens to related entities when an operation is performed on the
parent. [Link] means when you persist the parent, associated child entities
are also persisted automatically — you don't have to call persist on each child separately.
[Link] propagates merge operations to children. [Link]
means deleting the parent also deletes all associated children — use this carefully because it
can lead to accidental mass deletions. [Link] propagates refresh operations.
[Link] detaches associated entities when the parent is detached.
[Link] is a shorthand for all of the above. In practice, PERSIST and MERGE are
commonly used for owned associations where the child's lifecycle is tightly bound to the parent
— like Order and OrderItems. REMOVE is used less frequently and only when you're certain
that children should never exist without the parent. You should avoid cascading to shared
entities — if multiple parents reference the same child, cascading REMOVE from one parent
would delete the child that others still reference, causing integrity violations.

Q: How would you save millions of rows efficiently using Hibernate?


Good to know
Naive Hibernate batch insertion is extremely slow because by default Hibernate flushes and
clears the session after each entity, generating one INSERT per row. To handle millions of rows
efficiently, several techniques are combined. First, enable JDBC batching by setting
[Link].batch_size to a value like 50 in your configuration. This tells Hibernate to send 50
INSERTs in one JDBC call rather than 50 individual calls. Second, use ordered inserts with
hibernate.order_inserts=true so Hibernate groups inserts of the same type together, which is
required for batching to work with identity-type ID generation. Third, periodically flush and clear
the Session in your processing loop — after every batch, call [Link]() followed by
[Link](). Without clearing, the first-level cache accumulates all entities in memory,
eventually causing an OutOfMemoryError. Fourth, avoid using identity-type generated IDs like
auto-increment if you can, because Hibernate must execute each insert and get the generated
ID before it can batch the next one. Sequence-based IDs with allocation size matching your
batch size avoids this problem. For truly massive imports, sometimes bypassing Hibernate
entirely and using Spring's JdbcTemplate with batch updates, or using COPY commands in
PostgreSQL, is the right approach.

Q: What is @Version in Hibernate?


Good to know
@Version is Hibernate's annotation for enabling optimistic locking on an entity. You add a field
annotated with @Version — typically an integer or timestamp — to your entity. Hibernate
automatically increments this field on every update. When you read an entity, you get its current
version number. When you update it, Hibernate generates a SQL UPDATE with a WHERE
clause that includes both the entity's ID and the expected version number. If the version in the
database no longer matches — because another transaction updated the entity in between —
the UPDATE affects zero rows, and Hibernate throws an OptimisticLockException. Optimistic
locking is appropriate for scenarios where concurrent modifications are rare — you optimistically
assume no conflict and only detect and handle it when it occurs. This is more scalable than
pessimistic locking, which holds a database lock for the duration of a transaction and blocks all
concurrent access. The trade-off is that you need to handle the OptimisticLockException
gracefully, typically by retrying the operation or presenting a conflict error to the user.
10. Design Patterns

Q: Explain Singleton. How do you break it? How do you prevent that?
Almost always asked
The Singleton pattern ensures a class has only one instance and provides a global access point
to it. A thread-safe implementation uses double-checked locking: check if the instance is null,
synchronize only if it is, then check again inside the synchronized block before creating the
instance. The volatile keyword on the instance field prevents instruction reordering. There are
three ways to break a singleton. Reflection can access the private constructor and call
newInstance(), bypassing the private access modifier. Serialization creates a new instance
when deserializing, bypassing the constructor entirely. And cloning through the Cloneable
interface creates a copy. To prevent reflection attacks, throw an exception in the constructor if
an instance already exists. To prevent serialization issues, implement readResolve() and return
the existing instance. To prevent cloning, override clone() and throw
CloneNotSupportedException. The cleanest and most bulletproof singleton implementation in
Java is actually the enum singleton — since enums guarantee exactly one instance per
constant, handle serialization correctly by design, and cannot be instantiated by reflection. In
Spring applications, you rarely implement Singleton manually — Spring beans are singletons by
default, and the container manages the lifecycle.

Q: What is the Builder pattern?


Almost always asked
The Builder pattern is used to construct complex objects step by step. It's most valuable when
an object has many optional parameters — the alternative, telescoping constructors with
different parameter combinations, becomes unmanageable beyond three or four parameters
and is hard to read at the call site. With Builder, you create a static inner Builder class. It mirrors
the target class's fields and provides a fluent API — methods that set each field and return the
Builder itself, enabling method chaining. A final build() method validates the configuration and
creates the target object. Lombok's @Builder annotation generates all this code automatically. In
practice, I use the Builder pattern extensively for DTO construction in test code, for creating
request objects for external API calls, and for value objects with many optional fields. The
immutability benefit is significant too — because you configure the Builder and only create the
object at the end, the resulting object can be fully immutable. Builders are also great for
readability: new [Link]().name('Alice').age(30).email('alice@[Link]').build()
reads almost like English, unlike a constructor call where the parameters are positional and
ambiguous.

Q: What is the Factory Method pattern?


Very likely
The Factory Method pattern defines an interface for creating an object but lets subclasses
decide which class to instantiate. It's about delegation of instantiation to subclasses. A more
common variation, often called the Simple Factory or Static Factory, is a method that returns
different types based on a parameter. For example, a NotificationFactory with a method
createNotification(String type) that returns an EmailNotification, SMSNotification, or
PushNotification based on the type string. The caller doesn't need to know about the concrete
classes — it just calls the factory. The benefits are that object creation logic is centralized in one
place, making it easy to add new types without changing the client code — as long as the new
type implements the same interface. In Spring, the BeanFactory itself is a sophisticated factory.
In real applications, factories often read from configuration to decide which implementation to
create, enabling runtime behavior changes without code modifications.

Q: What is the Strategy pattern?


Very likely
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them
interchangeable. Instead of implementing multiple behaviors as if-else chains inside a class, you
extract each behavior into its own class implementing a common interface, and the context class
holds a reference to the current strategy. For example, a payment system might have different
payment strategies: CreditCardPayment, PayPalPayment, and CryptoPayment, all
implementing a PaymentStrategy interface with a pay(amount) method. The PaymentService
holds a reference to the current strategy and delegates to it. Switching payment methods means
changing which strategy is in use, without any change to the service itself. With Java 8 lambdas,
strategies that are single methods can be passed as functional interfaces directly, eliminating
the need for separate strategy classes for simple cases. The Strategy pattern is the runtime
polymorphism I reach for when I find myself writing if [Link]('A') do this else if
[Link]('B') do that — that's almost always better expressed as a strategy.

Q: What is the Observer pattern?


Very likely
The Observer pattern defines a one-to-many dependency between objects — when one object
changes state, all its dependents are notified automatically. The subject maintains a list of
observers and notifies them on state changes. Observers implement an interface with an update
method. This is the foundation of event-driven architecture. Spring's event system is a built-in
implementation. You publish events using [Link]() and handle
them in @EventListener methods. This decouples the publisher from subscribers — the order
service publishes OrderCreatedEvent and doesn't know or care whether the email service,
inventory service, or analytics service handles it. In Java, [Link] and Observer
were the classic implementation, though they're deprecated in Java 9. The pattern is also the
basis for reactive programming with libraries like RxJava and Project Reactor, where you
subscribe to an observable stream of events. Message brokers like Kafka generalize this to
distributed systems — producers publish to topics and multiple consumer groups independently
receive every message.

Q: What is the Decorator pattern?


Good to know
The Decorator pattern attaches additional responsibilities to an object dynamically, providing a
flexible alternative to subclassing. Decorators wrap the object they're decorating and add
behavior before or after delegating to it. Java's I/O streams are the canonical example —
BufferedInputStream wraps an InputStream and adds buffering; DataInputStream wraps a
stream and adds methods to read primitive types. You can stack decorators: new
BufferedInputStream(new DataInputStream(new FileInputStream(file))). Each adds a layer of
behavior. The decorator implements the same interface as the component it wraps, so it's
transparent to the client — you can use a decorated object wherever the base type is expected.
In Spring, this pattern appears in AOP — Spring's transaction and caching proxies are
decorators that wrap your beans and add cross-cutting behavior. The advantage over
subclassing is that you can add behavior to specific instances rather than all instances of a
class, and you can combine behaviors combinatorially without creating an explosion of
subclasses.
11. REST APIs & Best Practices

Q: What is idempotency in REST? Which HTTP methods are idempotent?


Almost always asked
Idempotency means that performing the same operation multiple times has the same effect as
performing it once. In HTTP, GET, PUT, DELETE, HEAD, and OPTIONS are idempotent. POST
is not idempotent — submitting the same POST request twice typically creates two resources.
GET is obviously idempotent because it only retrieves data. PUT is idempotent because putting
the same representation twice results in the same state — the second request just overwrites
with identical data. DELETE is idempotent because after the first deletion the resource is gone,
and subsequent delete requests on the same resource result in the same state: resource does
not exist (even if the HTTP status code changes from 200 to 404). Idempotency is critically
important in distributed systems. Network failures might cause a client to retry a request without
knowing if the original got through. If the operation is idempotent, retrying is safe. If it's not — like
POST creating a payment — you can implement idempotency keys: the client includes a unique
idempotency key in the header, the server stores the result of the first request keyed by that
value, and subsequent requests with the same key return the cached result without
re-executing.

Q: PUT vs POST vs PATCH.


Very likely
POST is used to create a new resource when the server determines the resource's URL.
Submitting an order, creating a user — the server assigns an ID and returns the new resource's
URL. POST is not idempotent. PUT is used to create or replace a resource at a specific URL.
The client provides the complete representation of the resource. If the resource exists, it's
replaced entirely. If it doesn't, it's created. PUT is idempotent. A common use case is updating a
user profile with the complete new state. PATCH is used for partial updates — you send only the
fields you want to change, not the entire resource. For example, changing just a user's email
without sending all other fields. PATCH is technically not guaranteed to be idempotent — it
depends on the semantics of the patch. A PATCH that says 'increment the count by 1' is not
idempotent. One that says 'set the count to 5' is. In practice, the choice depends on your use
case — if you always update the entire resource state, use PUT. If you frequently update
individual fields, PATCH is more efficient and less prone to accidental data loss from clients that
don't know all current field values.

Q: How do you secure REST APIs?


Very likely
Securing a REST API is a multi-layered concern. Authentication verifies who is calling — JWT
tokens or OAuth2 access tokens are the standard for stateless APIs. Every request must include
a valid token in the Authorization header, verified for signature and expiration on every request.
Authorization ensures authenticated callers can only access resources they have permission for
— implemented through role-based access control with Spring Security's @PreAuthorize or
security rules in the filter chain. HTTPS is non-negotiable — all traffic must be encrypted in
transit to prevent interception. Rate limiting prevents abuse — a client should not be able to
make unlimited requests. This is typically enforced at the API gateway layer using sliding
window or token bucket algorithms. Input validation prevents injection attacks — every incoming
payload should be validated against an expected schema before processing. Security headers
like Content-Security-Policy, X-Content-Type-Options, and X-Frame-Options defend against
various browser-based attacks. CORS configuration ensures only known origins can make
cross-origin requests. For sensitive operations, audit logging records who did what and when.
And regular penetration testing and vulnerability scanning should be part of the release process.

Q: What are the best practices for RESTful API design?


Very likely
Good REST API design starts with resource-oriented URLs — URIs should be nouns
representing resources, not verbs representing actions. /orders/123 is correct; /getOrder?id=123
is not. Use plural nouns for collections: /orders, /users. Represent relationships hierarchically:
/users/123/orders. Use HTTP methods semantically: GET for retrieval, POST for creation, PUT
for full update, PATCH for partial update, DELETE for removal. Return appropriate HTTP status
codes — 200 for success, 201 for created, 204 for no content, 400 for bad request, 401 for
unauthorized, 403 for forbidden, 404 for not found, 409 for conflict, 422 for validation errors, 500
for server errors. Version your API from the start — /api/v1/ — even if you think you won't need
it. Design error responses consistently — always return a JSON body with an error code,
message, and timestamp, never just an HTTP status code. Use pagination for collections —
never return unbounded lists. Cursor-based pagination is more efficient than offset-based for
large datasets. Filter, sort, and search through query parameters. Document with
OpenAPI/Swagger so consumers understand the contract. And always handle the happy path
and all error cases explicitly.

Q: What is HATEOAS?
Good to know
HATEOAS stands for Hypermedia As The Engine Of Application State and is the highest
maturity level of REST as defined by Richardson's Maturity Model. In a HATEOAS API,
responses include not just data but also links to related actions and resources. For example, a
GET /orders/123 response would include the order details plus links for canceling the order,
viewing the customer, or tracking shipment — only the actions that are valid given the current
state of the order. The client discovers what it can do next by following these links rather than
hard-coding action URLs. The benefit is that the client and server are more loosely coupled —
the client doesn't need to know the URL structure in advance, just the starting point. If the server
changes a URL, it updates the links in responses and clients that follow links continue to work. In
practice, HATEOAS is rarely fully implemented in real-world APIs because it adds complexity
and most API clients are built with knowledge of the API contract anyway. Spring HATEOAS
provides support for building HATEOAS responses in Spring Boot if you want to implement it.

Q: How do you handle API versioning?


Good to know
API versioning is a strategy to evolve your API without breaking existing clients. The four main
strategies each have different tradeoffs. URI versioning — /api/v1/users — is the simplest and
most visible. It's easy to route, test, and document. But it 'pollutes' the URI which is supposed to
identify a resource, not its version. Request parameter versioning — /api/users?version=2 —
keeps URIs cleaner but mixes versioning concerns into the query string. Header versioning uses
a custom header like API-Version: 2. It keeps URIs stable and is more RESTful, but it's invisible
in browser testing and requires clients to set custom headers. Content negotiation uses the
Accept header — Accept: application/[Link].v2+json. This is technically the most correct
REST approach since it negotiates representation format, but it's complex and rarely supported
well by tooling. Beyond the mechanism, the more important question is the deprecation policy.
You must give clients time to migrate. Best practices include documenting the deprecation in
response headers with a Deprecation and Sunset date, maintaining old versions for a committed
period — typically 6 to 12 months — and communicating changes through changelogs and
direct communication with API consumers.
12. Strings & Exceptions

Q: Why is String immutable? What is the String Constant Pool?


Almost always asked
String is immutable in Java for several important reasons. Safety is the first — Strings are used
everywhere for sensitive data like usernames, passwords, and network paths. If String were
mutable, a reference holder could change the String after passing it to a method, creating
security vulnerabilities. Thread safety is another reason — because the value never changes,
Strings are inherently thread-safe and can be shared across threads without synchronization.
The String Constant Pool, also called the String Intern Pool, is the mechanism enabled by
immutability. When you write String s = "hello", the JVM looks in the pool for an existing string
with the value 'hello'. If found, it returns a reference to that existing object. If not, it creates one
and adds it to the pool. This means two variables assigned the same literal share one object in
memory, saving space. When you use new String('hello'), you explicitly create a new object on
the heap, bypassing the pool — which is almost never what you want. The intern() method can
manually add a string to the pool. The practical implication for interviewing is understanding why
== compares references — two String literals might share a reference through the pool, but two
separately created String objects won't.

Q: String vs StringBuffer vs StringBuilder.


Very likely
String is immutable — every operation like concatenation or substring creates a new String
object. If you concatenate strings in a loop, you create a new object on every iteration, filling up
the heap with short-lived intermediate strings. The compiler does optimize simple concatenation
of literals, but not concatenation inside loops. StringBuffer is a mutable sequence of characters
with all its methods synchronized, making it thread-safe. But that synchronization has a cost —
it's slower in single-threaded contexts. StringBuilder is also a mutable sequence of characters
but without synchronization. It's faster than StringBuffer in single-threaded use. In practice, you
should use StringBuilder in any situation where you're building up a string piece by piece —
string concatenation in loops, constructing SQL queries, building messages. String is fine for
simple cases and immutable text. StringBuffer is rarely the right choice today — if you need a
thread-safe mutable string, you likely have a design problem, because sharing mutable state
across threads is generally something to avoid. The compiler actually converts simple string
concatenations to StringBuilder internally, but explicit use is clearer for complex cases.

Q: Checked vs unchecked exceptions — when to use each.


Very likely
Checked exceptions extend Exception but not RuntimeException. The compiler forces you to
either catch them or declare them in the method signature with throws. They're intended for
recoverable conditions that the caller might reasonably be expected to handle — like
FileNotFoundException when reading a file, or SQLException for database operations. The idea
is that if you're writing code that opens files, you're acknowledging the possibility that the file
might not exist, and you're forced to think about what to do in that case. Unchecked exceptions
extend RuntimeException. The compiler doesn't require you to handle them. They represent
programming errors — bugs — like NullPointerException, ArrayIndexOutOfBoundsException, or
IllegalArgumentException. The assumption is that proper code shouldn't encounter these, and if
it does, the right response is usually to fix the bug, not catch the exception. In modern practice,
many developers — and frameworks like Spring — prefer unchecked exceptions throughout.
The argument is that checked exceptions in the throws clause propagate through every layer of
the call stack, creating a coupling between layers and cluttering method signatures with
exceptions they can't meaningfully handle. Spring converts all SQL exceptions to unchecked
DataAccessExceptions for exactly this reason. My personal practice is to use checked
exceptions only at the boundary of external systems where recovery is genuinely possible, and
unchecked for internal error conditions.

Q: Why can't child methods throw broader checked exceptions?


Very likely
This is a Liskov Substitution Principle constraint enforced by the compiler. If a parent method
declares it throws IOException, code calling that method is written to handle IOException. If a
child class's overriding method could throw Exception — a broader checked exception — that
code would be inadequate. The caller wouldn't be prepared to handle the wider range of
exceptions. The rule is that an overriding method can throw fewer exceptions than the parent, or
narrower checked exceptions that are subtypes of the parent's declared exceptions, but not
broader ones. However, unchecked exceptions — RuntimeException and its subclasses — are
not subject to this restriction. An overriding method can throw any unchecked exception
regardless of what the parent declares. This is consistent with the principle that unchecked
exceptions represent programming errors that every method can potentially throw, so they're not
part of the method's contract in the same way checked exceptions are.

Q: What happens inside the JVM when a NullPointerException is thrown?


Good to know
When your code attempts to dereference a null reference — accessing a field, calling a method,
or indexing an array on null — the JVM detects this and creates a NullPointerException object.
Creating the exception object includes capturing the stack trace, which is an expensive
operation involving walking up the call stack and recording each frame. The JVM then begins
unwinding the call stack, looking for the nearest catch block that can handle the exception. Each
stack frame is examined — if the method has a try-catch that covers NullPointerException or a
parent type, execution jumps to that catch block. If no handler is found in the current method, the
JVM pops the current stack frame and continues searching up the call stack. If no handler is
found all the way up to the thread's top-level method, the thread's UncaughtExceptionHandler is
invoked, which by default prints the stack trace and terminates the thread. Java 14 introduced
'Helpful NullPointerExceptions' — the JVM now provides a detailed message in the exception
indicating exactly which variable was null, making debugging much easier than the historic
empty message.

Q: Does finally always execute?


Good to know
In the vast majority of cases, yes — finally executes regardless of whether an exception was
thrown or caught in the try block, and regardless of whether a return statement was executed
inside the try or catch. It even executes if a return statement is in the try block, with the finally
running before the actual return. However, there are scenarios where finally does not execute. If
the JVM itself crashes or is killed — for example, via [Link]() being called inside the try
block — the JVM terminates without running the finally. If the thread is killed or the process is
forcefully terminated by the OS, finally won't run. If the try block contains an infinite loop, finally
never gets a chance to run. And in theory, if the system loses power or the JVM encounters a
fatal error, finally won't execute. For practical purposes: finally reliably runs for all normal
application scenarios including exceptions and returns. [Link] is the most common real
scenario where it doesn't.
Java Backend Interview Answer
Guide
90 questions across 12 topics
OOP · Java 8 · Collections · Multithreading · JVM · Spring Boot · Spring Security · JWT ·
Microservices · Hibernate · Design Patterns · REST APIs

Curated for 3–4 years experience | 2025–2026

Interview tip: Always structure your answers using a three-part approach — what it is,
how it works internally, and when you'd use it in a real project. Interviewers at the 3–4 year
level want to see that you understand the 'why', not just the 'what'.

You might also like