1. Core Java (8, 11, 17)
How would you leverage Java 17 features to refactor a legacy Java 8 codebase?
Answer:
- Records: I would replace verbose POJOs and Lombok
@Data/@Valueclasses withrecordto reduce boilerplate for immutable data carriers.- Pattern Matching for
instanceof: I would eliminate explicit and redundant casting after aninstanceofcheck, making the code cleaner.- Sealed Classes: I would restrict which classes can extend an interface/class to enforce better domain-driven design and enable exhaustive
switchexpressions without default branches.- Text Blocks: I would refactor concatenated String queries (SQL, JSON) into highly readable multi-line text blocks (
""").
How has Garbage Collection evolved from Java 8 to Java 11/17?
Answer:
- Java 8: The default was Parallel GC, which focuses on throughput but can cause noticeable “stop-the-world” pause times.
- Java 11: G1GC became the established default. It segments the heap into regions and aims to provide predictable pause times by collecting regions with the most garbage first.
- Java 17 & Beyond: ZGC (Z Garbage Collector) and Shenandoah GC became production-ready. They perform expensive work concurrently with application threads, achieving sub-millisecond pause times, which is critical for low-latency financial or high-throughput microservices.
2. Concurrency & Multithreading
Explain the internal working of
ConcurrentHashMapin Java 8+ compared to earlier versions.Answer:
- Prior to Java 8,
ConcurrentHashMapused Segment-based locking (Lock Striping), dividing the map into 16 segments.- In Java 8+: It abandoned segments. Instead, it uses a highly concurrent approach leveraging
Nodearrays and Compare-And-Swap (CAS) operations for inserting new nodes. It synchronizes only on the specific bin (bucket) using thesynchronizedkeyword when an update/collision happens. It also transforms linked lists into Balanced Trees (Red-Black trees) when a bucket has more than 8 elements, degrading worst-case search fromto .
When would you use
CompletableFutureover traditionalFutureorExecutorService?Answer:
- Traditional
Future.get()is a blocking operation. It stops the current thread until the result is available.CompletableFutureallows for non-blocking asynchronous programming. I can chain multiple asynchronous tasks using.thenApply(),.thenAccept(), or combine them using.thenCombine(). It also provides robust exception handling pipelines using.exceptionally()or.handle(), making it perfect for orchestrating multiple downstream microservice calls.
3. Generics & Exceptions
Explain the PECS rule in Java Generics. When do you use
? extends Tvs? super T?Answer: PECS stands for Producer Extends, Consumer Super.
? extends T(Producer): Use this when the collection is a producer of data (you are reading from it). You can read items as typeT, but you cannot add anything (exceptnull) because the exact subtype is unknown.? super T(Consumer): Use this when the collection is a consumer of data (you are writing to it). You can safely add items of typeTor its subclasses, but reading from it returnsObject.
How do you handle Checked Exceptions inside Java 8 Streams?
Answer: Streams do not natively allow throwing checked exceptions (like
IOException) from functional interfaces likeFunctionorConsumer. To solve this, I would:
- Wrap the checked exception in an unchecked exception (like
RuntimeException) inside a standardtry-catchblock within the lambda.- Create a custom wrapper/helper method (often called
sneakoruncheck) that catches the checked exception and rethrows it as unchecked, keeping the stream pipeline clean.
4. Spring Boot, Data JPA & Hibernate
How do you identify and resolve the "N+1 selects" problem in Hibernate?
Answer:
- Identification: I spot it in logs (setting
spring.jpa.show-sql=true) or via APM tools when a query fetches a list of parent entities (1 query), and then lazy-loads a child collection for each parent (N queries).- Resolution:
- Use
JOIN FETCHin JPQL/HQL to fetch the parent and child entities in a single inner/outer join query.- Use
@EntityGraphon the repository method to dynamically override fetch strategies (lazy to eager) for specific queries without polluting the global entity mappings.
How does
@Transactionalpropagation work, specificallyREQUIREDvsREQUIRES_NEW?Answer:
REQUIRED(Default): If an active transaction exists, the method joins it. If not, it creates a new one. If the inner method throws a runtime exception, the entire outer transaction rolls back.REQUIRES_NEW: Always suspends the current transaction and creates a brand new one. If the inner transaction fails and rolls back, the outer transaction can catch the exception and successfully commit its own work.
5. Microservices Architecture & REST
How do you handle distributed transactions across multiple microservices?
Answer: Since ACID transactions don’t work across multiple databases, I implement the Saga Pattern.
- Choreography: Services publish domain events (e.g., via Kafka), and other services listen and react. Good for simple workflows.
- Orchestration: A central coordinator service tells participants what local transactions to execute.
- Crucially, both require implementing Compensating Transactions. If step 3 fails, the system must execute compensating actions to undo the local commits of steps 1 and 2.
How do you prevent cascading failures in a microservices ecosystem?
Answer: I use the Circuit Breaker pattern (via Resilience4j). If a downstream service starts timing out or throwing 500s, the circuit “opens” and immediately fails-fast, returning a fallback response or error instead of keeping threads blocked. I also pair this with aggressive timeouts, bulkheading (isolating connection pools), and rate limiting.
6. AWS Infrastructure
EC2 vs ECS/EKS vs AWS Lambda. How do you choose for a Spring Boot app?
Answer:
- EC2 (VM): Legacy approach. High maintenance (OS patching, scaling rules). Avoid unless strict OS-level access is required.
- ECS/EKS (Containers): The sweet spot for Spring Boot. Orchestrates Docker containers efficiently, handles long-running processes well, and scales predictably. ECS (Fargate) is great for zero-server-management.
- Lambda (Serverless): Excellent for event-driven, short-lived tasks. However, Spring Boot has historically suffered from “Cold Starts” in Lambda. If forced to use Lambda, I would compile Spring Boot to a native GraalVM image to reduce startup time from seconds to milliseconds.
7. Database & SQL Query Optimization
You are alerted that a PostgreSQL query suddenly takes 5 seconds. What are your exact steps to optimize it?
Answer:
- Run
EXPLAIN ANALYZEon the query to see the actual execution plan, not just the estimated one.- Look for
Seq Scan(Sequential Scans) on large tables, which indicates missing indexes.- Verify if indexes exist but aren’t being used due to functions on indexed columns (e.g.,
WHERE LOWER(name) = 'foo'). I’d fix this by adding a function-based index.- Check for stale database statistics causing the query planner to make bad choices. Run
ANALYZEorVACUUM.- Review if we are selecting unnecessary columns (
SELECT *) or doing inefficientJOINoperations.
8. CI/CD & DevOps (Harness, Jenkins, Maven)
How do you achieve Zero-Downtime deployments in a CI/CD pipeline?
Answer: I utilize a Blue/Green or Canary deployment strategy.
- In Blue/Green, the new version (Green) is deployed to an identical infrastructure parallel to the live version (Blue). After health checks pass, the load balancer routes traffic to Green instantly. If an issue occurs, we immediately route back to Blue.
- Tools like Harness or Jenkins, combined with Kubernetes Services, automate this traffic shifting and rollback process securely.
In a large multi-module Maven project, how do you optimize dependency management?
Answer: I use a parent
pom.xmlwith<dependencyManagement>. This centrally defines the versions of all third-party libraries and Spring BOMs (Bill of Materials). Sub-modules declare the dependencies they need without versions. This guarantees version consistency across microservices and prevents dependency hell or version collision at runtime.