40 questions with answers for candidates with 2 years of experience — 6 easy, 31 medium, 3 hard.
What interviewers expect: 2 years experience
At two years, you should explain your module end to end and handle deeper questions on JPA, transactions, the N+1 problem and basic multithreading. Interviewers look for ownership of features, clean code and the ability to debug production issues using logs. Medium DSA problems and some low level design, such as designing a parking lot, are common.
This set is weighted towards Java 8+ Features and Streams (6), Spring Boot and Microservices (6), Strings and Exception Handling (6), OOP in Java (5).
JPA, transactions and query tuning Multithreading basics Low level design Production debugging
Solved at this levelStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
✓ 3 questions free • Unlock the rest in seconds
01
What is a ClassLoader and what is the delegation model?
A ClassLoader loads .class files into the JVM. The main loaders are Bootstrap (core classes), Platform (formerly Extension) and Application (classpath). In the parent delegation model a loader first asks its parent to load a class and only loads it itself if the parent cannot. This prevents user code from replacing core classes like java.lang.String.
Single Responsibility: a class has one reason to change. Open/Closed: open for extension, closed for modification. Liskov Substitution: subclasses must work wherever the parent is expected. Interface Segregation: prefer small, focused interfaces. Dependency Inversion: depend on abstractions, not concrete classes. Spring dependency injection is a practical example of dependency inversion.
03
Can you override a static or private method in Java?
No. Private methods are not visible to subclasses, so a method with the same name in a child is a new method. Static methods belong to the class; a static method with the same signature in a child hides the parent one, and which one runs depends on the reference type, not the object. The @Override annotation on such methods gives a compile error.
04
Sort a list of employees by department, then by salary descending.
Chain comparators.
employees.sort(
Comparator.comparing(Employee::department)
.thenComparing(Employee::salary,
Comparator.reverseOrder()));
// Streams version returning a new list
List<Employee> sorted = employees.stream()
.sorted(Comparator.comparing(Employee::department)
.thenComparing(Employee::salary, Comparator.reverseOrder()))
.toList();
List.sort is stable, so equal keys keep their order.
A lambda can use a local variable from the enclosing method only if that variable is final or effectively final, meaning it is never reassigned after initialisation. Lambdas capture the value, not the variable, so this avoids confusing behaviour and races. To accumulate results, use a stream collector or an AtomicInteger instead of changing a captured local.
A deadlock happens when two or more threads each hold a lock and wait for a lock held by the other, so none can continue. It needs mutual exclusion, hold and wait, no preemption and circular wait. Prevent it by always acquiring locks in the same global order, using tryLock with a timeout, holding locks briefly and avoiding calls to unknown code while holding a lock.
07
Write a minimal Dockerfile for a Spring Boot application.
A multi-stage build keeps the image small.
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw -q package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]
Spring Boot can also build images without a Dockerfile using mvn spring-boot:build-image.
08
A stream pipeline that updates a shared ArrayList in forEach gives random results with parallel(). Why?
ArrayList is not thread safe, and parallel streams run forEach on many threads, so additions race, get lost or throw exceptions. Side effects in streams are the problem. Replace forEach with collect(Collectors.toList()) or toList(), which handles merging per thread safely. Keep lambdas stateless and avoid modifying external state inside stream operations.
Exception chaining wraps a low level exception in a higher level one while keeping the original as the cause, for example throw new OrderException("save failed", sqlEx). The full stack trace shows both, with Caused by lines. It lets each layer speak its own language without losing the root cause, which is essential when debugging production logs.
10
What is garbage collection and how does the JVM decide an object is garbage?
Garbage collection automatically frees memory used by objects that are no longer reachable. The JVM starts from GC roots such as local variables on thread stacks, static fields and active threads, and marks every object reachable from them. Anything not marked is garbage. Because reachability is used rather than reference counting, circular references between unreachable objects are still collected.
11
What is the difference between HashMap and ConcurrentHashMap?
HashMap is not thread safe; concurrent writes can lose data or corrupt the structure. ConcurrentHashMap supports full concurrency: reads are lock free and writes use CAS or lock a single bin, so threads rarely block each other. It does not allow null keys or values. It also provides atomic compound operations such as computeIfAbsent and merge, which avoid check-then-act races.
12
A downstream payment service is slow and your API threads pile up. How do you protect your service?
Set strict connect and read timeouts on the HTTP client. Add a Resilience4j circuit breaker so calls fail fast once the error rate crosses a threshold, a bulkhead to limit concurrent calls and retry with backoff only for safe idempotent requests. Return a clear fallback such as payment pending. Monitor breaker state through Actuator metrics and alerts.
13
Users get LazyInitializationException when the API returns an entity. What is happening?
The entity was loaded inside a transaction, the session closed, and then Jackson touched a lazy association outside it. Do not fix it by turning on open-session-in-view or making everything eager. Instead, map the entity to a DTO inside the service transaction and fetch needed associations explicitly with JOIN FETCH or an entity graph. Spring Boot logs a warning because open-in-view is on by default.
Access order plus removeEldestEntry gives LRU in a few lines.
class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
LruCache(int capacity) {
super(16, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> e) {
return size() > capacity;
}
}
get and put are O(1).
Strings are immutable so they can be safely shared from the string pool, used as HashMap keys with a cached hash code, passed between threads without locking and trusted in security checks such as file paths and class names. Any change, like concat or replace, creates a new String. For heavy modification use StringBuilder, which changes its internal buffer in place.
16
Your application hangs and CPU is near zero. How do you find out why?
Low CPU with a hang suggests threads are blocked, often a deadlock or waiting on a slow resource. Take several thread dumps with jcmd pid Thread.print. The JVM reports detected deadlocks at the end of the dump. Otherwise look for many threads BLOCKED on the same lock or WAITING on a connection pool or socket read, then fix lock order, add timeouts or increase the pool.
17
Find the first non-repeating character in a string.
Count in one pass, then scan again in order.
public static char firstUnique(String s) {
int[] cnt = new int[128];
for (char c : s.toCharArray()) cnt[c]++;
for (char c : s.toCharArray()) {
if (cnt[c] == 1) return c;
}
return '_';
}
Two passes, O(n) time and O(1) extra space for ASCII input.
18
An API listing 100 orders fires 101 SQL queries. How do you fix it?
This is the N+1 problem from a lazy association loaded in a loop or by JSON serialisation. Fetch what the endpoint needs in one query with JOIN FETCH or @EntityGraph(attributePaths = "items"), or use a DTO projection query. For collections with pagination, fetch ids first, then fetch associations in a second query to avoid in-memory paging. Add a test that asserts the query count.
19
A getter returns an internal List and callers modify it, causing bugs. How do you fix this?
This breaks encapsulation. Return an unmodifiable view with Collections.unmodifiableList(list) or a copy with List.copyOf(list), so callers cannot change internal state. Also copy the list passed into the constructor or setter. If the class should be fully immutable, store List.copyOf in a final field. Expose specific methods like addItem() when controlled changes are needed.
20
Using streams, find the employee with the highest salary in each department.
Group by department with a maxBy downstream collector.
Map<String, Optional<Employee>> top = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.maxBy(Comparator.comparingInt(Employee::salary))));
To get values without Optional, wrap with collectingAndThen(maxBy(...), Optional::get), which is safe because each group has at least one element.
singleton is the default: one instance per application context. prototype creates a new instance each time it is requested. Web scopes include request, session and application. Singletons must be stateless or thread safe because all requests share them. Injecting a prototype into a singleton gives only one instance, so use ObjectProvider or a scoped proxy to get fresh ones.
22
Describe a situation where you had to deliver under a tight deadline.
Situation: a bulk upload feature had to go live in four days for a client launch. Task: build it safely in time. Action: I split it into must-have and later items, used an ExecutorService to process rows in parallel, wrote focused tests for validation and kept the product owner updated daily. Result: we shipped on time, it handled 50,000 rows in under two minutes and we added the extras next sprint.
23
Clients report duplicate orders when they retry after a timeout. How do you prevent it?
Make the create order API idempotent. The client sends an Idempotency-Key header; the server stores the key with the result in a table with a unique constraint or in Redis. A retry with the same key returns the saved response instead of creating a new order. Database unique constraints on business keys add another safety net.
24
Tell me about a time you made a codebase more readable.
Situation: our reporting module had nested loops 200 lines long that new joiners feared. Task: make it maintainable without changing output. Action: I added snapshot tests on real report files, then refactored into small methods using streams, groupingBy and records for intermediate data, and reviewed each step with a teammate. Result: the module shrank by 40 percent and later changes took a day instead of a week.
25
Write a simple REST controller in Spring Boot for creating and fetching products.
The switch needs no default because the type set is closed.
sealed interface Shape permits Circle, Rect {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Rect(double w, double h) -> w * h;
};
}
27
Flatten a list of lists of integers and compute the sum of squares of the even numbers.
flatMap then use a primitive stream to avoid boxing.
int result = lists.stream()
.flatMap(List::stream)
.filter(n -> n % 2 == 0)
.mapToInt(n -> n * n)
.sum();
mapToInt returns an IntStream, whose sum() is simpler and faster than reduce on boxed Integers.
28
You need a cache that evicts the least recently used item when full. How would you build it in Java?
Extend LinkedHashMap with accessOrder set to true and override removeEldestEntry to return true when size exceeds capacity. Each get moves the entry to the end, so the eldest is the least recently used. It is not thread safe, so wrap it with synchronization for shared use. In production prefer Caffeine, which offers size and time based eviction with high concurrency.
29
You must expose configuration that differs between dev, QA and production. How do you manage it?
Use Spring profiles with application-dev.yml and application-prod.yml, and bind settings to typed @ConfigurationProperties classes with validation. Keep secrets out of the repo by injecting them through environment variables, Kubernetes Secrets or a vault. Activate the profile with SPRING_PROFILES_ACTIVE in each environment. Use the same build artefact everywhere and change only configuration.
30
Write a JDBC method to fetch a user by email safely.
Situation: the customer search page took 6 seconds at peak. Task: bring it under 500 ms. Action: I captured the SQL, ran EXPLAIN ANALYZE and found a full scan due to LOWER(email) on an unindexed column and an N+1 on addresses. I added a functional index, fetched addresses with an entity graph and added pagination. Result: response time dropped to 180 ms and database CPU fell by a third.
32
You need to add a new payment method to a system that has a big if-else on payment type. How would you redesign it?
This breaks the Open/Closed principle. I would create a PaymentProcessor interface with a pay() method and one implementation per type, such as UpiProcessor and CardProcessor. A factory or a Spring map of beans keyed by type picks the right one. Adding a new method then means adding one new class without touching existing tested code, and each processor can be unit tested alone.
33
Map a one-to-many relation between Order and OrderItem in JPA.
Make the child own the foreign key and keep both sides in sync.
@Entity
class Order {
@Id @GeneratedValue Long id;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
List<OrderItem> items = new ArrayList<>();
void addItem(OrderItem i) { items.add(i); i.setOrder(this); }
}
@Entity
class OrderItem {
@Id @GeneratedValue Long id;
@ManyToOne(fetch = FetchType.LAZY) Order order;
}
The simplest safe options are an enum or the holder idiom.
public class Config {
private Config() {}
private static class Holder {
static final Config INSTANCE = new Config();
}
public static Config getInstance() {
return Holder.INSTANCE;
}
}
The JVM loads Holder lazily and class initialisation is thread safe. An enum singleton also resists reflection and serialization attacks.
Fail-fast iterators, used by ArrayList and HashMap, throw ConcurrentModificationException if the collection is structurally modified during iteration other than through the iterator itself; they check a modCount. Fail-safe or weakly consistent iterators, as in ConcurrentHashMap and CopyOnWriteArrayList, work on a snapshot or tolerate changes and never throw, but may not show the latest updates.
36
A file handle leak causes Too many open files errors after a day. What is the likely cause?
Streams, readers or connections are opened but not closed on all paths, especially when an exception is thrown before close(). Check with lsof or jcmd which files are open. Fix by wrapping every resource in try-with-resources so it always closes, and make sure Files.lines() and Files.list() streams are closed too, since they hold file handles.
37
What is the difference between synchronized and ReentrantLock?
synchronized is a built-in keyword that locks a monitor and releases it automatically at block exit. ReentrantLock is an explicit lock that adds tryLock with timeout, interruptible locking, fairness options and multiple Condition objects, but you must unlock in a finally block. Both are reentrant. Prefer synchronized for simple cases and ReentrantLock when you need its extra features.
38
Write code that runs 10,000 blocking tasks using virtual threads.
Use the virtual-thread-per-task executor in Java 21.
try (var ex = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int id = i;
ex.submit(() -> {
Thread.sleep(Duration.ofMillis(200));
return id;
});
}
} // close() waits for all tasks
All tasks finish in about 200 ms because blocked virtual threads free their carriers.
String is immutable and thread safe by design. StringBuilder is mutable and not synchronized, so it is the fastest choice for building strings in a single thread, such as in loops. StringBuffer is mutable and synchronized, so it is thread safe but slower, and is rarely needed today. The compiler already optimises simple + concatenation outside loops.
40
Code builds a large CSV report with String += inside a loop and it is very slow. Why and what is the fix?
Each += creates a new String and copies all previous characters, so building n rows is O(n squared) in time and creates huge garbage. Use a single StringBuilder with append() inside the loop, ideally with an initial capacity, or write rows directly to a BufferedWriter for very large output. This turns minutes of work into milliseconds.
Frequently asked questions
What is asked in Java interviews for 2 years of experience?
At two years, you should explain your module end to end and handle deeper questions on JPA, transactions, the N+1 problem and basic multithreading. Interviewers look for ownership of features, clean code and the ability to debug production issues using logs. Medium DSA problems and some low level design, such as designing a parking lot, are common.
How many questions should I practise before a Java interview?
Work through all 40 questions here, then the company-wise sets for the employers you are targeting. Quality matters more than count: be able to explain why each answer is right.
Which topics matter most for 2 years of experience?
Focus first on JPA, transactions and query tuning, Multithreading basics, Low level design, Production debugging.