Top 40 Java Interview Questions and Answers (2026 Edition)
Preparing for a Java backend interview? These are the 40 questions that come up most often — each with a crisp, interview-ready answer. Every answer links to a full deep-dive guide in this series when you want the follow-up traps and trade-offs.
How to use this: read the question, say your answer out loud, then check. Interviewers score decision rules and failure modes, not definitions — for every answer, reach: when would you use this, what guarantee does it provide, and how does it fail?
Modern Java
1. What is a record, and where does its "immutability" end?
Answer: a record is a compact data carrier — the compiler generates the canonical constructor, accessors, equals, hashCode, and toString. Reach for it for DTOs, value objects, and tuple-like returns. But the immutability is shallow: component fields are final, yet a mutable component stays mutable — record Order(List<String> items) {} doesn't make the list immutable. That's the interview trap. Use the compact constructor to validate (Objects.requireNonNull) and take defensive copies (List.copyOf) — your code runs first, then the implicit assignments happen. Don't use records where you need inheritance or mutable state, and check framework integration (JPA entities, serialization), since there's no no-arg constructor. Deep dive →
2. What are sealed classes, and what problem do they solve?
Answer: sealed classes constrain the permitted direct-subtype hierarchy explicitly (the permits clause) — permitted subtypes can themselves be final, sealed, or non-sealed. The payoff is exhaustive pattern matching over a closed set: the compiler verifies you've handled every subtype. The trap: sealing is about API control and exhaustiveness, not security — reflection can still reach into permitted subclasses. Deep dive →
3. Explain pattern matching for instanceof and switch — including switch expressions.
Answer: pattern matching combines a type test, cast, and binding in one step — if (obj instanceof String s) lets you use s directly. In switch, patterns match on types with guards (case String s when s.isEmpty()), and a pattern switch throws on null unless you add case null — exactly the detail that separates people who've used it from people who've read about it. As an expression, switch returns a value with arrow syntax and no fall-through; every branch must produce a value (or throw), yield is required inside braced branches, and the compiler enforces exhaustiveness for enums and sealed types. Trap: order cases most-specific first, and don't mix colon and arrow syntax in one switch. Deep dive →
4. Virtual threads vs platform threads — what changed, and what didn't?
Answer: virtual threads make thread-per-task practical again for high-concurrency blocking I/O — they're cheap, JVM-managed threads, so blocking calls no longer pin expensive OS threads. But update your mental model past Java 21: the old warning that "blocking inside synchronized pins a carrier thread" was largely addressed by JEP 491 in JDK 24 — virtual threads are no longer generally pinned merely by blocking inside synchronized, so don't teach it as a timeless trap. What didn't change: virtual threads don't make CPU work faster, don't remove downstream capacity limits, and aren't a reason to create unbounded concurrency. The modern interview sentence: virtual threads remove thread scarcity — they do not remove resource scarcity. Deep dive →
5. What problems do structured concurrency and scoped values solve — and what is their status in your target JDK?
Answer: structured concurrency treats a group of related tasks as a single unit of work — if one fails, the rest are cancelled, and everything completes before the block exits. Scoped values are designed for bounded, immutable context propagation and can be a better fit than ThreadLocal for that use case, especially with virtual threads — but not every ThreadLocal use case maps directly to a scoped value. The critical interview discipline: their API and status have kept evolving across JDK releases, so say which JDK you're talking about and whether it treats them as preview, incubator, or final. An answer that names the status beats one that recites the API. Deep dive →
6. Virtual threads are cheap — so what do you still bound?
Answer: you stop tuning thread counts and start bounding scarce downstream resources: the database connection pool, concurrent calls to a downstream API, rate-limited third-party services. The shape of the answer: create a virtual thread per task, then bound access to the scarce resource (a semaphore around the pool, bulkheads per dependency). The arithmetic that scores: 1,000,000 virtual threads against a 100-connection pool still means ~100 concurrent database operations — threads were never the bottleneck. The trap answer is "virtual threads = unlimited concurrency." Deep dive →
Spring Boot & DI
7. How does auto-configuration actually work?
Answer: auto-configuration uses @Conditional annotations (OnClass, OnMissingBean, OnProperty) to register beans only when the classpath and properties call for them — classpath + properties + conditions → auto-configuration. Candidate configurations are listed in AutoConfiguration.imports; conditions decide which ones apply. The trap question: "how do you debug it?" — the --debug flag (or the auto-configuration report) shows exactly which conditions matched and which didn't. Deep dive →
8. @Component vs @Service vs @Repository — is there a real difference?
Answer: technically all three are @Component stereotypes — the container treats them identically for component scanning. The difference is semantics plus one behavior: @Repository also participates in Spring's persistence exception-translation mechanism (vendor exceptions become Spring's DataAccessException hierarchy, via Spring's post-processing infrastructure — the annotation alone doesn't magically catch everything). Use the stereotype that describes the layer — it documents intent and enables the translation where it matters. Deep dive →
9. @Value vs @ConfigurationProperties — what's the decision rule?
Answer: use @Value for isolated values; use @ConfigurationProperties for cohesive, typed configuration groups that benefit from binding and validation. The distinction isn't literally "validation is impossible with @Value" — it's that configuration properties give you type-safe, hierarchical binding plus @Validated for anything beyond a couple of keys. The trap: relaxed binding means my-property, myProperty, and MY_PROPERTY can all bind the same field — know it when debugging "why is this null". Deep dive →
10. What changed from Spring Boot 2 → 3 → 4 that matters during an upgrade?
Answer: don't recite release notes — the interview question is can you safely upgrade a production Spring application? Boot 3: Java 17 baseline, the javax.* → jakarta.* namespace migration (the big migration pain — audit every dependency that hasn't moved), Spring Framework 6, and observability/native-image evolution. Boot 4: the Spring Framework 7 generation, a newer dependency and platform baseline, modularization and evolution of test starters and APIs, Spring gRPC support, and further OpenTelemetry work — with real upgrade and deprecation implications. The upgrade discipline: check the baseline JDK, migrate the namespace, verify third-party starters, and run the full test suite — deprecations in 4.x are removals in waiting. Deep dive →
11. How does a request actually flow through a Spring Boot app?
Answer: the DispatcherServlet receives the request, consults HandlerMappings to find the @Controller method, then HandlerAdapters resolve arguments and convert @RequestBody. Filters run before the servlet (security, logging); interceptors run inside Spring MVC around the handler. The "draw it" follow-up tests whether you know where cross-cutting concerns belong. Deep dive →
12. @SpringBootTest vs @WebMvcTest vs @DataJpaTest — and when is an embedded database not enough?
Answer: @SpringBootTest loads the full application context — correct but slow. @WebMvcTest loads only the web layer (controllers with mocked services); @DataJpaTest loads only the JPA slice. Decision rule: test the narrowest slice that covers what you're asserting. The 2026 caveat: an embedded database is the default and it's fast, but if production runs PostgreSQL/MySQL-specific behavior — dialects, JSON columns, locking semantics — test important repository behavior against the real engine, typically with Testcontainers. Green tests against the wrong database are a false confidence trap. Deep dive →
13. What does the Actuator expose, and what do you lock down in production?
Answer: Actuator exposes production-ready endpoints under /actuator: health, metrics, info, env, and more. In production, expose health (and selectively info/metrics) but lock down env, beans, and heapdump behind security — they leak internals. And go one step further than "health is UP": liveness ≠ readiness — liveness asks "is the process alive, restart it if not," readiness asks "can it serve traffic right now" (pull it from the load balancer during startup or dependency outages). That distinction ties directly into Kubernetes probes and SRE practice. Deep dive →
Persistence & Transactions
14. How does @Transactional actually work — and why does self-invocation break it?
Answer: Spring normally applies declarative transactions through a proxy: the transaction boundary exists when the call passes through the proxy. A method calling another @Transactional method on this bypasses the proxy — no transaction, the classic trap. Rollback rules: unchecked exceptions roll back by default, checked exceptions don't (unless configured). And the production judgment: keep external network calls out of a long database transaction unless the consistency requirement truly demands that coupling — a slow downstream API holding your transaction open is how you get lock contention and timeouts. Deep dive →
15. What are transaction isolation levels, and what anomalies do they prevent?
Answer: the levels — read uncommitted, read committed, repeatable read, serializable — trade concurrency for protection against dirty reads, non-repeatable reads, and phantoms. Name the anomaly your business invariant actually needs protection from, then pick the weakest level that provides it — higher isolation means more contention and more deadlocks. The trap: isolation levels don't prevent lost updates on their own; concurrent read-modify-write needs locking (next question), not just a stricter level. Deep dive →
16. Your JPA query is slow — how do you detect and fix the N+1 problem?
Answer: detect it by looking at the SQL — enable query logging or assert query counts in tests; one query becoming hundreds is the signature. Fix it with fetch joins, entity graphs, or batch fetching so the association loads in the same round trip. The trap: don't blindly flip everything to EAGER — that just moves the cost to every query that touches the entity (see next question). Deep dive →
17. LAZY vs EAGER — why isn't EAGER the N+1 fix?
Answer: EAGER loads the association on every query whether you need it or not — cartesian products, memory bloat, and unpredictable object graphs that grow as the domain model grows. Default to LAZY and choose the fetch strategy per query (entity graph or fetch join for the use case that needs it). Decision rule: fetching is a per-use-case decision, not a per-mapping decision. Deep dive →
18. Two requests update the same row — optimistic or pessimistic locking?
Answer: optimistic (a version column; the loser gets a conflict exception and retries) wins under low contention — no locks held, high throughput. Pessimistic (explicit lock, e.g. SELECT ... FOR UPDATE) wins when contention is high or a retry is expensive — you pay lock wait time to avoid wasted work. The trap: optimistic locking without a retry strategy is just a fancy error message; and pessimistic locking inside a long transaction is how you build a deadlock. Deep dive →
19. How do you make an API operation idempotent — say, payment or order creation?
Answer: the client sends an idempotency key — a stable operation ID — with the request. The server stores the key with the result of the first execution and returns the stored result on replay instead of re-executing. Dedupe on the key, expire keys after a sensible window. This is the difference between "the user double-clicked" and "we charged them twice." The trap: retrying a non-idempotent write and calling the retry logic "resilience." Deep dive →
20. Your database transaction commits, but publishing the event fails. What now?
Answer: the transactional outbox pattern: in the same database transaction that applies the state change, insert the event into an outbox table. A separate relay reads the outbox and publishes to the broker, marking events sent. If the relay crashes, it replays from the outbox — a committed state change can never lose its event. This is the bridge between database state and message publication, and it connects transactions, JPA, messaging, and distributed correctness in one answer. Deep dive →
Concurrency & Virtual Threads
21. What does volatile actually guarantee — and what doesn't it?
Answer: a write to a volatile variable happens-before a subsequent read of that same variable — that gives you visibility and ordering guarantees (no instruction reordering around the access), but not atomicity: count++ on a volatile int still races. Don't say "immediately visible" — say happens-before. Decision rule: volatile for flags and safe publication; AtomicInteger or synchronization for read-modify-write. Deep dive →
22. synchronized vs ReentrantLock — when do you reach for the Lock?
Answer: synchronized is simpler and the JVM optimizes it aggressively; ReentrantLock adds tryLock with timeout, interruptible lock acquisition, and fairness policies. Reach for the Lock when you need timed or interruptible acquisition, or multiple Condition queues. The trap: a Lock must be released in finally — the compiler won't save you like it does with synchronized. Deep dive →
23. How does ConcurrentHashMap achieve thread safety — and where does it stop?
Answer: fine-grained locking — modern versions combine CAS with synchronized on individual bins — so readers never block and writers only contend on the same bin. Per-key operations like putIfAbsent are atomic. But make this explicit: atomic per-key operations ≠ an atomic multi-step or multi-key business operation — check-then-act across keys still needs external coordination, and that's a favorite interview follow-up because it bridges straight into distributed transactions. Iteration is weakly consistent: no ConcurrentModificationException, but you may miss concurrent updates. Deep dive →
24. Explain deadlock — and how do you prevent it?
Answer: deadlock needs all four Coffman conditions: mutual exclusion, hold-and-wait, no preemption, and circular wait. Prevention means breaking one of them — in practice, enforcing a consistent global lock ordering. The production follow-up: "how do you detect it?" — a thread dump (via jstack or your JDK diagnostic tooling) shows the deadlock and the lock cycle explicitly. Deep dive →
25. How do you size a thread pool — and how does that change with virtual threads?
Answer: distinguish the two models. Platform-thread pool: CPU-bound work gets roughly core count; blocking I/O gets more threads, reasoned as throughput × latency and bounded by memory and downstream capacity. Virtual-thread model: don't tune a huge worker pool at all — create a virtual thread per task, then bound access to scarce resources (database connections, downstream APIs, rate-limited services). The trap in both models: an unbounded pool under load becomes a fork bomb — always bound the queue and know your rejection policy. Deep dive →
26. How do you compose async work with CompletableFuture — without callback hell?
Answer: virtual threads don't make CompletableFuture obsolete — candidates should still understand sequential composition (thenApply/thenCompose), parallel composition (allOf/anyOf), exception propagation (exceptionally/handle), and executor choice. thenCompose flattens nested futures (like flatMap); thenApply transforms the value. The trap: the default ForkJoinPool.commonPool() is shared — for blocking I/O stages, supply your own Executor or you'll starve everyone else on the common pool. Deep dive →
27. What is ThreadLocal, and what's the catch — especially with virtual threads?
Answer: ThreadLocal gives each thread its own copy of a variable — handy for per-request context like a user id. Two catches. First, in pooled threads values leak across requests unless you remove() in a finally block — a classic source of memory leaks and cross-request contamination. Second, ThreadLocal isn't forbidden with virtual threads, but reconsider it when you're using it primarily for immutable contextual propagation — that's scoped values' design center — because large amounts of thread-local state across huge numbers of virtual threads have memory and design consequences. Scoped values are a design choice for that use case, not a blanket replacement. Deep dive →
Microservices & Resilience
28. Monolith vs microservices — when would you NOT split?
Answer: microservices buy independent deployability and team autonomy at the price of distributed-systems complexity: network failures, eventual consistency, observability overhead. Don't split with a small team, unclear domain boundaries, or when you can't afford the operational cost — a well-modularized monolith beats a premature distributed monolith every time. Deep dive →
29. How do you decide service boundaries — and who owns the data?
Answer: split along business capabilities using DDD bounded contexts (Orders, Payments, Inventory) — never along technical layers (there is no "database service"). Prefer clear data ownership by service; direct cross-service writes into another service's tables are a strong coupling smell — real migrations and legacy environments aren't always textbook-clean, but every exception should be named and temporary. The trap: drawing boundaries around teams instead of domains just recreates the monolith with network calls. Deep dive →
30. REST vs gRPC vs messaging — how do you choose?
Answer: drop the absolutes. REST when broad HTTP interoperability and resource-oriented APIs matter; gRPC when strongly typed RPC, streaming, and efficient service communication fit — and yes, gRPC can be externally exposed while REST works fine internally. Messaging when producer and consumer should be temporally decoupled. Decision rule: synchronous when you need an answer now, asynchronous when you don't — and for long-running operations, polling a job/status resource is a perfectly valid asynchronous API pattern, not "faking async." Deep dive →
31. How do you stop one failing service from cascading?
Answer: defense in depth: timeouts on every call (no unbounded waits), retries with exponential backoff and jitter, circuit breakers (fail fast when a dependency is down), and bulkheads (isolate thread pools per dependency). Two additions that matter: retry only idempotent/safe operations unless you have an idempotency mechanism — otherwise failure → retry becomes failure → duplicate side effects — and remember that retries are the most common cascade amplifier: unbounded retries turn a partial outage into a full one. Deep dive →
32. How do you handle transactions across services?
Answer: with the Saga pattern: a sequence of local transactions, each paired with a compensating action for rollback. Choreography (events, no central controller) is simpler to start but harder to trace; orchestration (a saga orchestrator commands each step) is easier to reason about. The trap: sagas give eventual consistency, not ACID — design for it instead of pretending. Deep dive →
33. How do you debug a request across 12 services?
Answer: think in three signals, not one tool: metrics tell you something is wrong, traces help locate the path, logs provide event-level context — a production incident may need them in any order depending on the failure. Propagate a correlation/trace ID on every request and put it in every structured log line so one ID reconstructs the whole journey. Make OpenTelemetry central: it standardizes how you instrument and export telemetry rather than being just another tracing UI alongside Jaeger or Zipkin. Deep dive →
34. API gateway vs service mesh — what does each do?
Answer: an API gateway handles edge concerns for external clients: routing, authentication, rate limiting, and request shaping. A service mesh (Istio, Linkerd) handles east-west traffic between services: mTLS, retries, circuit breaking, and telemetry. You don't need a mesh on day one — start with a gateway and add the mesh when service-to-service concerns get painful. Deep dive →
35. How do you evolve an API without breaking old clients?
Answer: backward compatibility as a discipline: only additive changes (new optional fields, new endpoints) — never rename or remove in place. For breaking changes, expand → migrate → contract: ship the new shape alongside the old, move writers, then move readers, then delete. Version explicitly where the contract demands it. This is daily production work — systems are never greenfield — and interviewers ask it to see whether you've ever actually retired an endpoint. Deep dive →
JVM Production & Debugging
36. Your Java service is slow in production — where do you start?
Answer: work the triage order before reaching for tools: is it CPU-bound (hot methods, tight loops), blocked (waiting on locks, downstream calls, pool exhaustion), or GC-bound (pauses eating your latency budget)? Then drill with the right instrument: thread dumps for blocked threads, CPU profiling and JFR for hot spots, GC logs for pause time — correlated against metrics, traces, and structured logs. The interview signal: a methodical elimination, not "I'd restart it." Deep dive →
37. The service pauses, or its memory keeps growing — how do you investigate?
Answer: separate the two symptoms. Pauses: check GC pause time and frequency against your latency budget, plus allocation rate — high allocation of short-lived objects is often the real culprit. Growing memory: distinguish a leak from a legitimately growing live set with heap usage trends over time, then take a heap dump and find what's retaining it. Instruments: GC logs, heap dumps, JFR and JDK tooling. And the trap answer to avoid: don't "fix" everything by raising -Xmx — a bigger heap just delays the same outage. Deep dive →
38. What changes when your JVM runs in a container?
Answer: modern JVMs are container-aware and read cgroup limits — but only if you let them. Size the heap as a percentage of the container limit (-XX:MaxRAMPercentage), not a hardcoded -Xmx: a fixed -Xmx above the container's limit gets your pod OOMKilled, and one sized for a developer laptop wastes the node. Same for CPU: check what the JVM sees as available processors under your CPU shares. The interview signal: you know the JVM doesn't see the host anymore — it sees the container. Deep dive →
39. How do you shut a Java service down gracefully?
Answer: on SIGTERM, stop accepting new work and drain in-flight requests before exiting — Spring Boot's graceful shutdown support exists for exactly this. In Kubernetes, that means honoring the termination grace period (a preStop hook plus terminationGracePeriodSeconds long enough for your longest request) so the pod finishes serving before it's killed. The trap: a fast shutdown that drops in-flight requests turns every deploy into a small outage. Deep dive →
40. You can't reproduce a production bug locally — now what?
Answer: if you can't reproduce it, instrument it. Correlate with the trace/correlation ID from structured logs, capture a JFR flight recording around the failure window, and take heap and thread dumps while it's happening — production evidence beats local guessing. Add the missing metric, log line, or assertion that would have caught it, so the next occurrence is a dashboard alert instead of a mystery. The senior sentence: "Every unreproducible bug is an observability gap with a bug attached." Deep dive →
In this series
- Top 40 Java Interview Questions and Answers (2026 Edition) (this post) — start here.
- Java 17 to 21 Interview Questions: Records, Pattern Matching, Virtual Threads — modern language features.
- Spring Boot Interview Questions and Answers (2026 Edition) — the framework, deeply.
- Java Multithreading and Concurrency Interview Questions: Deep Dive — threads, locks, and traps.
- Microservices Interview Questions for Java Developers — distributed systems thinking.
Related: System Design Interviews: A Practical Primer — the 4-step framework these Java questions plug into.
Comments
Post a Comment