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/@Value classes with record to reduce boilerplate for immutable data carriers.
  • Pattern Matching for instanceof: I would eliminate explicit and redundant casting after an instanceof check, 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 switch expressions 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 ConcurrentHashMap in Java 8+ compared to earlier versions.

Answer:

  • Prior to Java 8, ConcurrentHashMap used 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 Node arrays and Compare-And-Swap (CAS) operations for inserting new nodes. It synchronizes only on the specific bin (bucket) using the synchronized keyword 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 from to .

When would you use CompletableFuture over traditional Future or ExecutorService?

Answer:

  • Traditional Future.get() is a blocking operation. It stops the current thread until the result is available.
  • CompletableFuture allows 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 T vs ? 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 type T, but you cannot add anything (except null) 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 type T or its subclasses, but reading from it returns Object.

How do you handle Checked Exceptions inside Java 8 Streams?

Answer: Streams do not natively allow throwing checked exceptions (like IOException) from functional interfaces like Function or Consumer. To solve this, I would:

  1. Wrap the checked exception in an unchecked exception (like RuntimeException) inside a standard try-catch block within the lambda.
  2. Create a custom wrapper/helper method (often called sneak or uncheck) 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:
    1. Use JOIN FETCH in JPQL/HQL to fetch the parent and child entities in a single inner/outer join query.
    2. Use @EntityGraph on the repository method to dynamically override fetch strategies (lazy to eager) for specific queries without polluting the global entity mappings.

How does @Transactional propagation work, specifically REQUIRED vs REQUIRES_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:

  1. Run EXPLAIN ANALYZE on the query to see the actual execution plan, not just the estimated one.
  2. Look for Seq Scan (Sequential Scans) on large tables, which indicates missing indexes.
  3. 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.
  4. Check for stale database statistics causing the query planner to make bad choices. Run ANALYZE or VACUUM.
  5. Review if we are selecting unnecessary columns (SELECT *) or doing inefficient JOIN operations.

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.xml with <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.