Production Incidents • GC and JVM Tuning • Concurrency Design • API Design • Code Review • 2026

Java Interview Questions for Experienced Candidates (5 Years)

Java interviews for 5 years of experience stop asking what a HashMap is and start asking why you chose something and what it cost you. Expect questions on memory and garbage collection in production, thread pools and async code, API design, and stories about incidents, pushback and mentoring. It is written for Java developers with roughly five to seven years behind them who own a service or module, make design calls inside it, get paged when it breaks and review other people's code. Each answer below is a first-person story or a decision you can defend. Swap in your own project details before you say it out loud.

Search all questions by round, difficulty and level, or save the ones you want to practice.

JVM and GC 2 questions

Hard Technical round Mid-level, Senior Practice question

1. Which garbage collector did you run your service on, why that one, and what did the choice cost you?

What the interviewer is really testing:
Whether you picked a collector from measured goals like pause time and throughput, and whether you know every collector trades something away.
Answer frame:

Goal first: say what mattered for that service: short pauses, raw throughput or small memory.

Evidence: GC logs or pause numbers before and after the change, under real load.

Cost: name what you gave up, such as extra CPU or more heap headroom.

Sample spoken answer:

“Our pricing service sat behind a strict latency target, and on the default G1 setup we saw occasional pauses long enough to show up in the slowest requests. I turned on GC logging, confirmed the spikes lined up with collections, and then trialled ZGC on one instance under mirrored traffic. Pauses dropped to almost nothing. The cost was real, though: it used noticeably more CPU and wanted more free heap to work with, so we raised the container memory a little. For our batch jobs I did the opposite and kept a throughput-focused collector, because nobody cares about a pause in a nightly job but everyone cares if it finishes late. The lesson I took was to pick the collector per workload, and to change one thing at a time with logs to prove it.”

Red flag to avoid:

Switching collectors because a blog said so, with no measurement before or after and no idea what was traded away.

They may ask next:
  • How would you read a GC log to tell a long pause from lots of short ones?
  • When would tuning heap size beat switching collectors?
Say it in 60 seconds
Hard Situational round Mid-level, Senior Practice question

2. Your Java container keeps getting killed by the orchestrator for memory, but heap graphs look healthy. What was using the memory?

What the interviewer is really testing:
Whether you know the JVM uses a lot of memory outside the heap and can size a container for the whole process.
Answer frame:

Tell: killed from outside with no OutOfMemoryError and no heap dump.

Off-heap: metaspace, thread stacks, code cache, direct buffers and GC structures.

Fix: measure with native memory tracking, then size heap as a share of the container with headroom.

Sample spoken answer:

“The giveaway was that there was no OutOfMemoryError in the logs, just an exit code for a kill, so the kernel was killing the process for exceeding the container limit. The heap was only part of the story. I turned on native memory tracking and used jcmd to get a summary. We had several hundred threads, each with its own stack, and a network library allocating direct buffers that it cached and never gave back fast enough. The heap max had also been set almost equal to the container limit. I reduced the heap so it took a smaller share of the container, capped direct memory, and trimmed a thread pool that was oversized anyway. We accepted slightly more frequent GC in exchange for the process never getting killed.”

Red flag to avoid:

Assuming heap is the only memory the JVM uses and setting max heap equal to the container limit.

They may ask next:
  • Why doesn't a kill like this give you a heap dump?
  • How would you set heap size so it follows the container limit automatically?
Say it in 60 seconds

Production Incidents 2 questions

Hard Behavioral round Mid-level, Senior Practice question

3. One instance of your Java service sits at full CPU and stops answering, with nothing in the logs. Tell me how you found which code was burning it.

What the interviewer is really testing:
Whether you can map a hot OS thread to a Java stack in production and recognise a classic cause like regex backtracking.
Answer frame:

Find the thread: per-thread CPU from the OS, matched to a thread dump by its id in hex.

Confirm: several dumps a few seconds apart that show the same stack each time.

Fix and guard: remove the root cause, bound the input, and add a test so it cannot return.

Sample spoken answer:

“One pod went to full CPU and stopped answering health checks, but the logs showed nothing. I ran top with the per-thread view to find the hottest threads, converted their ids to hex, and matched them to the nid values in a jstack thread dump. I took three dumps a few seconds apart, and the same request threads were deep inside java.util.regex every time. A validation pattern had a repeat nested inside another repeat, and someone had pasted a huge value into a form field that almost matched, so the engine tried an enormous number of ways to split it. I rewrote the pattern so each input can match only one way, capped the input length before matching, and added a test that feeds a long near-miss string with a time limit. The cost is that a few very long but valid values now get rejected, which product agreed to.”

Code:
// Before: nested repeats, a long near-miss input backtracks for ages
Pattern slow = Pattern.compile("(\\w+\\s?)*");

// After: accepts the same inputs, but each one matches only one way
Pattern fast = Pattern.compile("(?:\\w+(?:\\s\\w+)*\\s?)?");

boolean ok = input.length() <= 200 && fast.matcher(input).matches();
Red flag to avoid:

Restarting the pod and moving on, or guessing from the code without checking which threads were actually running.

They may ask next:
  • Why take several thread dumps instead of one?
  • Apart from regex, what else commonly keeps a Java thread at full CPU?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

4. Your service started failing with timeouts waiting for a database connection, while the database itself looked idle. Tell me how you found the cause.

What the interviewer is really testing:
Whether you can tell pool exhaustion from a slow database, find a connection leak with evidence, and close the gap for good.
Answer frame:

Read the symptom: pool wait timeouts with an idle database mean connections are not coming back.

Find the leak: the pool's leak detection, or thread dumps showing who holds connections.

Fix for good: try-with-resources, a sweep for the same pattern, and an alert on pool wait time.

Sample spoken answer:

“Requests began failing because no connection was free in the pool within the timeout, yet the database showed almost no active queries. That pointed at our side: connections were being borrowed and never returned. Our pool had a leak-detection setting that logs the stack trace of any connection held longer than a threshold, so I switched it on for one instance. Within an hour it pointed at a report endpoint that opened a connection by hand and closed it at the end of the method, but an early return on an empty result skipped the close. Every empty report leaked one connection until the pool ran dry. I rewrote it with try-with-resources and searched for the same pattern, which turned up two more. We kept leak detection on with a generous threshold, accepting its small overhead, and added an alert on pool wait time.”

Code:
try (Connection c = dataSource.getConnection();
     PreparedStatement ps = c.prepareStatement(REPORT_SQL)) {
    ps.setLong(1, reportId);
    try (ResultSet rs = ps.executeQuery()) {
        if (!rs.next()) return Report.empty(); // still closes everything
        return Report.from(rs);
    }
}
Red flag to avoid:

Raising the pool size or restarting on a schedule without finding who keeps the connections.

They may ask next:
  • Why not just make the pool bigger?
  • How would thread dumps help if the pool had no leak detection?
Say it in 60 seconds

Concurrency Design 6 questions

Hard Technical round Mid-level, Senior Practice question

5. A downstream dependency slowed down and your thread pool's queue grew until the service fell over. How did you redesign the pool?

What the interviewer is really testing:
Whether you understand that an unbounded queue hides overload, and can choose bounds and a rejection policy on purpose.
Answer frame:

Root cause: an unbounded queue let work pile up in memory while callers waited.

Bound it: a fixed queue size and thread count sized from the downstream's real capacity.

Reject on purpose: pick what happens when full, and make it visible in metrics.

Sample spoken answer:

“The pool was built with a fixed-size factory method, which uses an unbounded queue under the hood. When the downstream got slow, tasks just piled up, memory grew, and by the time requests ran they had already timed out upstream, so we were doing useless work. I replaced it with a ThreadPoolExecutor that has a bounded queue and an explicit rejection policy. One thing people miss is that the pool only grows past its core size once the queue is full, so I set core and max the same to keep it predictable. For this path I used caller-runs, which slows the caller down and pushes back naturally. For another path I rejected fast and returned an error, because a quick failure was better than a slow one. We accepted that some requests now fail under overload, but the service stays up and recovers by itself.”

Code:
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    16, 16, 0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(200),
    new ThreadPoolExecutor.CallerRunsPolicy());
Red flag to avoid:

Making the queue bigger or adding threads without asking what the downstream can actually handle.

They may ask next:
  • How did you pick the queue size of 200 rather than 20 or 2000?
  • When is caller-runs a bad choice?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

6. You moved blocking calls to CompletableFuture to cut latency. What went wrong in production, and how did you fix it?

What the interviewer is really testing:
Whether you know which executor async stages run on, how errors and timeouts surface, and have seen the common pool starve.
Answer frame:

Default executor: async methods without an executor use the shared common pool.

Failure seen: blocking I/O in that pool starved everything else that used it.

Fix: a dedicated executor, a timeout on every stage, and explicit error handling.

Sample spoken answer:

“We fanned out three remote calls with supplyAsync and joined the results, and latency dropped nicely in testing. In production, under load, unrelated parts of the app got slow. The calls were blocking HTTP calls, and because we hadn't passed an executor, they ran on the common ForkJoin pool, which is small and shared across the whole JVM, including parallel streams. A slow downstream tied up every thread in it. I gave the fan-out its own bounded executor, added orTimeout on each future so one slow call couldn't hold the whole request, and used exceptionally to fall back to a default for the non-critical call. I also taught the team that join wraps failures in CompletionException, so our error logs had been hiding the real cause. The cost was one more pool to size and monitor.”

Code:
CompletableFuture<Price> price = CompletableFuture
    .supplyAsync(() -> pricing.fetch(id), ioPool)
    .orTimeout(300, TimeUnit.MILLISECONDS)
    .exceptionally(ex -> Price.unknown());
Red flag to avoid:

Not knowing which pool the async stages ran on, or having no timeout on remote calls inside a future.

They may ask next:
  • What is the difference between thenApply and thenApplyAsync in which thread runs the step?
  • How do you cancel the other calls when one of them fails?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

7. Your team wants to move a blocking service to virtual threads. How did you evaluate it, and what did you have to change first?

What the interviewer is really testing:
Whether you see virtual threads as a fix for waiting on I/O, not for CPU work, and know the traps that come with them.
Answer frame:

Fit: helps when threads mostly wait on I/O, not when work is CPU-bound.

Traps: pinning inside synchronized on the version you run, heavy ThreadLocal use, pooling them.

Limits stay: the database pool is still the real ceiling, so cap concurrency on purpose.

Sample spoken answer:

“Our service spent most of its time waiting on a database and two HTTP services, so it was a good fit. I ran a load test first with one executor swapped for the virtual-thread-per-task executor. Throughput went up, but then the database connection pool became the bottleneck, which made sense: virtual threads remove the thread limit, not the database limit. So I added a semaphore around the database calls to cap concurrency explicitly. On the Java version we ran, blocking inside a synchronized block pinned the carrier thread, and a JFR event showed a hot spot in a client library, so we swapped that lock for a ReentrantLock. We also stopped using ThreadLocal caches, since millions of threads would each get their own copy. We kept the CPU-heavy report generation on a normal fixed pool.”

Code:
Semaphore dbSlots = new Semaphore(50);
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
    exec.submit(() -> {
        dbSlots.acquire();
        try { return repo.load(id); }
        finally { dbSlots.release(); }
    });
}
Red flag to avoid:

Claiming virtual threads make any code faster, or forgetting that the connection pool and downstreams still set the limit.

They may ask next:
  • Why is putting virtual threads in a pool the wrong idea?
  • How would you detect pinning in a running service?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

8. You needed config that changes at runtime without a restart, read by every request thread. How did you make that thread-safe without slowing reads?

What the interviewer is really testing:
Whether you can design safe publication with an immutable snapshot and a single reference, instead of locking every read.
Answer frame:

Snapshot: build a new immutable config object for each change.

Publish: swap one volatile or atomic reference so readers see a whole snapshot.

Read once: each request reads the reference once and uses that snapshot throughout.

Sample spoken answer:

“The first version was a mutable map that a background thread updated while requests read it, and we occasionally saw a request with half old and half new values. I changed it so each reload builds a brand new immutable Config object and then swaps a single AtomicReference to point at it. Because the object is immutable and the reference write is safely published, every reader sees a complete, consistent snapshot with no lock on the read path. I also made request code read the reference once at the start and pass the snapshot along, so a request couldn't see two versions midway. The trade-off is that a reload allocates a whole new object and readers may use the old values for a moment, which was fine for our settings.”

Code:
private final AtomicReference<Config> current =
    new AtomicReference<>(Config.load());

void reload() { current.set(Config.load()); }

Config snapshot() { return current.get(); }
Red flag to avoid:

Putting a synchronized block around every read, or mutating a shared map in place and hoping for the best.

They may ask next:
  • Would a plain volatile field have been enough here? When wouldn't it be?
  • How would you validate a new config before you swap it in?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

9. How do you convince yourself, and a reviewer, that a class you wrote is really thread-safe? Tests passing once isn't proof.

What the interviewer is really testing:
Whether you know concurrency bugs hide from ordinary unit tests and have practical ways to raise the odds of catching them.
Answer frame:

Design first: keep shared state small, immutable where possible, and document which lock guards what.

Stress it: start many threads together with a latch and repeat the run many times.

Specialist tools: a concurrency stress harness for memory-model cases, plus careful review.

Sample spoken answer:

“I start by making the argument on paper: what state is shared, which lock or atomic protects it, and whether any check-then-act spans two operations. I write that down as a comment so the reviewer can check it. Then I write a stress test that starts, say, thirty-two threads at the same moment using a CountDownLatch, has them hammer the class, and checks an invariant at the end, like the final count matching the number of calls. I run it in a loop many times, because a race might show up once in a thousand runs. For low-level code where the memory model matters, I've used the OpenJDK jcstress harness. I'm honest in review that tests reduce the risk rather than prove safety, so the design argument matters as much as the green build.”

Red flag to avoid:

Saying a single passing unit test proves the class is thread-safe.

They may ask next:
  • Why does starting all threads at the same instant matter?
  • What would make you choose a concurrent collection over your own locking?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

10. You cached a slow remote lookup with ConcurrentHashMap.computeIfAbsent, and under load unrelated requests started to stall. Why, and how did you redesign it?

What the interviewer is really testing:
Whether you know computeIfAbsent holds a lock while the mapping function runs, and can keep slow work out of it without losing one load per key.
Answer frame:

Cause: the mapping function runs under a lock on that key's bin, so a slow call blocks other writes.

Fix: store a future per key, so the lock is held only while the future is created.

Cost: failed futures must be removed, and the cache still needs a size limit and expiry.

Sample spoken answer:

“We cached exchange rates from a slow remote service by calling it inside computeIfAbsent. It looked neat, but under load, requests for other keys started timing out. The reason is that computeIfAbsent runs the function while holding a lock on that key's bin in the map, so other writes that land in the same bin wait for the remote call to finish. The docs say the function should be short and simple, and ours took seconds when the downstream was slow. I changed the map to hold a CompletableFuture per key. Creating the future is instant, so the lock is held for almost no time, and every caller for the same key still shares one remote call. The trade-off is that a failed future would stay cached for ever, so I remove it when it fails, and we later moved to a caching library to get a size limit and expiry.”

Code:
private final ConcurrentHashMap<String, CompletableFuture<Rate>> cache =
    new ConcurrentHashMap<>();

CompletableFuture<Rate> rate(String key) {
    CompletableFuture<Rate> f = cache.computeIfAbsent(key,
        k -> CompletableFuture.supplyAsync(() -> remote.load(k), ioPool));
    f.whenComplete((r, ex) -> { if (ex != null) cache.remove(key, f); });
    return f;
}
Red flag to avoid:

Doing remote calls or other slow work inside computeIfAbsent, or caching failures for ever.

They may ask next:
  • Why not call get first, and put the value in after the remote call returns?
  • What goes wrong if the mapping function itself writes to the same map?
Say it in 60 seconds

Performance 4 questions

Hard Situational round Mid-level, Senior Practice question

11. Average latency looks fine, but p99 jumps every few minutes. Walk me through how you found the cause on a Java service.

What the interviewer is really testing:
Whether you look at tail latency with the right JVM tools and can separate GC pauses, lock contention and slow dependencies.
Answer frame:

Correlate: line up the spikes with GC logs, deploys, cron jobs and downstream latency.

Profile: a short JFR recording or sampling profiler during a spike, not an average.

Confirm: make one change and show the tail improves under the same load.

Sample spoken answer:

“The regular rhythm was the clue, so I started by lining up the spikes against GC logs and anything scheduled. They didn't match GC pauses. Then I took a Java Flight Recorder capture across a few spikes and looked at thread states and lock events rather than CPU. Every few minutes a cache refresh ran and took a lock that request threads also needed on the read path, so for a moment everyone queued behind it. I changed the refresh to build a new map off to the side and swap in the reference at the end, so readers never waited. The tail went back to normal. The trade-off was holding two copies of the cache in memory for a few seconds during each refresh, which we had room for.”

Red flag to avoid:

Looking only at averages or CPU graphs and guessing, without a profile taken during the spike.

They may ask next:
  • What would you have done if the spikes did line up with GC pauses?
  • Why can an average hide a problem like this?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

12. An in-memory lookup of about twenty million Long keys to Long values uses far more heap than the raw data. Why, and what did you do about it?

What the interviewer is really testing:
Whether you understand object and boxing overhead in the JVM and can choose a leaner structure when memory is the limit.
Answer frame:

Why: each entry is a node object plus two boxed Long objects plus a table slot.

Options: a primitive-keyed map from a library, or sorted primitive arrays with binary search.

Trade-off: less memory and GC work, in exchange for a less convenient API or slower updates.

Sample spoken answer:

“The raw data was sixteen bytes per pair, but a HashMap stores each entry as its own node object with a header, the hash, the key, the value and a next pointer, and then the key and value are separate boxed Long objects with their own headers. So the overhead was several times the data, and GC had millions of extra objects to trace. The data was loaded once a day and read constantly, so I built two sorted long arrays and used binary search. Heap use dropped sharply and full GCs became rare. The cost was that lookups went from constant time to log time, which was still fast enough, and updates meant rebuilding the arrays. If it had needed frequent writes, I'd have used a primitive-specialised map library instead.”

Code:
long[] keys;   // sorted
long[] values; // same order

long lookup(long key, long missing) {
    int i = Arrays.binarySearch(keys, key);
    return i >= 0 ? values[i] : missing;
}
Red flag to avoid:

Just raising the heap size without understanding where the memory goes.

They may ask next:
  • How would you measure the actual size of a structure like this?
  • What changes if the keys arrive in random order all day?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

13. Right after every deploy, the first few minutes of requests are slow and some time out. Why does a Java service do that, and how did you handle it?

What the interviewer is really testing:
Whether you understand class loading and JIT warm-up and can keep cold instances out of real traffic.
Answer frame:

Cause: classes load lazily and hot code runs interpreted until the JIT compiles it.

Also cold: connection pools, caches and lazy singletons that fill on first use.

Fix: warm up before marking ready, and roll out gradually.

Sample spoken answer:

“A fresh JVM loads classes the first time they're touched, and hot methods start out interpreted until the JIT has seen enough calls to compile them. On top of that our connection pools opened lazily and a few caches were empty. So the first real requests paid all those costs at once, and a few hit the upstream timeout. I added a warm-up step that runs a set of representative calls against the service itself and opens the pools before the readiness check turns green. We also switched to a gradual rollout so a new instance got a small slice of traffic first. The deploy takes a bit longer now, and we have to keep the warm-up calls current as the API changes, but the timeouts after deploy disappeared.”

Red flag to avoid:

Blaming the network or the load balancer without knowing that a cold JVM is slower by design.

They may ask next:
  • What risk is there in sending fake warm-up requests through real code paths?
  • What would you look at to cut JVM start-up time itself?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

14. Profiling showed logging was a real cost on a hot path in your Java service. What was going on, and what did you change?

What the interviewer is really testing:
Whether you know that argument building, synchronous appenders and noisy levels all cost time, and can fix them without losing useful logs.
Answer frame:

Hidden cost: string building and expensive arguments even when the level is off.

I/O cost: synchronous writes and a shared appender lock under load.

Fix: placeholders or guards, async appenders, and fewer lines at the right level.

Sample spoken answer:

“A flame graph showed a surprising slice of time in logging. Two things stood out. Some debug lines used string concatenation and called toString on big objects, which ran even though debug was off. And the appender wrote to disk synchronously, so request threads contended on its lock at peak. I changed the debug lines to placeholder style, and for the ones that called an expensive method, wrapped them in an isDebugEnabled check. I moved to an asynchronous appender with a bounded buffer, and I deleted info lines that logged every item in a loop. The trade-off with async logging is that if the buffer fills you either block or drop lines, so we chose to drop debug lines first and never drop errors.”

Red flag to avoid:

Assuming logging is free, or turning off all logging to fix performance.

They may ask next:
  • Does placeholder logging stop an argument like a method call from being evaluated?
  • What would you log on a hot path, if anything?
Say it in 60 seconds

API Design 3 questions

Medium Technical round Mid-level, Senior Practice question

15. Other teams depend on a Java library you maintain. How do you change its public API without breaking them?

What the interviewer is really testing:
Whether you think about binary and source compatibility, deprecation and versioning as the owner of code others rely on.
Answer frame:

Add, don't break: new methods or overloads first, old ones kept working.

Deprecate clearly: mark with a reason and a replacement, and give a removal window.

Design for change: small public surface, final classes, interfaces and immutable return values.

Sample spoken answer:

“I maintained an internal client library that about a dozen services used. When we needed to change how retries were configured, I didn't change the existing method. I added a builder-based option alongside it, made the old method call the new code, and marked the old one deprecated with a note pointing at the replacement. We bumped the minor version, wrote a short migration note, and removed the old method only in the next major version after checking that no service still called it. Over time I also shrank what was public: classes became final unless designed for extension, internals moved to a package we documented as not for use, and methods returned unmodifiable collections so nobody depended on mutating them. It's slower than just changing things, but no team got a surprise build failure.”

Red flag to avoid:

Renaming or changing method signatures in a minor release and telling other teams to just update.

They may ask next:
  • What kind of change compiles fine for callers but still breaks them at runtime?
  • How would you find out who still uses a deprecated method?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

16. Did you use sealed interfaces and pattern-matching switch to model a domain? Why that over plain polymorphism, and what did it cost?

What the interviewer is really testing:
Whether you understand when a closed set of types with exhaustive switches beats virtual methods, and the trade-off each way.
Answer frame:

Closed set: sealed types list every case, so the compiler checks switches are exhaustive.

When it fits: a stable set of types with many different operations written elsewhere.

Cost: adding a new type forces edits in every switch, which is the point but is work.

Sample spoken answer:

“We modelled payment methods as a sealed interface with records for card, wallet and bank transfer. Lots of different parts of the code needed to do something per type, like fees, display text and refunds, and putting all of that as methods on each class would have tangled payment logic with UI and reporting. With a switch over a sealed type and no default branch, the compiler tells you if you missed a case. When we later added a new payment type, the build failed in every place that needed updating, which was exactly what we wanted. The cost is that adding a type touches many files. If the set of types changed often and the operations were stable, I'd go back to ordinary polymorphism.”

Code:
sealed interface Payment permits Card, Wallet, BankTransfer {}
record Card(String last4) implements Payment {}
record Wallet(String provider) implements Payment {}
record BankTransfer(String ref) implements Payment {}

String label(Payment p) {
    return switch (p) {
        case Card c -> "Card ending " + c.last4();
        case Wallet w -> w.provider() + " wallet";
        case BankTransfer b -> "Transfer " + b.ref();
    };
}
Red flag to avoid:

Using instanceof chains with a silent default branch, so a new type slips through unhandled.

They may ask next:
  • Why does adding a default branch weaken the design here?
  • How would this work if the types lived in different modules?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

17. When you write a generic utility method others will call, how do you choose between List<T>, List<? extends T> and List<? super T>?

What the interviewer is really testing:
Whether you can apply producer-extends, consumer-super to make APIs flexible for callers, not just recite the rule.
Answer frame:

Producer extends: a parameter you only read from takes ? extends T.

Consumer super: a parameter you only write into takes ? super T.

Both or return: read and write means exact T; return types avoid wildcards.

Sample spoken answer:

“The rule I use is producer extends, consumer super. If my method only reads from a collection, I take a wildcard with extends, so a caller can pass a list of a subtype. If it only puts things in, I take super, so they can pass a list of a supertype. I learned this properly when a teammate couldn't pass a list of Integer to my helper that took a list of Number, and had to copy it first. After I changed the parameters, both calls worked with no casting. I don't use wildcards in return types, because that pushes the awkwardness onto every caller. The trade-off is signatures that look scarier in the docs, so I add a short comment with an example call.”

Code:
static <T> void copyAll(Collection<? extends T> from,
                        Collection<? super T> to) {
    to.addAll(from);
}

// works: copy Integers into a List<Number>
copyAll(List.of(1, 2, 3), new ArrayList<Number>());
Red flag to avoid:

Making every parameter an exact List<T> and forcing callers to copy collections to match.

They may ask next:
  • Why can't you add an element to a List<? extends Number>?
  • Where does Collections.max use a bounded wildcard, and why?
Say it in 60 seconds

Reliability 2 questions

Hard Technical round Mid-level, Senior Practice question

18. Your services exchanged data using built-in Java serialization. Why would you move away from it, and how did you migrate safely?

What the interviewer is really testing:
Whether you know the security and versioning problems of native serialization and can plan a migration with both formats live.
Answer frame:

Risks: deserializing untrusted bytes can run harmful code paths; class changes break old data.

Target: a format with explicit schema and clear evolution rules.

Migrate: read both, write new, then remove the old path once nothing sends it.

Sample spoken answer:

“Two services passed cached objects to each other with ObjectOutputStream. It worked until someone added a field to a class that never declared a serialVersionUID, and cached entries from the old version started failing to read after a deploy. The bigger worry was security: deserializing bytes from a shared cache meant any serializable class on the classpath could be instantiated, which is a well-known attack route. I moved us to JSON with explicit DTOs and rules for adding optional fields. The migration went in stages: readers accepted both formats, then writers switched to the new one, and after the old entries expired we deleted the old path. In the meantime I added a deserialization filter to allow only our own classes. The trade-off was a little more code for the DTOs.”

Red flag to avoid:

Not knowing that deserializing untrusted data is a security risk, or planning a big-bang switch across services.

They may ask next:
  • What does a serialVersionUID actually protect you from?
  • How would you handle a field that must be renamed in the new format?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

19. Tell me about an outage caused by a remote call with no timeout. What timeouts did you set afterwards, and how did you choose the numbers?

What the interviewer is really testing:
Whether you learned that every remote call needs connect and request timeouts sized from real latency, and how that affects threads.
Answer frame:

What happened: one slow dependency held threads until the whole service stopped answering.

Timeouts: separate connect and overall request timeouts on every client.

Numbers: based on the dependency's real latency and your own deadline, not a guess.

Sample spoken answer:

“One of our dependencies started accepting connections and then barely responding. Nobody had set a request timeout on our HTTP client, so request threads waited as long as the socket stayed open. Within minutes every thread was stuck and our own health check failed, so the outage spread upstream. After recovering, I went through every client in the service. I set a short connect timeout and a request timeout on each one, sized from that dependency's slow-end latency plus a margin, and made sure the total stayed under the deadline our own callers gave us. I also added a limit on how many calls could be in flight to each dependency. The cost is that we now fail some requests that might have succeeded slowly, and we had to explain that trade to product.”

Code:
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofMillis(500))
    .build();

HttpRequest req = HttpRequest.newBuilder(uri)
    .timeout(Duration.ofSeconds(2))
    .build();
Red flag to avoid:

Picking timeouts at random or adding retries without timeouts, which makes the pile-up worse.

They may ask next:
  • How do retries interact with timeouts, and how do you avoid a retry storm?
  • Why should your timeout be shorter than your caller's?
Say it in 60 seconds

Code Review and Mentoring 3 questions

Medium Behavioral round Mid-level, Senior Practice question

20. A junior on your team keeps sending pull requests with clever but hard-to-read Java. How do you review their code and help them grow?

What the interviewer is really testing:
Whether you can give specific, kind review feedback that teaches judgement, not just demand changes.
Answer frame:

Specific: point to the exact line and why a reader will struggle.

Teach the why: explain the principle once, then let them apply it.

Follow through: pair on one change and notice when they improve.

Sample spoken answer:

“I had a junior who loved long stream chains with nested collectors and one-letter lambda names. The code worked, but nobody else could change it safely. Instead of rewriting it in comments, I picked the worst example and asked them to walk me through it on a call. Halfway through, they got lost in their own code, which made the point better than I could. I explained my rule of thumb: optimise for the next person reading it, name intermediate steps, and use a plain loop when it's clearer. In later reviews I marked comments as must-fix or just a suggestion, so they knew what mattered. A few weeks on, their pull requests were easier to review, and they started leaving the same kind of comment on others' code.”

Red flag to avoid:

Rewriting their code yourself or leaving vague comments like 'this is messy' with no reason.

They may ask next:
  • How do you handle a junior who pushes back on your review comments?
  • When do you approve code you'd have written differently?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

21. Tell me about a time you said no to a technical plan on your service, such as a big rewrite. How did you make the case?

What the interviewer is really testing:
Whether you push back with evidence and an alternative, and can disagree without damaging the working relationship.
Answer frame:

The plan: what was proposed and what worried you about it.

Evidence: measurements or a small spike, plus a cheaper option that met the real goal.

Outcome: what was decided and how you kept the relationship good.

Sample spoken answer:

“A senior colleague proposed rewriting our order service in a fully reactive style because it was hitting thread limits under load. I agreed the problem was real but thought a rewrite would cost months and make the code much harder for the rest of the team to debug. I profiled the service and found most threads were stuck on two slow downstream calls with no timeouts, and the pool was badly sized. I wrote a one-page note with those numbers and proposed a two-week fix: timeouts, a bounded pool and running the two calls concurrently. We agreed to try that first with a clear target. It met the target, so the rewrite was dropped. I made sure to credit my colleague for spotting the problem, and we kept the reactive idea on the list in case load grew much further.”

Red flag to avoid:

Saying no based on taste alone, or going along with a plan you believed was wrong and saying nothing.

They may ask next:
  • What would you have done if the smaller fix hadn't met the target?
  • How do you disagree with someone more senior than you?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

22. Tell me about a piece of work you led with two or three other developers, such as splitting or migrating a module. How did you plan it and keep it safe?

What the interviewer is really testing:
Whether you can break work into safe, shippable steps, share it across a small group and manage risk without a big-bang release.
Answer frame:

Plan: small steps that each ship on their own, with a way back.

People: who owned which part and how you kept everyone in sync.

Safety net: tests, feature flags or side-by-side runs, and the result.

Sample spoken answer:

“I led a group of three to pull the invoicing code out of a large Java monolith into its own module with a clean interface. Instead of one big branch, I split it into steps that each shipped separately. First we wrote characterisation tests around the current behaviour. Then we introduced an interface inside the monolith and moved callers onto it one area at a time. Each person owned a set of callers, and we met for fifteen minutes twice a week to catch conflicts early. For the riskiest part, calculating totals, we ran old and new code side by side and logged any difference for two weeks before switching. It took longer than one big change would have on paper, but we never had a rollback, and the team could keep shipping features alongside it.”

Red flag to avoid:

Describing a long-lived branch merged in one go with no tests or rollback plan.

They may ask next:
  • What did you do when the side-by-side run showed differences?
  • How did you decide the order of the steps?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

You practiced these. On the real call, ClapAssist helps with the rest.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.

Download with 10 free minutes
Mac and Windows · Stays out of screen share · No card