Java Interview Questions for 5 Years Experience
These scenarios focus on tradeoffs, reliability, concurrency, and diagnosis. A useful answer states assumptions, compares options, and explains how evidence would change the decision. There is often more than one reasonable design.
1. A service creates a database row and sends a message. How do you handle partial success?
A local database transaction does not automatically include an external message system. Consider an outbox written in the same database transaction, followed by a separate publisher. Design retries and consumers for duplicates. State the consistency and delivery guarantees rather than claiming exactly-once behavior without an end-to-end argument.
2. What would you inspect before increasing a thread pool?
Measure task latency, queue growth, CPU use, blocking time, and downstream limits. More workers can increase contention or overwhelm a database. Decide how to bound waiting work, reject or slow new requests, and shut down cleanly.
3. How would you respond to a growing heap in production?
First distinguish temporary allocation pressure from retained growth. Compare measurements over time and inspect which reference paths retain objects. Check caches, listeners, queued tasks, and long-lived request data. Validate the diagnosis before changing heap size or collector flags.
4. Why is a benchmark loop in main often misleading?
JIT compilation, warmup, dead-code elimination, input choice, and garbage collection can distort the result. Use a suitable harness such as JMH for microbenchmarks. Also measure the application workload, because a faster isolated operation may not improve the user-visible path.
5. How would you prevent a lock-order deadlock?
Give shared locks a stable acquisition order and apply it consistently across code paths. Review callbacks and external calls made while holding a lock. Consider timed locks or smaller critical sections where appropriate, but explain the failure and recovery behavior too.
6. How do you design retry behavior for a payment-like command?
Decide which failures are retryable and how the command is identified. Use an idempotency key and stored outcome where the business rules require it. Apply bounded retries, backoff, and observability. A retry must not silently repeat a business action.
7. How would you evolve a public Java API?
Identify callers and compatibility requirements. Prefer additive changes where practical and plan deprecation rather than immediate removal. Distinguish source compatibility, binary compatibility, and behavior compatibility. Test old clients as well as newly compiled ones.
8. What does safe publication mean?
Other threads must obtain an object through a mechanism with the required visibility guarantees. Examples include class initialization, suitable volatile access, or locking. Merely finishing construction in one thread does not make every unsynchronized handoff correct. Avoid letting this escape during construction.
9. How would you review a singleton?
Ask why global shared state is needed. Check initialization, thread safety, class-loader boundaries, test isolation, and lifecycle. A dependency managed by an application container may be easier to replace and test. A singleton pattern does not guarantee one instance across separate JVM processes.
10. What is your approach to database connection pool tuning?
Measure connection wait time, active usage, transaction duration, and database capacity. A large pool can overload the database instead of increasing throughput. Confirm that connections are returned promptly and that slow transactions are investigated.
11. How would you use reflection in a library responsibly?
Prefer a public contract where possible. If reflection is necessary, define supported access paths and handle module restrictions and failures explicitly. Do not assume setAccessible(true) always succeeds. Cache metadata only with attention to class-loader retention.
12. Why might a parallel stream make a task slower?
Splitting work, combining results, contention, and blocking can cost more than the work saved. Shared mutable state can also make the result incorrect. Measure realistic inputs and choose an execution model that fits the task rather than enabling parallelism automatically.
13. How do you reason about serialization boundaries?
Specify the data format, versioning rules, trust boundary, and size limits. Java native deserialization of untrusted data is not a general-purpose integration choice. Prefer a deliberately designed interchange format when systems need independent evolution.
14. What belongs in a performance incident write-up?
Record the impact, timing, workload, measurements, cause, mitigation, and validation. Separate observations from hypotheses. Include follow-up changes that prevent recurrence or make detection earlier. Avoid attributing the incident to a person instead of the technical and process conditions.
15. How would you choose between adding a design pattern and keeping direct code?
Name the concrete variation or change pressure first. Use the smallest design that handles it clearly. A pattern is useful when it makes those changes easier to contain, not when it merely adds familiar class names. Explain the tradeoff and show how the design can be tested.
More Scenario-Based Questions
For each situation, explain what you would check first, how you would fix the problem, and how you would confirm the fix.
16. Two requests try to book the last available seat. How would you prevent double booking?
Scenario: A bus has one seat left. Two users click Book at almost the same time. Both requests read the available seat count as one.
Answer: Reading the count and updating it later can allow both requests to succeed. Use a database operation that reduces the count only when it is greater than zero. Check the number of updated rows: one means the seat was reserved, and zero means no seat was available. Save the booking in the same transaction so a failed booking does not permanently reduce the count. A Java synchronized block alone cannot protect bookings handled by separate application instances.
Follow-up: How would you test this with two requests running together?
Working Java demonstration: This small program protects one seat inside one JVM. The database approach described above is still required when several application instances share the booking data.
class SeatBooking {
private int available = 1;
synchronized boolean book() {
if (available == 0) {
return false;
}
available--;
return true;
}
}
public class HelloWorld {
public static void main(String[] args) throws InterruptedException {
SeatBooking booking = new SeatBooking();
java.util.concurrent.atomic.AtomicInteger successful =
new java.util.concurrent.atomic.AtomicInteger();
Runnable request = () -> {
if (booking.book()) {
successful.incrementAndGet();
}
};
Thread first = new Thread(request);
Thread second = new Thread(request);
first.start();
second.start();
first.join();
second.join();
System.out.println("Successful bookings: " + successful.get());
}
}Output:
Successful bookings: 117. A HashMap cannot find a key that was added earlier. What would you check?
Scenario: An employee object is used as a map key. After its department is changed, looking up the same object returns null.
Answer: Check whether the department is used by equals() or hashCode(). Changing a field used in these methods can change where the key should be found, while the stored entry remains in its earlier location. Prefer an immutable employee ID as the key. If a custom key class is needed, keep the fields used for equality unchanged and implement equals() and hashCode() consistently.
Follow-up: Why does using the same object reference not solve this problem?
Working example: This version uses an immutable employee ID as the key. Changing the employee's department does not change that key.
import java.util.HashMap;
import java.util.Map;
class Employee {
private String department;
Employee(String department) {
this.department = department;
}
void changeDepartment(String department) {
this.department = department;
}
String getDepartment() {
return department;
}
}
public class HelloWorld {
public static void main(String[] args) {
Map<Integer, Employee> employees = new HashMap<>();
Employee employee = new Employee("Support");
employees.put(101, employee);
employee.changeDepartment("Engineering");
System.out.println(employees.get(101).getDepartment());
}
}Output:
Engineering18. A request counter reports fewer requests than expected. Why?
Scenario: Several worker threads run requestCount++. After processing 10,000 requests, the count is sometimes smaller than 10,000.
Answer: Incrementing a number includes reading its value, adding one, and writing the result. Two threads can read the same value and overwrite each other's update. Making the field volatile does not make this complete operation atomic. Use AtomicInteger.incrementAndGet() for an atomic integer increment, or protect all updates with the same lock. Check the final count after all workers have finished.
Follow-up: Would a counter in one JVM show the total across three application instances?
Working example: Two threads each perform 5,000 atomic increments. The main thread waits for both before reading the total.
import java.util.concurrent.atomic.AtomicInteger;
public class HelloWorld {
public static void main(String[] args) throws InterruptedException {
AtomicInteger requestCount = new AtomicInteger();
Runnable countRequests = () -> {
for (int i = 0; i < 5000; i++) {
requestCount.incrementAndGet();
}
};
Thread first = new Thread(countRequests);
Thread second = new Thread(countRequests);
first.start();
second.start();
first.join();
second.join();
System.out.println("Requests: " + requestCount.get());
}
}Output:
Requests: 1000019. A background task shows the previous user's request ID in its logs. What could cause it?
Scenario: An application stores the request ID in a ThreadLocal. A later task on the same worker thread prints an old request ID.
Answer: Pool threads are reused, so a thread-local value can remain available to the next task if it is not removed. Set the required value at the start of the task and call remove() in a finally block. When work moves to another thread, pass the required context deliberately rather than assuming it moves automatically. Test both a successful task and a task that throws an exception.
Follow-up: Why is clearing the value only on the successful path insufficient?
Working example: A single-thread executor reuses its worker. The second task checks that the first task removed its request ID.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HelloWorld {
private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
executor.submit(() -> {
REQUEST_ID.set("request-101");
try {
System.out.println("First task: " + REQUEST_ID.get());
} finally {
REQUEST_ID.remove();
}
}).get();
executor.submit(() ->
System.out.println("Second task: " + REQUEST_ID.get())
).get();
} finally {
executor.shutdown();
}
}
}Output:
First task: request-101
Second task: null20. Two users edit the same customer record. How would you avoid silently losing an update?
Scenario: Two users open version 4 of a customer record. One changes the phone number, and the other later saves an address from the older screen.
Answer: Store a version number with the record. Update it only when the saved version still matches the version the user read, and increase the version as part of that update. If no row is updated, another change has already happened. Ask the user to reload or apply a clear merge rule. Do not silently retry an old form if doing so would overwrite the newer data.
Follow-up: When would you choose database locking instead of a version check?
Working Java demonstration: This in-memory record shows a version check. For database storage, perform the version comparison and update together in the SQL statement.
class CustomerRecord {
private String phone = "1111111111";
private int version = 4;
synchronized boolean updatePhone(int expectedVersion, String newPhone) {
if (version != expectedVersion) {
return false;
}
phone = newPhone;
version++;
return true;
}
synchronized String getPhone() {
return phone;
}
}
public class HelloWorld {
public static void main(String[] args) {
CustomerRecord customer = new CustomerRecord();
System.out.println("First update: "
+ customer.updatePhone(4, "2222222222"));
System.out.println("Older update: "
+ customer.updatePhone(4, "3333333333"));
System.out.println("Current phone: " + customer.getPhone());
}
}Output:
First update: true
Older update: false
Current phone: 222222222221. A CSV import runs out of heap memory. How would you improve it?
Scenario: A nightly import works with a small file but fails when the file contains several million records. The code loads every row into a list before processing.
Answer: Read and process records gradually instead of keeping the whole file in memory. Use a CSV parser that supports quoted values and records that span multiple lines. Write database changes in bounded batches, close resources, and avoid collecting all successful rows just for a final report. Record failed rows in a controlled way and define whether a restart should resume or repeat completed work.
Follow-up: What would you measure to confirm that memory use stays bounded as the file grows?
Working Java demonstration: This example creates a small practice CSV file and counts its records one at a time. Its deliberately simple format has no quoted fields or embedded newlines. Use a CSV library for the more general format described above.
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
public class HelloWorld {
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("student-import-", ".csv");
try {
Files.write(file, Arrays.asList(
"id,name", "1,Meera", "2,Kabir", "3,Tara"),
StandardCharsets.UTF_8);
int count = 0;
try (BufferedReader reader = Files.newBufferedReader(
file, StandardCharsets.UTF_8)) {
reader.readLine(); // Skip the header in this known file format.
String line;
while ((line = reader.readLine()) != null) {
if (!line.trim().isEmpty()) {
count++;
}
}
}
System.out.println("Records processed: " + count);
} finally {
Files.deleteIfExists(file);
}
}
}Output:
Records processed: 3Only the tiny test file is created from a list. The reading loop does not collect the file's records into memory.
22. A request waits for three services, and one becomes slow. What would you change?
Scenario: An order summary calls customer, delivery, and rewards services. The rewards service sometimes takes 20 seconds, delaying the whole response.
Answer: Decide which results are required and which are optional. Set timeouts for the outbound calls and keep them within the overall request time budget. If rewards information is optional, return a clearly defined fallback when it is unavailable. Keep the number of waiting tasks bounded. Timing out while waiting for a result does not necessarily stop the underlying work, so check how the client and task handle cancellation.
Follow-up: How would you prevent retries from adding even more work during an outage?
Working Java demonstration: A latch represents a rewards service that has not responded. The caller waits for a limited time, then uses a fallback and requests cancellation. A real HTTP client also needs its own connection and response timeouts.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class HelloWorld {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
CountDownLatch serviceResponse = new CountDownLatch(1);
Future<String> rewards = executor.submit(() -> {
serviceResponse.await();
return "100 reward points";
});
try {
System.out.println(rewards.get(100, TimeUnit.MILLISECONDS));
} catch (TimeoutException exception) {
rewards.cancel(true);
System.out.println("Rewards temporarily unavailable");
} finally {
executor.shutdownNow();
}
}
}Output:
Rewards temporarily unavailable23. A ConcurrentHashMap stores lists, but concurrent updates still fail. Why?
Scenario: Several threads add order IDs to an ArrayList stored as a value in a ConcurrentHashMap.
Answer: The map protects its own operations. It does not automatically make the objects stored inside it thread-safe. Choose a suitable concurrent collection for the values, or protect every read and write to each list with an agreed locking approach. Another option is to replace an immutable value through a map computation. Choose based on whether the application needs individual updates or a consistent snapshot of several values.
Follow-up: Why does creating the list with computeIfAbsent() not make later ArrayList.add() calls thread-safe?
Working example: Both the map and its queue values support concurrent operations. Each worker adds a different order ID.
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public class HelloWorld {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, ConcurrentLinkedQueue<String>> orders =
new ConcurrentHashMap<>();
Thread first = new Thread(() -> orders
.computeIfAbsent("customer-1", key -> new ConcurrentLinkedQueue<>())
.add("order-101"));
Thread second = new Thread(() -> orders
.computeIfAbsent("customer-1", key -> new ConcurrentLinkedQueue<>())
.add("order-102"));
first.start();
second.start();
first.join();
second.join();
System.out.println("Orders saved: " + orders.get("customer-1").size());
}
}Output:
Orders saved: 2The insertion order can vary between runs. The total is checked after both workers finish.
Practice an Answer
Choose one question and explain it with a situation you have actually worked on. Separate the symptom, cause, change, and result. If you have not used a feature in production, say so and explain the small test you would build to explore it.
See you in next page....🙂