โšก Ultimate Masterclass โ€ข Java 21 โ€“ 25 Focus

Modern Java for Interviews & Online Assessments

Move beyond verbose Java 8/11 habits. This masterclass teaches how Java is actually written in Java 21 through Java 25 โ€” covering modern language ergonomics, data-oriented design, concurrency with Virtual Threads, JVM memory, Generational ZGC, Java 8-to-25 modernization, and modern unit and integration testing.

๐Ÿ“š 15 Comprehensive Modules โฑ๏ธ ~85 min deep dive ๐ŸŽฏ 33 Interactive Quizzes & Drills ๐Ÿš€ Java 25 LTS aligned
Module 01 Runtime & Syntax Evolution

The Modern Java Paradigm (Java 21 to 25)

Java has evolved from an overly verbose, ceremony-heavy enterprise language into a lean, expressive, data-oriented language. The Long-Term Support (LTS) cadence anchored by Java 21 (September 2023) and culminating in Java 25 (September 2025 LTS) represents the biggest leap in developer ergonomics since Java 8.

1. Java Version & Feature Evolution Matrix

Interviewers frequently ask candidates to explain the differences between LTS baselines:

VersionStatusKey Features & ParadigmsInterview Mindset
Java 8Legacy BaselineLambdas, Stream API, Optional, default methods.Historical baseline; writing code like this in 2026 indicates lack of modernization.
Java 11LTS (2018)var in lambdas, HTTP Client, string utilities (isBlank, repeat).Enterprise migration milestone; modularity (JPMS) maturity.
Java 17LTS (2021)Records, Sealed classes, Pattern matching for instanceof, Text Blocks.Introduced data-oriented programming to Java.
Java 21LTS (2023)Virtual Threads (Loom), Sequenced Collections, Record Patterns, Pattern matching switch, Generational ZGC.Modern concurrency and pattern matching foundation.
Java 25LTS (2025/2026)Stream Gatherers finalized, Flexible Constructor Bodies, Compact Object Headers, Module imports, Primitive patterns.Maximum developer speed, zero-ceremony scripts, next-gen memory efficiency.

2. Implicitly Declared Classes & Instance Main Methods (Java 22โ€“25)

In online assessments (LeetCode, HackerRank, CodeSignal) and rapid prototyping, you no longer need the ancient ritual of declaring an explicit class and a verbose public static void main(String[] args). Java now supports instance main methods and implicit class declarations (JEP 477, JEP 495).

Legacy Java (Java 8โ€“17) โŒ Ceremony
public class Solution {
    public static void main(String[] args) {
        System.out.println("Hello, Assessment!");
    }
}
Modern Java 25 โœ… Clean & Direct
// Unnamed class & instance main
void main() {
    IO.println("Hello, Assessment!");
}

3. Module Import Declarations: import module java.base;

In Java 23 through Java 25 (JEP 476), you can import all public packages exported by the java.base module with a single statement:

// Java 23/25: Imports all public packages exported by java.base
import module java.base;

void main() {
    List<String> names = List.of("Alice", "Bob", "Charlie");
    Map<Integer, List<String>> byLength = names.stream()
        .collect(Collectors.groupingBy(String::length));
    IO.println(byLength);
}

4. Local Variable Type Inference (var) โ€” Best Practices

  • Use var when it eliminates redundant ceremony:
    var frequencyMap = new HashMap<String, List<Integer>>();
  • Use var in try-with-resources:
    try (var reader = new BufferedReader(new FileReader(path))) { ... }
  • DO NOT use var when it obscures intent:
    var result = executeQuery(); (unclear return type).
  • Remember its limitations: Cannot be used on fields, method parameters, or method return types.

๐ŸŽฏ Module 01 Takeaways

  • Java 25 reduces boilerplate to zero: instance main methods and import module java.base;.
  • var is for eliminating repetition, not hiding domain types.
  • Java is fully statically typed: var determines the exact type at compile time.
Knowledge Check 01A โ€ข Type Inference

Which of the following statements about var in modern Java is valid?

Knowledge Check 01B โ€ข Module Imports

In Java 23โ€“25, what happens if code contains import module java.base; alongside an explicit single-type import?

Module 02 Data Modeling & Immutability

Domain Modeling: Records, Immutability & Flexible Constructors

Modern Java models data with Records, compact constructors, and strict Sealed Hierarchies.

1. Records: Transparent Data Carriers

A record is an immutable class whose state description is declared in the header. The compiler automatically generates private final fields, canonical constructors, public accessors (e.g. id(), NOT getId()), correct value-based equals() and hashCode(), and a descriptive toString().

public record User(long id, String email, int age) {
    // Compact constructor: input validation & normalization
    public User {
        Objects.requireNonNull(email, "email must not be null");
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative: " + age);
        }
        email = email.trim().toLowerCase(); // Normalize before assignment
    }
}
๐ŸŽฏ Interview Question: Records vs Lombok @Value

"Why choose Java Records over Lombok's @Value or @Data?"
Answer: Records are a first-class language feature baked into the JVM bytecode and type system. They provide semantic transparency โ€” meaning the compiler and serialization frameworks guarantee that a record's state can be deconstructed into its components and reconstructed through its canonical constructor. This enables Record Patterns (deconstruction) and compiler exhaustiveness checking.

2. Flexible Constructor Bodies (Statements Before super(...))

Finalized in Java 25 (JEP 513; previewed in Java 22โ€“24 via JEP 447, 482 and 492), Java allows statements before calling super(...), provided they do not reference the uninitialized instance (this):

class Channel {
    protected final int timeoutMs;

    protected Channel(int timeoutMs) {
        this.timeoutMs = timeoutMs;
    }
}

public class SecureChannel extends Channel {
    private final String cipherKey;

    public SecureChannel(String rawKey, int timeoutMs) {
        // Java 25 (final): Statements before super(...)!
        if (rawKey == null || rawKey.length() < 32) {
            throw new IllegalArgumentException("Key too short or null");
        }
        int sanitizedTimeout = Math.clamp(timeoutMs, 100, 10_000);

        super(sanitizedTimeout);
        this.cipherKey = rawKey;
    }
}

void main() {
    var tmp = new SecureChannel("Bozo Ozo Froso Wozo Sozo Mozo Katozo Olozo", 0);
    IO.println(tmp.cipherKey);
}

3. Sealed Classes & Algebraic Data Types (ADTs)

By combining sealed interfaces with record classes, modern Java provides Algebraic Data Types (Sum Types + Product Types):

public sealed interface OrderStatus permits OrderStatus.Pending, OrderStatus.Shipped, OrderStatus.Delivered, OrderStatus.Cancelled {
    record Pending(Instant createdTime) implements OrderStatus {}
    record Shipped(String trackingNumber, Instant shippedTime) implements OrderStatus {}
    record Delivered(Instant deliveryTime, String recipientSignature) implements OrderStatus {}
    record Cancelled(String reason, Instant cancelTime) implements OrderStatus {}
}

๐ŸŽฏ Module 02 Takeaways

  • Use records for all DTOs, query results, map keys, and composite return values.
  • Use compact constructors for validation without redundant field assignments.
  • Model domain states with sealed interfaces + records to guarantee compile-time exhaustiveness.
  • Take advantage of flexible constructor bodies to validate before delegating to super().
Knowledge Check 02A โ€ข Record Immutability

What happens if you try to mutate a record's component field via a custom method?

Knowledge Check 02B โ€ข Flexible Constructors

Under Java 22โ€“25 Flexible Constructor Bodies (JEP 482), which operation is FORBIDDEN before calling super(...)?

Module 03 Control Flow & Safety

Modern Pattern Matching Masterclass

Pattern matching in modern Java turns clumsy type checking and manual casting into clean, declarative data queries across instanceof, enhanced switch expressions, record deconstruction, and primitive patterns.

1. Pattern Matching for instanceof

// Flow scoping: 's' is only in scope where condition is guaranteed true
if (obj instanceof String s && !s.isBlank()) {
    IO.println("Length: " + s.length());
}

2. Pattern Matching in switch with Guard Clauses (when)

String describe(Object obj) {
    return switch (obj) {
        case null                  -> "Null reference";
        case Integer i when i < 0  -> "Negative integer: " + i;
        case Integer i             -> "Non-negative integer: " + i;
        case String s when s.isEmpty() -> "Empty string";
        case String s              -> "String of length " + s.length();
        case List<?> list          -> "List containing " + list.size() + " elements";
        default                    -> "Unknown object type: " + obj.getClass().getSimpleName();
    };
}

3. Record Deconstruction Patterns & Unnamed Patterns (_)

record Point(double x, double y) {}
record Circle(Point center, double radius) implements Shape {}
record Rectangle(Point topLeft, double width, double height) implements Shape {}

sealed interface Shape permits Circle, Rectangle {}

double calculateArea(Shape shape) {
    return switch (shape) {
        // Discard unneeded center point with '_' (JEP 456)
        case Circle(_, var radius) -> Math.PI * radius * radius;

        // Nested deconstruction: extract coordinates directly
        case Rectangle(Point(var x, var y), var w, var h) -> {
            IO.println("Top-left: (" + x + ", " + y + ")");
            yield w * h;
        }
    }; // NO 'default:' needed! Compiler proves Circle and Rectangle cover all Shape permits.
}

4. Primitive Types in Patterns (Java 23 to 25)

// Java 23/25 Primitive patterns (JEP 455): zero autoboxing overhead!
int statusCode = 200;
String status = switch (statusCode) {
    case 200 -> "OK";
    case 404 -> "Not Found";
    case int code when code >= 500 -> "Server Error (" + code + ")";
    case int code -> "Other Status: " + code;
};

๐ŸŽฏ Module 03 Takeaways

  • Use when guards for clean conditional branching inside switch.
  • Deconstruct records directly in cases; discard unneeded fields with _.
  • Never write a default: on a sealed switch in production or interview code.
Knowledge Check 03A โ€ข Exhaustiveness & Sealed Classes

Why is omitting the default clause considered superior when switching on a sealed hierarchy?

Knowledge Check 03B โ€ข Flow Scoping Rules

Given if (obj instanceof String s && s.length() > 5), why does replacing && with || fail compilation?

Module 04 Collections & Data Structures

Modern Collections & OA Algorithms

Java 21 introduced Sequenced Collections, fixing a 25-year-old gap in the standard library.

1. Sequenced Collections (Java 21 LTS)

SequencedCollection<String> items = new ArrayList<>(List.of("A", "B", "C", "D"));

String first = items.getFirst();       // "A"
String last  = items.getLast();        // "D"

items.addFirst("Z");                  // ["Z", "A", "B", "C", "D"]
items.removeLast();                   // Removes "D"

// Constant-time reversed view (not a copy!)
for (String s : items.reversed()) {
    IO.print(s + " ");                    // C B A Z
}

2. Top Map Idioms for Online Assessments

Map<String, List<String>> graph = new HashMap<>();

// 1. Grouping / Adjacency List: computeIfAbsent
graph.computeIfAbsent("NodeA", k -> new ArrayList<>()).add("NodeB");

// 2. Word Frequency / Counter: merge (1 line!)
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
    counts.merge(word, 1, Integer::sum);
}

// 3. Fallback queries: getOrDefault
int currentRank = counts.getOrDefault("target", 0);

3. Sorting Mastery with Comparator

record Candidate(String name, int score, int yearsExp) {}

List<Candidate> list = getCandidates();

// Multi-level sort: score descending, then yearsExp descending, then name alphabetically
list.sort(Comparator
    .comparingInt(Candidate::score).reversed()
    .thenComparingInt(Candidate::yearsExp).reversed()
    .thenComparing(Candidate::name)
);

// Handling nulls safely
list.sort(Comparator.nullsLast(Comparator.comparing(Candidate::name)));

๐ŸŽฏ Module 04 Takeaways

  • Use SequencedCollection (getFirst(), getLast(), reversed()) instead of index arithmetic.
  • Use map.merge(key, 1, Integer::sum) for instantaneous frequency tables.
  • Use ArrayDeque for both Stacks and Queues; explain to your interviewer why Stack is obsolete.
  • Use Comparator.comparingInt() to prevent integer subtraction overflow bugs.
Knowledge Check 04A โ€ข Comparator Overflow Trap

Why is (a, b) -> a.val - b.val dangerous as a custom comparator in an online assessment?

Knowledge Check 04B โ€ข Map.computeIfAbsent Semantics

In map.computeIfAbsent(key, k -> computeVal(k)), what occurs if computeVal(k) returns null?

Module 05 Functional Pipelines

Streams & Stream Gatherers (Java 22โ€“25)

Stream Gatherers (introduced in Java 22, finalized in Java 25 / JEP 485) allow custom, intermediate stateful operations without dropping out of stream pipelines.

1. Essential Modern Collectors & teeing()

record Transaction(String user, String currency, double amount) {}

List<Transaction> txs = getTransactions();

// Teeing: compute two independent reductions in a single pass (Java 12+)
record Stats(double sum, long count, double average) {}

Stats stats = txs.stream()
    .collect(Collectors.teeing(
        Collectors.summingDouble(Transaction::amount),
        Collectors.counting(),
        (sum, count) -> new Stats(sum, count, count == 0 ? 0 : sum / count)
    ));

2. Stream Gatherers in Java 25

A. Chunking Batches with Gatherers.windowFixed(n)

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);

List<List<Integer>> batches = numbers.stream()
    .gather(Gatherers.windowFixed(3))
    .toList();
// Output: [[1, 2, 3], [4, 5, 6], [7, 8]]

B. Sliding Windows with Gatherers.windowSliding(k)

// Moving average calculation of window size 3
List<Double> prices = List.of(10.0, 12.0, 14.0, 16.0, 18.0);

List<Double> movingAverages = prices.stream()
    .gather(Gatherers.windowSliding(3))
    .map(window -> window.stream().mapToDouble(Double::doubleValue).average().orElse(0.0))
    .toList();
// Windows: [10, 12, 14] -> 12.0; [12, 14, 16] -> 14.0; [14, 16, 18] -> 16.0

C. Cumulative Running Sum with Gatherers.scan()

// Prefix sums without mutable loops!
List<Integer> values = List.of(3, 1, 4, 1, 5);

List<Integer> prefixSums = values.stream()
    .gather(Gatherers.scan(() -> 0, Integer::sum))
    .toList();
// Output: [3, 4, 8, 9, 14]

๐ŸŽฏ Module 05 Takeaways

  • Use Collectors.teeing() to produce dual statistics (e.g. min + max) in a single pass.
  • Stream Gatherers allow intermediate stateful operations without dropping out of the stream.
  • Know Gatherers.windowFixed(), Gatherers.windowSliding(), and Gatherers.scan().
Knowledge Check 05A โ€ข Stream Gatherers Architecture

What problem did Stream Gatherers specifically solve in the Java Stream API?

Knowledge Check 05B โ€ข Sliding Window Stream Calculation

If a stream of 10 elements is passed to .gather(Gatherers.windowSliding(3)), how many list elements are emitted?

Module 06 Modern Concurrency

Concurrency: Virtual Threads & Structured Concurrency

Java 21 introduced Virtual Threads (Project Loom), while Java 21โ€“25 matures Structured Concurrency and Scoped Values.

1. Virtual Threads vs Platform Threads

A traditional Platform Thread is an OS thread wrapper (~1MB stack, costly context switch). A Virtual Thread is a lightweight JVM-managed thread (~1KB heap). The JVM mounts virtual threads onto OS carrier threads and unmounts them whenever they perform blocking I/O.

// Spawning 100,000 concurrent tasks effortlessly!
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100_000; i++) {
        final int taskId = i;
        executor.submit(() -> {
            Thread.sleep(Duration.ofMillis(100)); // Unmounts from carrier thread!
            return taskId * 2;
        });
    }
} // Auto-close joins and waits for all tasks!
โš ๏ธ Golden Rules for Virtual Threads
  1. NEVER POOL VIRTUAL THREADS! Create them per request.
  2. Avoid carrier thread pinning: Replace synchronized with ReentrantLock in high-throughput I/O pathways!
  3. Use for I/O bound tasks, NOT CPU-bound tasks.

2. Structured Concurrency (Java 21โ€“25)

record UserDashboard(User profile, List<Order> orders) {}

UserDashboard fetchDashboard(long userId) throws Exception {
    try (var scope = StructuredTaskScope.open(Joiner.<Object>awaitAllSuccessfulOrThrow())) {
        Subtask<User> userSubtask = scope.fork(() -> fetchUserProfile(userId));
        Subtask<List<Order>> ordersSubtask = scope.fork(() -> fetchRecentOrders(userId));

        scope.join(); // Propagates first failure, cancelling siblings!

        return new UserDashboard(userSubtask.get(), ordersSubtask.get());
    }
}

3. Scoped Values (Java 21โ€“25)

public static final ScopedValue<SecurityContext> CURRENT_USER = ScopedValue.newInstance();

void handleRequest(Request req) {
    SecurityContext ctx = authenticate(req);
    ScopedValue.where(CURRENT_USER, ctx).run(() -> {
        processOrder(); // Downstream methods call CURRENT_USER.get() with zero memory leak risk!
    });
}

๐ŸŽฏ Module 06 Takeaways

  • Platform threads are scarce (~1MB); virtual threads are abundant (~1KB).
  • Never pool virtual threads; use Executors.newVirtualThreadPerTaskExecutor().
  • Use ReentrantLock instead of synchronized in I/O code to prevent thread pinning.
  • Use StructuredTaskScope for parent-child task cancellation and error propagation.
Knowledge Check 06A โ€ข Loom Thread Pinning

What happens if a Virtual Thread blocks inside a synchronized block during a database query?

Knowledge Check 06B โ€ข Virtual Thread Pooling Anti-Pattern

Why is wrapping Virtual Threads in a fixed-size pool (e.g. Executors.newFixedThreadPool(200)) an anti-pattern?

Module 07 Architecture & Clean Code

OOP & Modern Design Patterns

Modern Java replaces heavy Gang of Four (GoF) boilerplate with native language features.

1. Strategy Pattern -> Lambdas & Method References

public class PricingService {
    public double calculateTotal(double basePrice, UnaryOperator<Double> discountStrategy) {
        return discountStrategy.apply(basePrice);
    }
}

Call sites pass lambda strategies or method references without boilerplate classes:

// Usage:
service.calculateTotal(100.0, price -> price * 0.85); // Inline lambda
service.calculateTotal(100.0, PricingStrategies::holidayDiscount); // Method reference

2. Visitor Pattern -> Pattern Matching on Sealed Hierarchies

sealed interface Expr permits Constant, Add, Multiply {}
record Constant(double val) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Multiply(Expr left, Expr right) implements Expr {}

double eval(Expr expr) {
    return switch (expr) {
        case Constant(var val)          -> val;
        case Add(var left, var right)      -> eval(left) + eval(right);
        case Multiply(var left, var right) -> eval(left) * eval(right);
    };
}

3. Thread-Safe Singleton: The Correct Idioms

Option A: Enum Singleton (Effective Java Item 3)

public enum CacheManager {
    INSTANCE;
    public void put(String k, Object v) { /* ... */ }
}

Option B: Initialization-on-demand Holder Idiom (lazy, zero synchronization)

public class DatabaseClient {
    private DatabaseClient() {}

    private static class Holder {
        private static final DatabaseClient INSTANCE = new DatabaseClient();
    }

    public static DatabaseClient getInstance() {
        return Holder.INSTANCE; // ClassLoader guarantees thread-safety
    }
}

๐ŸŽฏ Module 07 Takeaways

  • Replace heavyweight Strategy classes with standard functional interfaces.
  • Replace the Visitor pattern with pattern matching on sealed hierarchies.
  • Use the Initialization-on-Demand Holder idiom or Enum singleton instead of manual double-checked locking.
Knowledge Check 07A โ€ข Lazy Initialization Holder Idiom

Why is the Initialization-on-Demand Holder idiom considered superior to double-checked locking?

Knowledge Check 07B โ€ข Modern Visitor Pattern vs Sealed Hierarchies

How does replacing the traditional GoF Visitor pattern with pattern matching on a sealed hierarchy improve code design?

Module 08 Robustness & Failure Modes

Exception Handling, Resource Management & Failure Modeling

How an engineer handles failures distinguishes juniors from staff engineers. Modern Java shifts away from checked exception pollution toward clean resource scopes and functional failure modeling.

1. Checked vs Unchecked Exceptions: The Modern Consensus

Java is the only mainstream language with checked exceptions. While designed with noble intent, checked exceptions compose terribly with functional pipelines (lambdas cannot throw checked exceptions without ugly wrapping) and leak implementation details.

  • Checked Exceptions: Use strictly for recoverable external conditions where the caller must provide an alternative path (e.g. FileNotFoundException).
  • Unchecked (RuntimeException): Standard for programming bugs, invariant violations, and domain errors (e.g. IllegalArgumentException, IllegalStateException).

2. Advanced try-with-resources & Suppressed Exceptions

try-with-resources ensures resources implementing AutoCloseable are cleanly closed in reverse order of declaration. If an exception occurs in both the try block AND the close() call, Java does not lose the second exception: it attaches it as a suppressed exception:

try (var in = new FileInputStream("data.txt");
     var out = new FileOutputStream("out.txt")) {
    in.transferTo(out);
} catch (IOException e) {
    IO.println("Primary error: " + e.getMessage());
    for (Throwable suppressed : e.getSuppressed()) {
        IO.println("Suppressed during close: " + suppressed.getMessage());
    }
}

3. Modern Failure Modeling: The Sealed Result<T, E> Pattern

In high-throughput services (and trading/fintech interviews), creating exception instances is expensive because the JVM captures the full execution stack trace via Throwable.fillInStackTrace(). Modern Java models expected failures cleanly with a sealed record hierarchy:

// Zero stack-trace overhead; expressive domain failure modeling
public sealed interface Result<T, E> permits Result.Success, Result.Failure {
    record Success<T, E>(T value) implements Result<T, E> {}
    record Failure<T, E>(E error) implements Result<T, E> {}

    default boolean isSuccess() { return this instanceof Success; }
}

Caller pattern-matches without try/catch:

Result<User, String> res = findUser("alice@corp.com");
String message = switch (res) {
    case Result.Success(var user) -> "Found user: " + user.name();
    case Result.Failure(var err)  -> "Lookup failed: " + err;
};

๐ŸŽฏ Module 08 Takeaways

  • Prefer unchecked exceptions for modern domain services and stream compatibility.
  • Always inspect e.getSuppressed() when debugging resource closure issues.
  • Never return null when an Optional or Result<T, E> expresses absent or failed results.
  • Never use Optional as a method parameter, field, or map key.
Knowledge Check 08A โ€ข Stack Trace Overhead

Why is instantiating exceptions in tight loops considered an anti-pattern for high-throughput Java systems?

Knowledge Check 08B โ€ข Optional Design Idioms

Why is using Optional<T> as a class field or method parameter discouraged in Java?

Module 09 JVM Architecture & Runtimes

JVM Internals, Memory Model & Modern Garbage Collection

Senior and Staff engineering interviews routinely probe JVM memory layout, the Java Memory Model (JMM), and modern Garbage Collection strategies.

1. JVM Memory Areas & Object Layout

๐Ÿ–ฅ๏ธ JVM Runtime Memory Architecture HotSpot 64-bit Architecture
Heap Memory (Garbage Collected) Shared Across All Threads โ€ข Managed by GC
Young Generation
Eden Space Survivor S0 Survivor S1

All new object allocations occur in Eden. Minor GCs copy surviving objects between S0 and S1 before promoting to Tenured.

Old Generation (Tenured)
Tenured Space Compact Headers (Java 24/25)

Holds long-lived domain objects surviving tenuring threshold. Collected via concurrent mark-sweep phases in G1 or colored pointers in ZGC.

Metaspace & Native Shared โ€ข Off-Heap
  • Class Metadata: Method bytecodes, field definitions, constant pool (native OS RAM)
  • Code Cache: JIT-compiled C1/C2 native machine instructions
  • Off-Heap Native: DirectByteBuffers & FFM Arena memory
Thread Stacks Per Thread โ€ข Isolated
  • Stack Frames: Pushed/popped per method call
  • Local Primitives: int, long, double stored directly on stack
  • References: 64-bit pointers pointing to objects on Heap

2. Compact Object Headers (Project Lilliput / Java 24โ€“25 / JEP 450)

Every object in HotSpot has an object header containing the Mark Word (locking state, hash code, GC age) and the Klass Word (pointer to class metadata). On 64-bit systems, headers previously consumed 12 to 16 bytes.

In Java 24โ€“25 (JEP 450), Compact Object Headers shrink the header down to just 8 bytes (64 bits). This reduces live heap footprint by 10% to 20% in production, dramatically improving CPU cache locality and memory density.

3. The Java Memory Model (JMM) & happens-before

Modern multi-core CPUs reorder instructions and cache memory in L1/L2/L3 caches. Without memory barriers, thread B may see thread A's writes in a completely scrambled order!

// The happens-before guarantee guarantees visibility and ordering:
// 1. volatile write happens-before subsequent volatile read of same variable.
// 2. Unlocking a monitor happens-before subsequent lock of same monitor.
// 3. Thread.start() happens-before any action in the started thread.
// 4. Thread termination happens-before Thread.join() returns.

4. Modern Garbage Collection: G1 vs Generational ZGC

GC AlgorithmPause Time (Latency)Heap ScalabilityBest Fit / Production Use Case
G1 GC (Default)10ms โ€“ 100ms (predictable goal)4GB โ€“ 64GBGeneral-purpose services where high throughput is prioritized over sub-millisecond tail latency.
Generational ZGC (Java 21+ / JEP 439)< 1 millisecond max pause!16MB up to 16 TerabytesUltra-low-latency APIs, financial order matching, gaming backends, large in-memory caches. Uses colored pointers and load barriers.
Parallel GCHigh (Stop-the-World pauses)Any sizeOffline batch computing, scientific data crunching, background ETL pipelines.

๐ŸŽฏ Module 09 Takeaways

  • HotSpot heap objects now feature Compact Object Headers (8 bytes in Java 24/25).
  • volatile provides two guarantees: visibility (flushing cache lines) and ordering (preventing instruction reordering).
  • Choose Generational ZGC (-XX:+UseZGC -XX:+ZGenerational) when tail latency must stay under 1ms.
Knowledge Check 09A โ€ข Generational ZGC Concurrency

How does Generational ZGC achieve sub-millisecond pause times on multi-terabyte heaps?

Knowledge Check 09B โ€ข Volatile vs Atomic Operations

Given private volatile int count = 0;, why is count++ thread-unsafe?

Module 10 Next-Gen Runtime

Low-Level & Next-Gen Java: FFM API, Valhalla & Class-File API

Java 21โ€“25 modernizes low-level programming, replacing unsafe legacy hacks with clean, safe, high-performance APIs.

1. Foreign Function & Memory (FFM) API (Java 22+ / JEP 454)

For decades, calling C libraries or managing off-heap native memory required either brittle JNI or dangerous sun.misc.Unsafe. The Foreign Function & Memory (FFM) API provides type-safe, bounds-checked native interop:

// Off-heap native memory allocation with automatic lifecycle management
try (Arena arena = Arena.ofConfined()) {
    // Allocate 1,024 bytes off-heap outside JVM GC management
    MemorySegment segment = arena.allocate(1024);

    // Write a 64-bit long at offset 0
    segment.set(ValueLayout.JAVA_LONG, 0, 42_000_000L);

    long val = segment.get(ValueLayout.JAVA_LONG, 0);
    IO.println("Native value: " + val);
} // Arena closed: native memory deallocated instantly with zero memory leaks!

2. Project Valhalla & Value Objects (Java 23โ€“25 preview)

In Java, "everything has identity" (identity hash code, synchronization monitor, mutability). This causes heavy cache misses because an array of Point objects is an array of 64-bit pointers pointing to fragmented objects scattered across memory.

Project Valhalla introduces Value Classes (value class): objects without identity. The JVM flattens value objects directly into array memory:

Legacy Java Objects โŒ Pointer Indirection
[Ptr 0] โ”€โ”€> [Header (16B) | x=1 | y=2] (Addr: 0x10A) [Ptr 1] โ”€โ”€> [Header (16B) | x=3 | y=4] (Addr: 0x48F) [Ptr 2] โ”€โ”€> [Header (16B) | x=5 | y=6] (Addr: 0x932)

Array contains pointers pointing to distant heap objects. Every access requires a pointer dereference, causing CPU L1/L2 cache misses.

Valhalla Value Classes โœ… Flattened Contiguous Memory
[ x: 1 | y: 2 ][ x: 3 | y: 4 ][ x: 5 | y: 6 ] // Zero Headers โ€ข Zero Pointers โ€ข 100% Cache Friendly

Array directly holds the primitive values in-place without object headers or heap pointer chasing ("Codes like a class, works like an int").

3. Class-File API (Java 24โ€“25 / JEP 457, JEP 466)

Frameworks (Spring, Hibernate, Mockito) historically depended on external ASM or ByteBuddy libraries to parse and generate bytecode. The Class-File API bakes a standardized, high-level bytecode inspection and transformation model directly into the standard JDK.

๐ŸŽฏ Module 10 Takeaways

  • FFM API (Arena, MemorySegment) replaces Unsafe and JNI for safe off-heap native memory.
  • Valhalla Value Objects eliminate identity locks and headers, enabling flat contiguous arrays ("Codes like a class, works like an int").
  • The Class-File API eliminates third-party dependencies for bytecode generation.
Knowledge Check 10A โ€ข Project Valhalla Value Objects

What is the primary memory advantage of a Project Valhalla Value Class over a standard class?

Knowledge Check 10B โ€ข FFM Arena & Memory Safety

In the Foreign Function & Memory (FFM) API, what guarantee does Arena.ofConfined() provide?

Module 11 Assessment Solutions

End-to-End Online Assessment (OA) Problem Walkthroughs

Let's implement the 4 most frequently tested assessment patterns using clean, idiomatic Java 25.

Pattern 1: LRU Cache (Least Recently Used)

Design / Data Structures Time: O(1) | Space: O(Capacity)

Using modern Java 21+ SequencedMap / LinkedHashMap, you can implement an entire O(1) LRU Cache in under 20 lines without error-prone doubly linked list pointer updates!

public class LRUCache<K, V> {
    private final int capacity;
    // accessOrder = true: SequencedMap orders by last access!
    private final LinkedHashMap<K, V> map;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.map = new LinkedHashMap<>(capacity, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                return size() > capacity; // Auto-evicts oldest when capacity exceeded
            }
        };
    }

    public synchronized V get(K key) {
        return map.getOrDefault(key, null);
    }

    public synchronized void put(K key, V value) {
        map.put(key, value);
    }
}

Pattern 2: Monotonic Stack (Next Greater Element)

Monotonic Stack Time: O(N) | Space: O(N)

Given temperatures or prices, find the number of days until a warmer day. We use ArrayDeque (never Stack):

public int[] dailyTemperatures(int[] temperatures) {
    int n = temperatures.length;
    int[] result = new int[n];
    // ArrayDeque used as LIFO stack of indices
    Deque<Integer> stack = new ArrayDeque<>();

    for (int i = 0; i < n; i++) {
        while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
            int prevIdx = stack.pop();
            result[prevIdx] = i - prevIdx;
        }
        stack.push(i);
    }
    return result;
}

Pattern 3: Top K Frequent Elements

Heap / Frequency Map Time: O(N log K) | Space: O(N)
public int[] topKFrequent(int[] nums, int k) {
    Map<Integer, Integer> countMap = new HashMap<>();
    for (int num : nums) {
        countMap.merge(num, 1, Integer::sum); // Idiomatic 1-line counter
    }

    // Min-Heap of map entries keeping the top K elements
    PriorityQueue<Map.Entry<Integer, Integer>> minHeap =
        new PriorityQueue<>(Comparator.comparingInt(Map.Entry::getValue));

    for (var entry : countMap.entrySet()) {
        minHeap.offer(entry);
        if (minHeap.size() > k) {
            minHeap.poll(); // Evict smallest frequency
        }
    }

    int[] topK = new int[k];
    for (int i = 0; i < k; i++) {
        topK[i] = minHeap.poll().getKey();
    }
    return topK;
}

Pattern 4: Prefix Tree (Trie) with Fast Search

Tree / String Search Insert: O(L) | Search: O(L)
public class Trie {
    private static class Node {
        final Node[] children = new Node[26];
        boolean isEndOfWord = false;
    }

    private final Node root = new Node();

    public void insert(String word) {
        Node curr = root;
        for (int i = 0; i < word.length(); i++) {
            int idx = word.charAt(i) - 'a';
            if (curr.children[idx] == null) {
                curr.children[idx] = new Node();
            }
            curr = curr.children[idx];
        }
        curr.isEndOfWord = true;
    }

    public boolean startsWith(String prefix) {
        Node curr = root;
        for (int i = 0; i < prefix.length(); i++) {
            int idx = prefix.charAt(i) - 'a';
            if (curr.children[idx] == null) return false;
            curr = curr.children[idx];
        }
        return true;
    }
}

Pattern 5: Subarray Sum Equals K (Prefix Sums + Hash Map)

Prefix Sums / Hashing Time: O(N) | Space: O(N)

Find the total number of continuous subarrays whose sum equals k. Essential insight: If prefixSum[j] - prefixSum[i] = k, then subarray (i, j] sums to k.

public int subarraySum(int[] nums, int k) {
    int count = 0;
    int currentSum = 0;
    // Map stores: prefixSum -> frequency of occurrence
    Map<Integer, Integer> prefixFreq = new HashMap<>();
    prefixFreq.put(0, 1); // CRITICAL: Base case for subarrays starting at index 0!

    for (int num : nums) {
        currentSum += num;
        // Check if there is an earlier prefix sum such that: currentSum - target = k
        int target = currentSum - k;
        count += prefixFreq.getOrDefault(target, 0);

        // Record current prefix sum into map
        prefixFreq.merge(currentSum, 1, Integer::sum);
    }
    return count;
}

Pattern 6: Course Schedule / Dependency Graph (Kahn's BFS)

Graph / Topological Sort Time: O(V + E) | Space: O(V + E)

Determine if you can finish all courses given prerequisite pairs. Detects cycles in Directed Acyclic Graphs (DAG).

public boolean canFinish(int numCourses, int[][] prerequisites) {
    int[] inDegree = new int[numCourses];
    List<List<Integer>> adj = new ArrayList<>(numCourses);
    for (int i = 0; i < numCourses; i++) adj.add(new ArrayList<>());

    for (int[] edge : prerequisites) {
        int course = edge[0];
        int prereq = edge[1];
        adj.get(prereq).add(course);
        inDegree[course]++;
    }

    // Queue of courses with 0 prerequisites
    Deque<Integer> queue = new ArrayDeque<>();
    for (int i = 0; i < numCourses; i++) {
        if (inDegree[i] == 0) queue.offer(i);
    }

    int visitedCourses = 0;
    while (!queue.isEmpty()) {
        int curr = queue.poll();
        visitedCourses++;

        for (int neighbor : adj.get(curr)) {
            if (--inDegree[neighbor] == 0) {
                queue.offer(neighbor);
            }
        }
    }

    // If visited == total, no cycles exist!
    return visitedCourses == numCourses;
}

Pattern 7: Merge Intervals (Greedy Sort + In-Place Reduction)

Intervals / Sorting Time: O(N log N) | Space: O(N)
public int[][] merge(int[][] intervals) {
    if (intervals.length <= 1) return intervals;

    // Modern comparator sorting by interval start time
    Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));

    List<int[]> merged = new ArrayList<>();
    int[] current = intervals[0];
    merged.add(current);

    for (int i = 1; i < intervals.length; i++) {
        int[] next = intervals[i];
        if (next[0] <= current[1]) {
            // Overlapping intervals: greedily expand end boundary
            current[1] = Math.max(current[1], next[1]);
        } else {
            // Disjoint interval: move to next
            current = next;
            merged.add(current);
        }
    }
    return merged.toArray(int[][]::new);
}

Pattern 8: Concurrent Token Bucket Rate Limiter

System Design / Concurrency Time: O(1) | Space: O(1)

Thread-safe token bucket rate limiter without blocking locks, designed for high-concurrency Virtual Thread workloads.

public class TokenBucketRateLimiter {
    private final long capacity;
    private final double refillTokensPerNano;
    private final AtomicLong availableTokens;
    private final AtomicLong lastRefillNano;

    public TokenBucketRateLimiter(long capacity, double tokensPerSecond) {
        this.capacity = capacity;
        this.refillTokensPerNano = tokensPerSecond / 1_000_000_000.0;
        this.availableTokens = new AtomicLong(capacity);
        this.lastRefillNano = new AtomicLong(System.nanoTime());
    }

    public boolean tryAcquire() {
        refill();
        long current;
        do {
            current = availableTokens.get();
            if (current <= 0) return false;
        } while (!availableTokens.compareAndSet(current, current - 1));
        return true;
    }

    private void refill() {
        long now = System.nanoTime();
        long last = lastRefillNano.get();
        long elapsedNanos = now - last;
        if (elapsedNanos <= 0) return;

        long newTokens = (long) (elapsedNanos * refillTokensPerNano);
        if (newTokens > 0 && lastRefillNano.compareAndSet(last, now)) {
            availableTokens.updateAndGet(curr -> Math.min(capacity, curr + newTokens));
        }
    }
}

Interactive "Spot The Bug" OA Drills (CodeSignal / Karat / Amazon Style)

In technical assessments, you are often given code that compiles but fails edge-case tests. Identify the subtle bug in each snippet below:

๐Ÿ› Bug Spotting Drill 01

The List<Integer>.remove() Overload Trap

Examine the code snippet below. Why does it fail when attempting to remove the number 2?

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4));
int valToRemove = 2;
numbers.remove(valToRemove);
// Expectation: [1, 3, 4]
// Actual Result: IndexOutOfBoundsException or [1, 2, 4]!
๐Ÿ› Bug Spotting Drill 02

Binary Search Infinite Loop

Why does this standard binary search enter an infinite loop when searching for an element not present?

int binarySearch(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = (left + right) / 2;
        if (nums[mid] == target) return mid;
        if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid; // <-- Look closely here!
        }
    }
    return -1;
}
๐Ÿ› Bug Spotting Drill 03

Custom Comparator Contract Crash

Why does this comparator crash with IllegalArgumentException: Comparison method violates its general contract on large production inputs?

record Stock(String symbol, double price) {}

List<Stock> stocks = getStocks();
stocks.sort((a, b) -> (int) (a.price() - b.price())); // <-- Bug here!
๐Ÿ› Bug Spotting Drill 04

Subarray Sum Equals K Missing Base State

What fails if an engineer writes Subarray Sum Equals K without initializing prefixFreq.put(0, 1)?

๐ŸŽฏ Module 11 Takeaways

  • Use LinkedHashMap(cap, 0.75f, true) for instant O(1) LRU Caches.
  • Use ArrayDeque for monotonic stacks and queues.
  • Use map.merge() + Min-Heap PriorityQueue for Top-K problems.
  • Never cast double differences to int in comparators; use Double.compare().
  • Always remember prefixFreq.put(0, 1) in prefix sum subarray problems.
Knowledge Check 11 โ€ข Min-Heap vs Max-Heap Complexity

Why is maintaining a Min-Heap of size K better than a Max-Heap of size N when finding Top K Frequent Elements?

Module 12 Interview & OA Playbook

The Online Assessment (OA) Playbook & Traps

In automated online assessments (HackerRank, CodeSignal, LeetCode), hidden test cases specifically target Java edge cases: integer overflow, autoboxing identity, negative modulo, and I/O time-limit-exceeded (TLE).

1. High-Speed Fast I/O Template (Prevent TLE)

java.util.Scanner is slow because it uses regex parsing under the hood. If an assessment problem inputs 100,000 integers, Scanner will cause a Time Limit Exceeded (TLE). Use BufferedReader and StringTokenizer:

import module java.base;

public class FastIO {
    static class FastReader {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;

        String next() {
            while (st == null || !st.hasMoreTokens()) {
                try {
                    String line = br.readLine();
                    if (line == null) return null;
                    st = new StringTokenizer(line);
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            }
            return st.nextToken();
        }

        int nextInt() { return Integer.parseInt(next()); }
        long nextLong() { return Long.parseLong(next()); }
    }

    public static void main(String[] args) {
        FastReader in = new FastReader();
        // Read fast inputs without TLE
    }
}

2. The Top 7 OA Traps That Fail Hidden Tests

Trap 1: The Integer Cache Trap (-128 to 127)

Integer a = 127, b = 127;
IO.println(a == b);      // true (cached flyweights)

Integer c = 128, d = 128;
IO.println(c == d);      // FALSE! Distinct heap instances!
IO.println(c.equals(d)); // true: ALWAYS use .equals() for wrapper comparisons!

Trap 2: Binary Search Midpoint Overflow

// WRONG: (low + high) can overflow positive 32-bit int if both are > 1 billion
int mid = (low + high) / 2;

// CORRECT: subtractive midpoint or bitwise shift
int mid = low + (high - low) / 2;
// OR:
int mid = (low + high) >>> 1; // Unsigned right shift preserves sign bit

Trap 3: Negative Modulo Arithmetic

// WRONG: -1 % 5 == -1 in Java!
int index = val % k;

// CORRECT: wrap-around positive modulo
int index = (val % k + k) % k;
// OR:
int index = Math.floorMod(val, k);

Trap 4: Arrays.asList() vs List.of()

Integer[] arr = {1, 2, 3};
List<Integer> list1 = Arrays.asList(arr);
// list1.add(4); // THROWS UnsupportedOperationException! Fixed-size list backed by array.
list1.set(0, 99); // MUTATES original array arr[0]!

List<Integer> list2 = List.of(1, 2, 3);
// list2.set(0, 99); // THROWS UnsupportedOperationException! Truly unmodifiable.

Trap 5: Mutable Keys in HashMaps

If you mutate an object after putting it as a key in a HashMap, its hashCode() changes, making it unfindable in the bucket table. Always use immutable record types as map keys!

Trap 6: Floating Point Precision in Decimals

IO.println(0.1 + 0.2); // Prints 0.30000000000000004!

BigDecimal price1 = new BigDecimal("0.1"); // Always construct from String, not double!
BigDecimal price2 = new BigDecimal("0.2");
IO.println(price1.add(price2)); // Prints 0.3 exact

Trap 7: Array Covariance Runtime Trap

Object[] arr = new String[5]; // Compiles because Java arrays are covariant!
arr[0] = 123; // Compiles, but THROWS ArrayStoreException at runtime!

๐ŸŽฏ Module 12 Takeaways

  • Use BufferedReader + StringTokenizer to avoid TLE in competitive programming.
  • Never compare wrapper objects (Integer, Long) with ==.
  • Calculate binary search midpoints with low + (high - low) / 2.
  • Use Math.floorMod(val, k) for circular array indices.
  • Always make HashMap keys immutable records.
Knowledge Check 12A โ€ข Integer Cache Identity Trap

What is the output of running (new Integer(100) == new Integer(100)) versus (Integer.valueOf(100) == Integer.valueOf(100))?

Knowledge Check 12B โ€ข Modulo Arithmetic & Circular Arrays

In cyclic array problems, why is (index - k) % N dangerous when moving backwards, and what is the fix?

Module 13 Enterprise Architecture & Migration

Modernising Legacy Java (Java 8 to Java 25 Migration Playbook)

Migrating legacy enterprise systems (Java 8, 11, or 17) to Java 21/25 is one of the most common real-world initiatives. Interviewers frequently ask: "How would you migrate a legacy Java 8 service to modern Java 25 without breaking production?"

1. The 4-Phase Migration Roadmap

Phase 1 Runtime Upgrade

Run on JDK 25 First

Execute existing Java 8/11 bytecode on modern JDK 25 without recompiling. Instant wins: modern G1/ZGC garbage collectors, improved JIT optimizations, and container awareness (cgroup v2).

Phase 2 Build & Dependencies

Upgrade Toolchain

Upgrade build tools (Maven 3.9+, Gradle 8.10+). Upgrade byte-manipulating dependencies (ByteBuddy, Lombok, Mockito, Spring Boot 3.4+) to support modern class-file bytecodes.

Phase 3 Fix Removed APIs

Resolve Encapsulation

Address removed JDK packages (JAXB, CORBA, Nashorn). Replace illegal reflective access into JDK internals (sun.misc.*) with standard modern APIs like FFM and VarHandles.

Phase 4 Idiomatic Refactoring

Modern Code Patterns

Refactor verbose POJOs into records, complex class hierarchies into sealed types, multi-line strings into text blocks, and thread pools into Virtual Threads.

2. The Top 4 Modernization Refactoring Patterns

Pattern 1: Verbose POJO/DTO -> Record

Legacy Java 8 (POJO) โŒ 60+ Lines Boilerplate
public class CustomerDTO {
    private final String id;
    private final String email;

    public CustomerDTO(String id, String email) {
        this.id = id;
        this.email = email;
    }
    public String getId() { return id; }
    public String getEmail() { return email; }
    // equals(), hashCode(), toString()...
}
Modern Java 25 โœ… 1 Line (Records)
// Compiler auto-generates components,
// accessors, equals(), hashCode(), and toString()
public record CustomerDTO(String id, String email) {
    public CustomerDTO {
        Objects.requireNonNull(id, "id must not be null");
    }
}

Pattern 2: Multi-line String Concatenation -> Text Blocks (Java 15+)

Legacy Java 8 โŒ Ugly Escapes & Concats
String query = "SELECT id, name, email\n"
    + "FROM customers\n"
    + "WHERE active = 1\n"
    + "ORDER BY name ASC;";
Modern Java 25 โœ… Native Text Blocks
String query = """
    SELECT id, name, email
    FROM customers
    WHERE active = 1
    ORDER BY name ASC;
    """;

Pattern 3: Legacy Date/Time (`Date` / `SimpleDateFormat`) -> `java.time.*` (JSR-310)

SimpleDateFormat is notoriously NOT thread-safe. Concurrent calls in Java 8 routinely cause corrupted dates or race conditions. Always migrate to immutable java.time:

// WRONG (Java 8 trap): SimpleDateFormat shared across threads corrupts internal Calendar state!
// SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

// MODERN JAVA (Thread-safe, immutable, nanosecond precision):
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate date = LocalDate.parse("2026-10-01", formatter);
Instant timestamp = Instant.now();
ZonedDateTime utcTime = timestamp.atZone(ZoneId.of("UTC"));

Pattern 4: Fixed Thread Pools -> Virtual Threads

// Legacy: pool of 200 OS threads (risks thread starvation during I/O spikes)
// ExecutorService pool = Executors.newFixedThreadPool(200);

// Modern Java 21-25: Virtual threads per task (millions concurrent, non-exhausting)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> httpClient.send(request, HttpResponse.BodyHandlers.ofString()));
}

3. Migration Pitfalls & Breaking Changes

  • Encapsulation of JDK Internals: In Java 17+, internal APIs like sun.misc.* are strongly encapsulated. Avoid --add-opens hacks; migrate to standard APIs.
  • Default Charset UTF-8 (JEP 400): In Java 8, FileReader used the OS default charset (e.g. Windows-1252), causing corrupted characters when deploying to Linux containers. Java 18+ defaults to UTF-8 everywhere.
  • Removal of Finalizers: Object.finalize() is deprecated for removal. Use Cleaner or try-with-resources.
  • Primitive Wrapper Constructors: new Integer(10) and new Double(3.14) are deprecated for removal. Use Integer.valueOf(10) in preparation for Project Valhalla.

๐ŸŽฏ Module 13 Takeaways

  • Always run on the modern JDK before upgrading source code compilation.
  • Replace POJOs with records and SQL/JSON string concatenation with text blocks.
  • Never use java.util.Date or SimpleDateFormat; migrate to java.time.*.
  • Replace I/O thread pools with Executors.newVirtualThreadPerTaskExecutor().
Knowledge Check 13A โ€ข SimpleDateFormat Thread-Safety

Why is sharing a single instance of SimpleDateFormat across threads dangerous in legacy Java?

Knowledge Check 13B โ€ข Deprecated SecurityManager Migration

When upgrading an enterprise Java 8 codebase to Java 25, how should legacy code relying on SecurityManager be handled?

Module 14 Quality Engineering & Verification

Modern Testing: Unit, Integration & Virtual Threads

Testing in modern Java combines JUnit 5 (Jupiter), fluent assertions with AssertJ, real-world infrastructure testing via Testcontainers, and safe verification of Virtual Threads.

1. The Modern Test Architecture (The Testing Trophy)

Unit Tests

Pure Domain Logic

Blazing fast in-memory tests verifying record invariants, algorithmic functions, and data transformations. Zero mocks for records!

JUnit 5 โ€ข AssertJ
E2E Smoke Tests

Critical User Flows

Focused end-to-end smoke checks verifying full service integration, authentication, and live ingress.

RestAssured โ€ข Playwright

2. Modern Unit Testing with JUnit 5 & AssertJ

Modern tests use @Nested for hierarchical BDD structure and AssertJ for readable assertions:

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertAll;

@DisplayName("User Account Service Tests")
class UserServiceTest {

    private final UserService service = new UserService();

    @Nested
    @DisplayName("When registering a new user")
    class RegistrationTests {

        @Test
        @DisplayName("Should successfully create active user with normalized email")
        void shouldCreateActiveUser() {
            var request = new RegisterRequest("  ALICE@CORP.COM  ", "Secret123!");
            User user = service.register(request);

            // AssertJ: fluent, chained, readable failure messages
            assertThat(user)
                .isNotNull()
                .satisfies(u -> {
                    assertThat(u.email()).isEqualTo("alice@corp.com");
                    assertThat(u.isActive()).isTrue();
                    assertThat(u.createdAt()).isBeforeOrEqualTo(Instant.now());
                });
        }

        // Modern Parameterized Testing
        @ParameterizedTest(name = "Invalid email '{0}' should throw exception")
        @ValueSource(strings = {"", "   ", "missing-at-sign", "@nodomain.com"})
        void shouldRejectInvalidEmails(String invalidEmail) {
            assertThatThrownBy(() -> service.register(new RegisterRequest(invalidEmail, "Secret123!")))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("Invalid email");
        }
    }
}

3. Mocking Best Practices: What to Mock vs. What NOT to Mock

๐ŸŽฏ Interview Gold: Mockito Standards in Modern Java
  • DO NOT mock Records, Value Objects, or Collections: Records are lightweight, immutable, and have no side effects. Instantiate real records in tests! Mocking them adds fragile reflection overhead.
  • DO mock out-of-process boundaries: Third-party payment APIs (Stripe), email dispatchers, external SMS gateways.
  • Verify interactions sparingly: Prefer asserting state (method return values) over verifying internal method calls (verify(mock, times(1))) to prevent brittle tests that break on harmless refactoring.

4. Testing Concurrency & Virtual Threads Safely

Never use Thread.sleep() in unit or integration tests โ€” it causes flaky, slow CI builds. Use timeouts, latches, or structured scopes:

@Test
@Timeout(value = 3, unit = TimeUnit.SECONDS) // Fails test if virtual thread deadlocks!
@DisplayName("Should process 1,000 tasks concurrently on virtual threads")
void shouldProcessTasksConcurrently() throws Exception {
    var counter = new LongAdder();
    int taskCount = 1_000;

    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        List<Future<?>> futures = IntStream.range(0, taskCount)
            .mapToObj(i -> executor.submit(() -> {
                counter.increment();
            }))
            .toList();

        // Wait deterministically for all tasks
        for (var f : futures) {
            f.get(1, TimeUnit.SECONDS);
        }
    }

    assertThat(counter.sum()).isEqualTo(taskCount);
}

5. Real Integration Testing with Testcontainers

In-memory databases like H2 often hide production bugs because they do not support PostgreSQL-specific window functions, JSONB operations, or locking semantics. Testcontainers spins up real Docker containers managed directly by your JUnit 5 test lifecycle:

import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
class CustomerRepositoryIT {

    // Starts a real PostgreSQL container before tests run; tears it down when done
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @Test
    void shouldPersistAndRetrieveCustomerWithJsonb() {
        String jdbcUrl = postgres.getJdbcUrl();
        var dataSource = createDataSource(jdbcUrl, postgres.getUsername(), postgres.getPassword());
        var repo = new CustomerRepository(dataSource);

        repo.save(new Customer("c1", "Alice", "{\"tier\": \"gold\"}"));
        Customer found = repo.findById("c1").orElseThrow();

        assertThat(found.name()).isEqualTo("Alice");
    }
}

๐ŸŽฏ Module 14 Takeaways

  • Use AssertJ for fluent, expressive assertions that print descriptive diffs on failure.
  • Never mock immutable records; instantiate them directly.
  • Avoid Thread.sleep() in tests; use @Timeout and deterministic futures.
  • Use Testcontainers instead of H2 to test real database dialects and concurrency semantics.
Knowledge Check 14A โ€ข Testcontainers vs In-Memory H2

Why is testing database repositories with Testcontainers preferred over in-memory databases like H2?

Knowledge Check 14B โ€ข Testing Records & Mocking Anti-Patterns

In modern unit testing with Mockito and JUnit 5, why is mocking Java record instances an anti-pattern?

Module 15 Online Assessment Mastery

Cracking Online Coding Assessments (TestGorilla, LeetCode, HackerRank & CodeSignal)

Online Assessments (OAs) on platforms like TestGorilla, HackerRank, CodeSignal, Karat, and Codility are automated gatekeepers. Unlike live human interviews where communication and partial reasoning earn points, OAs grade binary test assertions against strict execution timeboxes (usually 1.0 to 2.0 seconds per test in Java) and tight memory caps (typically 256MB to 512MB).

1. The OA Grading Architecture & Scoring Tactics

Automated testing suites do not grade test cases equally. Understanding how tests are distributed is the difference between scoring 65% (rejection) and 100% (interview invite):

๐Ÿ“Š The 80/20 Test Distribution

Test suites typically contain: 2โ€“3 Public Sample Tests (10% score), 8โ€“12 Hidden Edge Cases (null, 0, 1, negatives, duplicates) (50% score), and 5โ€“8 Scale Stress Tests ($N = 10^5$, checking if $O(N)$ vs $O(N^2)$ TLEs) (40% score).

โš ๏ธ The Static State Trap

Platforms like TestGorilla, LeetCode, and HackerRank execute multiple test cases inside the same JVM process. Never use static mutable fields (e.g. static int counter = 0;); values persist across tests and cause random, inexplicable test failures!

โฑ๏ธ Silent TLE from Print Flushes

Leaving System.out.println() inside loops on scale test cases ($10^5$ iterations) will trigger a Time Limit Exceeded (TLE) purely due to terminal I/O buffer flushing, even if your algorithm is mathematically $O(N)$.

โšก The 10-Minute Triage Formula

If you cannot formulate the optimal $O(N)$ solution within 5 minutes, implement the $O(N^2)$ brute-force first! This secures the base 60% points on sample and functional tests before you attempt the optimal optimization.

2. The 10 Essential Online Assessment Question Archetypes

Over 90% of coding assessment questions map to one of ten core archetypes. Mastering the identification triggers, state invariants, and clean Java templates guarantees rapid solutions:

Archetype 01

Sliding Window (Dynamic Invariant)

Time: O(N) โ€ข Space: O(min(N, M))
Recognition Triggers: "contiguous subarray", "longest / shortest substring satisfying condition X", "at most K distinct elements".
Strategy: Maintain two pointers left and right. Expand right greedily. When the window invariant is violated, contract left until validity is restored. For ASCII strings, an array int[128] or int[256] is 5x faster than HashMap.
// Example: Longest Substring Without Repeating Characters (LeetCode 3)
public int lengthOfLongestSubstring(String s) {
    if (s == null || s.isEmpty()) return 0;
    int[] lastSeen = new int[128];
    Arrays.fill(lastSeen, -1);
    int maxLen = 0, left = 0;

    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        if (lastSeen[c] >= left) {
            left = lastSeen[c] + 1; // Fast-forward left past duplicate
        }
        lastSeen[c] = right;
        maxLen = Math.max(maxLen, right - left + 1);
    }
    return maxLen;
}
Hidden OA Trap: Forgetting to verify if lastSeen[c] >= left before moving left, which accidentally drags left backwards on characters seen in earlier, closed windows.
Archetype 02

Two Pointers (Opposite Direction Convergence)

Time: O(N) โ€ข Space: O(1)
Recognition Triggers: "sorted array pair", "palindrome verification", "trapping rainwater", "container with maximum capacity".
Strategy: Place left = 0 and right = N - 1. Compute candidate metric. Make a greedy decision to eliminate the side that can never contribute to a superior answer.
// Example: Container With Most Water (LeetCode 11)
public int maxArea(int[] height) {
    int left = 0, right = height.length - 1;
    int maxWater = 0;

    while (left < right) {
        int width = right - left;
        int minH = Math.min(height[left], height[right]);
        maxWater = Math.max(maxWater, width * minH);

        // Advance the pointer pointing to the shorter wall (bottleneck)
        if (height[left] < height[right]) {
            left++;
        } else {
            right--;
        }
    }
    return maxWater;
}
Hidden OA Trap: Incrementing/decrementing both pointers simultaneously when heights are equal without updating the max candidate first, skipping potential symmetric answers.
Archetype 03

Fast & Slow Pointers (Floyd's Tortoise & Hare)

Time: O(N) โ€ข Space: O(1)
Recognition Triggers: "detect cycle in linked list", "find middle node in one pass", "find duplicate number in array where values are 1..N".
Strategy: slow advances 1 step, fast advances 2 steps. In a cycle, fast will lap and meet slow. To find cycle entrance, reset slow to head and advance both by 1 step.
// Example: Linked List Cycle Detection (LeetCode 141)
public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) return false;
    ListNode slow = head;
    ListNode fast = head;

    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true; // Collision guarantees cycle
    }
    return false;
}
Hidden OA Trap: Checking while (fast.next != null) without first checking while (fast != null && fast.next != null), which triggers immediate NullPointerException on even-length or null lists.
Archetype 04

Monotonic Stack (Next Greater / Smaller Element)

Time: O(N) โ€ข Space: O(N)
Recognition Triggers: "next greater element", "daily temperatures", "largest rectangle in histogram", "stock span", "buildings with sunset view".
Strategy: Store indices (not values) in a stack maintained in strictly monotonic order. When a incoming element violates monotonicity, pop top elements and record the answer: the incoming element is their "next greater".
// Example: Daily Temperatures (LeetCode 739)
public int[] dailyTemperatures(int[] temperatures) {
    int n = temperatures.length;
    int[] result = new int[n];
    Deque<Integer> stack = new ArrayDeque<>(); // Monotonically decreasing indices

    for (int curr = 0; curr < n; curr++) {
        while (!stack.isEmpty() && temperatures[curr] > temperatures[stack.peek()]) {
            int prev = stack.pop();
            result[prev] = curr - prev; // Distance to next warmer day
        }
        stack.push(curr);
    }
    return result;
}
Hidden OA Trap: Storing values instead of indices in the stack, making it impossible to compute distances or look up corresponding array positions.
Archetype 05

Interval Merging & Sweep-Line Scheduling

Time: O(N log N) โ€ข Space: O(N)
Recognition Triggers: "meeting rooms", "overlapping time intervals", "minimum conference rooms needed", "calendar booking conflict".
Strategy: Sort intervals by start time using Integer.compare(a[0], b[0]). Use a Min-Heap (PriorityQueue) of active meeting end times. If the earliest meeting ends before current starts, reuse the room (poll).
// Example: Meeting Rooms II (LeetCode 253)
public int minMeetingRooms(int[][] intervals) {
    if (intervals == null || intervals.length == 0) return 0;
    // Sort meetings chronologically by start time
    Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));

    // Min-heap tracking earliest available room end times
    PriorityQueue<Integer> endTimes = new PriorityQueue<>();

    for (int[] meeting : intervals) {
        if (!endTimes.isEmpty() && endTimes.peek() <= meeting[0]) {
            endTimes.poll(); // Room is free, reuse it
        }
        endTimes.offer(meeting[1]); // Allocate / extend room with current meeting's end time
    }
    return endTimes.size(); // Peak concurrent rooms
}
Hidden OA Trap: Using (a, b) -> a[0] - b[0] which integer-overflows on intervals with large negative timestamps (e.g. historical timestamps or extreme Unix epoch offsets).
Archetype 06

Multi-Source BFS & Grid Level Traversal

Time: O(R ร— C) โ€ข Space: O(R ร— C)
Recognition Triggers: "shortest path in unweighted maze", "minutes until virus/fire spreads to entire grid", "distance of nearest zero".
Strategy: Enqueue all initial starting sources into an ArrayDeque at level 0. Process layer-by-layer with snapshot size int size = queue.size(). Mark visited immediately on enqueue to avoid exponential memory explosions.
// Example: Rotting Oranges (LeetCode 994)
public int orangesRotting(int[][] grid) {
    int rows = grid.length, cols = grid[0].length;
    Deque<int[]> queue = new ArrayDeque<>();
    int fresh = 0;

    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            if (grid[r][c] == 2) queue.offer(new int[]{r, c});
            else if (grid[r][c] == 1) fresh++;
        }
    }
    if (fresh == 0) return 0;

    int minutes = 0;
    int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    while (!queue.isEmpty() && fresh > 0) {
        int levelSize = queue.size();
        for (int i = 0; i < levelSize; i++) {
            int[] cell = queue.poll();
            for (int[] d : dirs) {
                int nr = cell[0] + d[0], nc = cell[1] + d[1];
                if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1) {
                    grid[nr][nc] = 2; // Mark rotten immediately!
                    fresh--;
                    queue.offer(new int[]{nr, nc});
                }
            }
        }
        minutes++;
    }
    return fresh == 0 ? minutes : -1;
}
Hidden OA Trap: Marking cells visited when polling from the queue instead of when pushing. Multiple neighbors can push the same unvisited cell, resulting in $O(4^N)$ duplicate queue allocations and Memory Limit Exceeded (MLE).
Archetype 07

Backtracking with State Pruning (DFS with Undo)

Time: O(2^T) โ€ข Space: O(T) call stack
Recognition Triggers: "generate all valid combinations / permutations", "word search on boggle board", "sudoku solver", "N-Queens".
Strategy: 1. Sort inputs first to enable early breaks. 2. Append choice to current path. 3. Recurse down. 4. Backtrack by removing last element (path.removeLast()).
// Example: Combination Sum with Early Prune (LeetCode 39)
public List<List<Integer>> combinationSum(int[] candidates, int target) {
    List<List<Integer>> result = new ArrayList<>();
    Arrays.sort(candidates); // Essential for prune break
    backtrack(candidates, target, 0, new ArrayList<>(), result);
    return result;
}

private void backtrack(int[] candidates, int remain, int start,
                       List<Integer> path, List<List<Integer>> result) {
    if (remain == 0) {
        result.add(new ArrayList<>(path)); // Snapshot valid result
        return;
    }
    for (int i = start; i < candidates.length; i++) {
        if (candidates[i] > remain) break; // Prune: array is sorted, subsequent elements also exceed!
        path.add(candidates[i]);
        backtrack(candidates, remain - candidates[i], i, path, result); // i allows reuse
        path.removeLast(); // Backtrack undo step (Java 21 Sequenced Collections)
    }
}
Hidden OA Trap: Adding result.add(path) directly without a copy constructor new ArrayList<>(path). Because path is mutated in-place, the result list ends up containing empty lists when recursion finishes!
Archetype 08

Binary Search on Monotonic Solution Space ("Minimax")

Time: O(N log(Range)) โ€ข Space: O(1)
Recognition Triggers: "minimum possible maximum", "capacity to ship within D days", "Koko eating bananas in H hours", "split array into K parts with minimal largest sum".
Strategy: Even if the input array is unsorted, the answer space (possible capacities/speeds) is monotonic: if capacity $C$ works, all capacities $> C$ also work. Binary search between low = max(weights) and high = sum(weights).
// Example: Capacity To Ship Packages Within D Days (LeetCode 1011)
public int shipWithinDays(int[] weights, int days) {
    int low = 0, high = 0;
    for (int w : weights) {
        low = Math.max(low, w); // Must at least fit heaviest single package
        high += w;              // Maximum possible capacity: ship all on day 1
    }

    int optimalCapacity = high;
    while (low <= high) {
        int mid = low + (high - low) / 2; // Midpoint without overflow
        if (canShip(weights, days, mid)) {
            optimalCapacity = mid;        // Feasible: search for smaller capacity
            high = mid - 1;
        } else {
            low = mid + 1;               // Infeasible: must increase capacity
        }
    }
    return optimalCapacity;
}

private boolean canShip(int[] weights, int maxDays, int capacity) {
    int daysNeeded = 1, currentLoad = 0;
    for (int w : weights) {
        if (currentLoad + w > capacity) {
            daysNeeded++;
            currentLoad = 0;
        }
        currentLoad += w;
    }
    return daysNeeded <= maxDays;
}
Hidden OA Trap: Setting low = 0 instead of low = max(weights). If capacity is less than the largest item, canShip() enters an infinite loop or records impossible splits.
Archetype 09

Dynamic Programming (1D Rolling Tabulation)

Time: O(Amount ร— Coins) โ€ข Space: O(Amount)
Recognition Triggers: "fewest coins to make amount", "maximum non-adjacent house robbery", "count ways to climb stairs", "partition into equal subset sums".
Strategy: Formulate subproblem $dp[i]$: the minimum coins to make amount $i$. Initialize table with sentinel amount + 1. Base case $dp[0] = 0$. Transition: $dp[i] = \min(dp[i], dp[i - c] + 1)$.
// Example: Coin Change (LeetCode 322)
public int coinChange(int[] coins, int amount) {
    if (amount < 1) return 0;
    int[] dp = new int[amount + 1];
    Arrays.fill(dp, amount + 1); // Sentinel value (higher than any valid answer)
    dp[0] = 0;

    for (int i = 1; i <= amount; i++) {
        for (int coin : coins) {
            if (coin <= i) {
                dp[i] = Math.min(dp[i], dp[i - coin] + 1);
            }
        }
    }
    return dp[amount] > amount ? -1 : dp[amount];
}
Hidden OA Trap: Initializing with Integer.MAX_VALUE; adding + 1 in dp[i - coin] + 1 causes 32-bit integer overflow into negative numbers (-2,147,483,648), destroying the Math.min() comparison.
Archetype 10

Prefix Tree (Trie & Dictionary Search)

Time: O(Word Length) โ€ข Space: O(ฮฃ ร— L ร— N)
Recognition Triggers: "prefix autocomplete", "check if string has prefix in dictionary", "wildcard regex search ('b.d')", "maximum XOR pair".
Strategy: Build an n-ary tree where each node holds 26 children (Node[] children = new Node[26]) and a boolean isEndOfWord. Inserting and querying takes $O(L)$ where $L$ is word length, completely independent of the number of words stored.
// Example: Implement Prefix Trie (LeetCode 208)
public final class Trie {
    private static final class Node {
        final Node[] children = new Node[26];
        boolean isEndOfWord = false;
    }

    private final Node root = new Node();

    public void insert(String word) {
        Node curr = root;
        for (int i = 0; i < word.length(); i++) {
            int idx = word.charAt(i) - 'a';
            if (curr.children[idx] == null) {
                curr.children[idx] = new Node();
            }
            curr = curr.children[idx];
        }
        curr.isEndOfWord = true;
    }

    public boolean startsWith(String prefix) {
        Node curr = root;
        for (int i = 0; i < prefix.length(); i++) {
            int idx = prefix.charAt(i) - 'a';
            if (curr.children[idx] == null) return false;
            curr = curr.children[idx];
        }
        return true;
    }
}
Hidden OA Trap: Using HashMap<Character, Node> for standard lowercase English words instead of fixed Node[26] arrays. In OAs with $10^5$ queries, the object allocation and hashing overhead results in TLE.

3. The Pre-Flight Defensive Edge-Case Checklist

Before clicking "Submit" or "Run Final Tests", mentally trace your solution through these 6 failure modes that represent 92% of hidden test case rejections:

๐Ÿ›ก๏ธ The 6-Point OA Pre-Flight Checklist

  • ๐Ÿ”ฒ Empty / Null Input: Does code crash on nums == null || nums.length == 0 or s.isEmpty()?
  • ๐Ÿ”ฒ Single Element (N = 1): Does left < right never execute and return uninitialized defaults?
  • ๐Ÿ”ฒ All Duplicates: Does [5, 5, 5, 5] cause infinite loops in two-pointer or binary search conditions?
  • ๐Ÿ”ฒ Negative Values & Zeros: Does modulo (a - b) % N produce negative indexes? Use Math.floorMod().
  • ๐Ÿ”ฒ 32-Bit Overflow: Does accumulating sums or midpoint calculations overflow past Integer.MAX_VALUE? Use long.
  • ๐Ÿ”ฒ Clean State: Did you remove all static mutable fields and System.out.println() statements?

๐ŸŽฏ Module 15 Takeaways

  • Never declare static mutable fields on TestGorilla or HackerRank; test runners share the JVM.
  • If optimal code is not clicking within 5 minutes, code the brute force first to lock in 60% partial credit.
  • Remove all System.out.println() before submitting to avoid I/O-induced Time Limit Exceeded errors.
  • Use int[128] or int[26] instead of HashMap for character frequency tracking.
  • Always pre-flight test against $N=0$, $N=1$, all identical values, and 32-bit integer overflow.
Knowledge Check 15A โ€ข The Static State Trap in OA Runners

Why does writing private static int counter = 0; frequently fail on TestGorilla or HackerRank assessments?

Knowledge Check 15B โ€ข Binary Search on Solution Space

When can you use Binary Search on a problem where the input array is completely unsorted?