Java interview questions for 3 years experience: 15 answers that pass
At three years, interviewers test depth, not syntax. Here are the 15 Java questions you'll face on JVM, GC, threads, Spring and microservices, with model answers.
At three years of experience, nobody asks you to write a for loop. They ask why your service paused for two seconds in production, and most candidates go quiet. Say you can build a REST API in an afternoon, but that question makes you freeze. You know the code works. You just cannot explain what happens underneath it.
A 3-year Java interviewer tests depth, not syntax. Expect 15 questions across six areas: JVM memory, equality and immutability, garbage collection, concurrency, Spring internals and microservices. Each answer needs a mechanism and a trade-off, not a definition. This post adds a red-flag answer and runnable code to each topic, and you can check the topic mix against the InterviewBit Java question bank. Memorised textbook lines are the main reason candidates fail this round.
If you are still building the basics, start with my list of Java developer skills for freshers and come back here after.
JVM memory questions: bytecode, the heap with the stack, and the JIT compiler
Interviewers start with the JVM because it shows whether you know what runs your code, not just how to write it, and the InterviewBit Java question bank opens with the same topics. The three questions below come up in almost every round at this level.
How does Java achieve platform independence?
Model answer: The compiler turns source code into bytecode, not machine code. Bytecode runs on any machine that has a JRE, so the same .class file works on Windows and on Linux without recompiling.
Likely follow-up: "Is the JVM itself platform independent?" No. Each operating system needs its own JVM build. The bytecode is portable; the machine that reads it is not.
What lives on the heap and what lives on the stack?
Model answer: Each thread gets its own stack. It holds method frames, local primitives and references to objects. The objects themselves live on the heap, which all threads share.
This explains the two errors. A StackOverflowError usually means recursion with no exit, so frames pile up until the stack is full. An OutOfMemoryError means the heap is full of objects that are still reachable, often from a cache or a static list that only grows.
Likely follow-up: "How would you find that leak?" Take a heap dump and look for the object type with the most retained memory.
What does the JIT compiler do?
Model answer: The JIT compiler is part of the JVM. It watches which bytecode runs often (the "hot" code) and compiles it to native machine code, so later calls skip interpretation.
Likely follow-up: "Why are the first requests after a deploy slower?" Because the JIT has not compiled the hot paths yet. Teams often send warm-up traffic before a new instance takes real load.
Equality and immutability: equals paired with hashCode, and the final keyword
These two questions look basic, but a weak answer here tells the interviewer you have hit real bugs without understanding them. Both are listed in the InterviewBit set, and both lead into longer discussions.
What is the difference between == and equals(), and why does hashCode matter?
Model answer: == compares references, so it asks "is this the same object?" equals() compares contents. The contract says that if two objects are equal, they must return the same hashCode().
A HashMap uses the hash code to pick a bucket first. It only calls equals() inside that bucket. Override equals() alone and two "equal" keys land in different buckets:
class UserKey {
private final String email;
UserKey(String email) { this.email = email; }
@Override
public boolean equals(Object o) {
if (!(o instanceof UserKey)) return false;
return email.equals(((UserKey) o).email);
}
// hashCode() is NOT overridden
}
Map<UserKey, String> roles = new HashMap<>();
roles.put(new UserKey("a@x.com"), "admin");
roles.get(new UserKey("a@x.com")); // usually returns null
Red flag answer: "I always generate both in the IDE." That is good practice, but it does not explain the bucket lookup. The interviewer wants the reason.
What does final do, and how do you build an immutable class?
Model answer: On a variable, final stops reassignment. On a method, it stops overriding. On a class, it stops anyone extending it. To build an immutable class, mark the class final, make every field private final, skip setters, and copy any mutable input (like a List) in the constructor and getters.
String is the classic example, which is why the String vs StringBuilder vs StringBuffer question often follows. If final gets mixed up with static in your head, read static vs non-static in Java first.
Red flag answer: "A final variable makes the object immutable." It does not. A final List can still have items added. Only the reference is fixed.
Garbage collection: G1 vs CMS vs Parallel compared
One caution first: CMS was removed in JDK 14, so the table below compares G1 with Parallel and ZGC, and keeps CMS only as a reminder. G1 is the default collector on Java 17 and 21. The collector you name matters less than whether you can explain compaction and pause-time trade-offs in plain words. Oracle's G1 tuning guide gives you that comparison, though it describes Java 8. Compaction means moving live objects together so free memory sits in one block instead of scattered gaps. Interviewers also ask about ZGC and Shenandoah, which aim for even shorter pauses. Know G1 first.
| Collector | Compaction | Pause-time behaviour | Heap layout |
|---|---|---|---|
| G1 | Compacts | Meets a pause target with high probability, not as a guarantee | Equally-sized regions |
| CMS (removed in JDK 14) | Does not compact | Short pauses, but fragmentation can force long ones | Contiguous young and old generations |
| ZGC | Compacts while the application runs | Aims for very short pauses, even on large heaps | Heap split into regions |
| Parallel | Whole-heap compaction only | Compacting the whole heap causes long stop-the-world pauses | Contiguous young and old generations |
How does G1 differ from CMS and Parallel?
Model answer: G1 is a server-style collector built for multiprocessor machines with large heaps. It splits the heap into equal regions, so it can clean the regions with the most garbage first. Unlike CMS, it compacts, which avoids fragmentation. Unlike Parallel, it does not have to compact the whole heap at once. Oracle calls G1 the long-term replacement for CMS.
Red flag answer: "G1 is newer, so it is faster." Speed is the wrong word. G1 trades some throughput for shorter, more predictable pauses.
How do you tune a JVM for pause time?
Model answer: Per the Oracle G1 guide, MaxGCPauseMillis defaults to 200 ms and InitiatingHeapOccupancyPercent defaults to 45. The second flag is the heap occupancy at which G1 starts a concurrent marking cycle. G1 is not real-time, so the pause goal is a target it usually hits. The same guide warns against fixing the young generation size with -Xmn, because that overrides the pause goal. This is also how you answer the two-second pause question. Turn on GC logging with -Xlog:gc and look for a line like this (an illustrative example, your numbers will differ): [12.345s][info][gc] GC(87) Pause Young (Normal) (G1 Evacuation Pause) 2048M->1900M(4096M) 2013.456ms. Read the last number first: 2013 ms is the pause, ten times the 200 ms goal. The heap figures show almost nothing was freed, since 2048M only fell to 1900M, so a large set of objects is still reachable. Your first action is to take a heap dump and look for a cache or static list that keeps growing. Your second is to check that -Xmn is not fixed, and only then adjust the pause goal.
Red flag answer: Setting a very small pause goal and calling it a guarantee. A smaller goal makes G1 collect more often, which can lower throughput.
Concurrency questions: synchronization with deadlock, and why thread pools matter
Concurrency questions separate people who have read about threads from people who have debugged one, and the Oracle concurrency tutorial covers every topic in this section. Its main headings are synchronization, deadlock and liveness.
What is the difference between synchronized and volatile?
Model answer: synchronized gives two guarantees: only one thread runs the block at a time, and changes are visible to the next thread that takes the lock. volatile gives visibility only. A volatile int count still breaks under count++, because read, add and write are three separate steps.
Red flag answer: "volatile makes a variable thread-safe." It makes reads fresh. It does not make updates atomic. Use AtomicInteger for counters.
How does a deadlock happen, and how do you prevent it?
Model answer: Thread A holds lock X and waits for lock Y. Thread B holds Y and waits for X. Neither can move. You spot it with a thread dump (jstack prints "Found one Java-level deadlock"). You prevent it by always taking locks in the same order, or by using tryLock with a timeout so a thread backs off.
A trimmed thread dump shows the problem like this:
Found one Java-level deadlock:
"Thread-A": waiting to lock monitor 0x01 (object lockY),
which is held by "Thread-B"
"Thread-B": waiting to lock monitor 0x02 (object lockX),
which is held by "Thread-A"
Mention that deadlock is one liveness problem among others. Starvation (a thread never gets the lock) and livelock (threads keep reacting to each other and never finish) are the two the tutorial also names.
Red flag answer: "Just add more synchronized." More locks usually create more chances for deadlock.
Why use an ExecutorService instead of new Thread()?
Model answer: Since version 5.0, Java has shipped high-level APIs in java.util.concurrent, including executors and thread pools, as the Oracle tutorial explains. A pool reuses threads instead of creating one per task, caps how many run at once, and returns a Future you can wait on with a timeout.
ExecutorService pool = Executors.newFixedThreadPool(poolSize);
try {
Future<Report> future = pool.submit(() -> buildReport(userId));
Report report = future.get(timeoutSeconds, TimeUnit.SECONDS);
} finally {
pool.shutdown();
}
Red flag answer: Forgetting shutdown(). The pool's threads keep the JVM alive, and the interviewer will notice.
Spring questions: bean lifecycle, AOP and @Transactional
Spring questions test whether you know what the framework does behind your annotations, and the Spring AOP reference explains the proxy model that ties all three together.
What is the Spring bean lifecycle?
Model answer: Walk through it in order.
Instantiate
The container calls the constructor and creates the object.
Inject dependencies
Spring fills fields, setters or constructor arguments with other beans.
Run init callbacks
@PostConstruct, thenafterPropertiesSet(), then any custom init method.Apply BeanPostProcessors
These run before and after the init callbacks, and one of them can wrap the bean in a proxy.
Ready for use
The bean (or its proxy) is handed to anyone who asks for it.
Destroy
On shutdown,
@PreDestroyruns for singleton beans.
What is AOP, and where does Spring use it?
Model answer: The key unit in AOP is the aspect. An aspect pulls one crosscutting concern, like transactions or logging, out of the business code and applies it in many places. The Spring docs name declarative transaction management as its most important service: you write @Transactional, and an aspect opens and commits the transaction for you.
Why does @Transactional not work when a method calls another method in the same class?
Short answer: Spring applies @Transactional through a proxy that wraps your bean. Calls from outside go through the proxy, so the transaction starts. A call from inside the same class uses this, which is the real object, so it skips the proxy and no transaction begins. Fixes: move the method to a separate bean, or switch to AspectJ weaving, which changes the class at build or load time so the call needs no proxy.
@Service
class OrderService {
public void placeOrder() {
save(); // same-class call: skips the proxy, no transaction
}
@Transactional
public void save() { /* ... */ }
}
Red flag answer: "It should work, it has the annotation." This tells the interviewer you treat annotations as magic.
Microservices questions on failure and data consistency
At this level interviewers want to hear what you do when a service call fails, not a definition of microservices. Both questions below are about failure: one about calls, one about data.
How do you stop one slow service from taking down the others?
Model answer: Set a timeout on every outgoing call, so a slow service cannot hold your threads forever. Retry only a few times, with backoff, and only for safe operations. Add a circuit breaker (Resilience4j is the common Java choice): after repeated failures it stops calling the service for a while and returns a fallback.
Red flag answer: "We retry until it works." Unlimited retries pile more load onto a service that is already struggling.
How do you keep data consistent when each service owns its database?
Model answer: Use a saga. Each service runs a local transaction and publishes an event. If a later step fails, earlier services run compensating actions, such as refunding a payment. The data becomes consistent over time, not instantly. That is eventual consistency.
Pros
- Each service keeps its own schema and can deploy alone.
- No locks held across the network, so one slow service does not block the rest.
Cons
- You must write and test a compensating action for every step.
- Users can briefly see an in-between state, which a single shared database would hide.
Red flag answer: "Use a distributed transaction across all services." Two-phase commit locks resources across services and fails badly when one is down.
Red flags that fail a 3-year Java candidate
Most rejections come from a few habits in how you answer, not from missing one fact. Say the interviewer asks how you would find a memory leak, and you have never done it. "I would take a heap dump and look at retained size, though I have only done it in practice projects" passes. "I have never needed to" does not.
Here is the do and don't list drawn from the 15 answers above:
- Don't recite a definition and stop. Do add one example from code you wrote.
- Don't say you never touched GC or threads. Do describe what you would check first.
- Don't claim you used a pattern you cannot explain. Interviewers ask "why" twice.
- Don't treat Spring annotations as magic. Do name the proxy or callback behind them.
If you also interview for frontend roles, my JavaScript interview questions for five years of experience covers browser and async JavaScript at a more senior level. This post stays on the JVM and Spring at three years. For a database-heavy round, the SQL Server interview questions use the same answer style.
Pick the three questions you could not answer aloud, write a one-minute answer to each with one example, and practise saying them to a friend before your next interview. Start with the ones that come with a code example above, equals and hashCode and the @Transactional self-call, because you can test those yourself in ten minutes.