Java 21 — Interview Questions
Stack context: This system uses Java 21 as the runtime for all six Spring Boot services. Java 21 is an LTS release introducing virtual threads (Project Loom), records, sealed classes, pattern matching, and text blocks — all relevant to writing clean, performant microservice code.
Q1 — What are the most important new features in Java 21? junior
Answer: Java 21 is an LTS (Long-Term Support) release with several production-ready features:
Language features:
- Records (JDK 16 finalized): Immutable data carriers with auto-generated boilerplate.
- Sealed classes (JDK 17 finalized): Restricted class hierarchies.
- Pattern matching for
instanceof(JDK 16 finalized): Eliminate explicit casts. - Pattern matching for
switch(JDK 21 finalized): Switch on types, not just values. - Text blocks (JDK 15 finalized): Multi-line strings without escaping.
Platform features:
- Virtual threads (Project Loom, JDK 21 finalized): Lightweight threads for massive concurrency.
- Sequenced collections:
SequencedCollection,SequencedMapinterfaces withgetFirst(),getLast(). - String templates (preview in JDK 21): Type-safe string interpolation.
Q2 — What are records in Java and when should you use them? junior
Answer:
Records are transparent, immutable data carriers. The compiler auto-generates constructor, accessors, equals(), hashCode(), and toString().
// Record declaration
public record ProductDto(UUID id, String name, BigDecimal price, int stock) {}
// Usage
ProductDto product = new ProductDto(UUID.randomUUID(), "Laptop", new BigDecimal("999.99"), 50);
System.out.println(product.name()); // accessor (not getName())
System.out.println(product); // auto toString: ProductDto[id=..., name=Laptop, ...]
Compact constructor (validation):
public record OrderRequest(UUID productId, int quantity) {
public OrderRequest { // compact constructor (no parens with params)
if (quantity <= 0) throw new IllegalArgumentException("Quantity must be positive");
Objects.requireNonNull(productId);
}
}
Use when: DTOs, event objects, value objects, method return groupings. Don't use when: Mutable state is needed, JPA entities (need no-arg constructor), inheritance required.
This system: OrderCreatedEvent, PaymentProcessedEvent are candidates for records as they are immutable event payloads.
Q3 — What are sealed classes and when are they useful? junior
Answer: Sealed classes restrict which classes can extend or implement them, creating closed type hierarchies that the compiler can reason about exhaustively.
// Sealed hierarchy — only these three can implement PaymentResult
public sealed interface PaymentResult permits PaymentSuccess, PaymentFailure, PaymentPending {}
public record PaymentSuccess(String transactionId, BigDecimal amount) implements PaymentResult {}
public record PaymentFailure(String reason, int retryCount) implements PaymentResult {}
public record PaymentPending(Instant estimatedCompletion) implements PaymentResult {}
Exhaustive switch (compiler checks all cases covered):
String message = switch (result) {
case PaymentSuccess s -> "Paid: " + s.transactionId();
case PaymentFailure f -> "Failed: " + f.reason();
case PaymentPending p -> "Pending until: " + p.estimatedCompletion();
// No default needed — compiler knows all subtypes
};
Benefits: Replaces fragile if/instanceof chains, prevents unintended subclassing, enables exhaustive switch analysis.
Q4 — What is pattern matching for switch in Java 21? junior
Answer: Java 21 finalizes pattern matching in switch expressions, allowing matching on types, guarded patterns, and null handling.
// Old way (verbose, unsafe)
if (obj instanceof String s) {
System.out.println(s.length());
} else if (obj instanceof Integer i) {
System.out.println(i * 2);
}
// Java 21 pattern matching switch
String result = switch (obj) {
case String s when s.length() > 10 -> "Long string: " + s;
case String s -> "Short string: " + s;
case Integer i -> "Integer: " + i;
case null -> "null";
default -> "Unknown: " + obj.getClass();
};
Guarded patterns (when clause): Refine a type match with an additional condition.
Null handling: Switch statements previously threw NullPointerException on null input. Java 21 switch allows explicit case null.
Real use case in this system:
String statusMessage = switch (order.getStatus()) {
case PENDING -> "Awaiting payment";
case CONFIRMED -> "Payment confirmed, preparing shipment";
case FAILED -> "Payment failed. Please retry.";
};
Q5 — What are virtual threads in Java 21 and how do they improve concurrency? junior
Answer: Virtual threads (Project Loom) are lightweight threads managed by the JVM, not the OS. Unlike platform threads (which map 1:1 to OS threads and use ~1 MB stack), virtual threads use ~few KB of heap and the JVM multiplexes many virtual threads onto a small pool of carrier (platform) threads.
Key properties:
- Create millions of virtual threads without running out of memory.
- Blocking operations (I/O,
sleep,lock.acquire()) unmount the virtual thread from the carrier thread, freeing the carrier to run other work. - API is identical to
Thread— no async/reactive programming model change needed.
// Old: thread pool with ~200 threads
ExecutorService exec = Executors.newFixedThreadPool(200);
// Java 21: virtual thread executor — creates a new virtual thread per task
ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor();
// Spring Boot 3.2+ enable for all Tomcat request threads
// application.yml:
// spring.threads.virtual.enabled: true
Impact on this system: With spring.threads.virtual.enabled=true, each HTTP request in MVC services gets a virtual thread. Even if the request blocks on Redis or DB, the carrier thread is freed — enabling much higher concurrency without increasing thread count.
Q6 — What are text blocks and when are they useful? junior
Answer: Text blocks are multi-line string literals that preserve formatting and eliminate most escape sequences. They use triple-quote delimiters.
// Old way — escape-heavy JSON
String json = "{\"orderId\":\"123\",\"status\":\"CONFIRMED\"}";
// Text block — readable
String json = """
{
"orderId": "123",
"status": "CONFIRMED"
}
""";
// SQL in text block
String sql = """
SELECT o.id, o.status, u.email
FROM orders o
JOIN users u ON o.customer_id = u.id
WHERE o.created_at > :since
ORDER BY o.created_at DESC
""";
// HTML email template
String html = """
<html>
<body>
<h1>Order %s Confirmed</h1>
</body>
</html>
""".formatted(orderId);
Indentation: The text block's indentation is determined by the position of the closing """. Leading whitespace up to that column is stripped.
Q7 — What is the difference between Optional and null in Java? junior
Answer:
Optional<T> is a container that may or may not contain a non-null value. It forces callers to explicitly handle the absent case, making null intent explicit in the API.
// Method that might return nothing
public Optional<Product> findById(UUID id) {
return productRepository.findById(id); // Spring Data returns Optional
}
// Caller must handle absence
productService.findById(id)
.map(ProductDto::from) // transform if present
.orElseThrow(() -> new NotFoundException("Product " + id)); // or throw
// Don't use Optional as a parameter or field — only as return type
Rules:
- Use
Optionalas a return type to signal "value may be absent". - Never use
Optional.get()without checkingisPresent()first. - Don't use
Optionalfor collections — return empty list instead ofOptional<List<T>>. - Don't use
Optionalin JPA entities or as method parameters (anti-pattern).
Q8 — What is var type inference and what are its limitations? junior
Answer:
var (introduced in Java 10) allows the compiler to infer the type from the right-hand side. It's a compile-time feature — the type is still statically determined.
// var infers Map<String, List<OrderDto>>
var ordersByCustomer = new HashMap<String, List<OrderDto>>();
// var in for-each
for (var entry : ordersByCustomer.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue().size());
}
// var reduces verbosity with long generics
var productService = applicationContext.getBean(ProductService.class);
Limitations:
- Cannot use
varwithout initializer:var x;— compiler must infer type. - Cannot use in method signatures, fields, or constructor parameters.
- Reduces readability when type is not obvious from the right-hand side.
- Bad:
var x = getData();— what isx? Good:List<OrderDto> orders = getData();
Best practice: Use var where the type is obvious from the literal or constructor call, not when the type requires context to understand.
Q9 — What is the difference between Comparable and Comparator in Java? junior
Answer:
Comparable<T> |
Comparator<T> |
|
|---|---|---|
| Implemented by | The class itself | External / lambda |
| Defines | Natural ordering | Custom/multiple orderings |
| Method | compareTo(T other) |
compare(T a, T b) |
| Modify existing class | Yes (must own the class) | No — works on any class |
// Comparable — Order has a natural ordering by createdAt
public class Order implements Comparable<Order> {
@Override
public int compareTo(Order other) {
return this.createdAt.compareTo(other.createdAt);
}
}
// Comparator — multiple orderings without modifying Order
Comparator<Order> byAmount = Comparator.comparing(Order::getTotalAmount).reversed();
Comparator<Order> byCustomerThenDate = Comparator
.comparing(Order::getCustomerId)
.thenComparing(Order::getCreatedAt);
orders.sort(byCustomerThenDate);
Q10 — What are functional interfaces and how are lambdas used in Java? junior
Answer: A functional interface has exactly one abstract method. Lambdas provide a concise syntax to implement functional interfaces.
Built-in functional interfaces (java.util.function):
| Interface | Method | Use |
|---|---|---|
Function<T, R> |
R apply(T t) |
Transform T to R |
Predicate<T> |
boolean test(T t) |
Filter/condition |
Consumer<T> |
void accept(T t) |
Side effects |
Supplier<T> |
T get() |
Lazy value provider |
BiFunction<T, U, R> |
R apply(T t, U u) |
Two-arg transform |
// Stream pipeline using lambdas
List<OrderDto> pendingOrders = orders.stream()
.filter(o -> o.getStatus() == PENDING) // Predicate
.map(o -> new OrderDto(o.getId(), o.getTotal())) // Function
.sorted(Comparator.comparing(OrderDto::getTotal).reversed())
.collect(Collectors.toList());
Method references (shorthand for lambdas):
orders.stream().map(Order::getId) // instance method
orders.stream().map(UUID::toString) // static method
orders.stream().filter(Objects::nonNull) // null check
Q11 — What are Java Streams and how do they differ from collections? junior
Answer: Streams are a declarative pipeline for processing sequences of elements. Unlike collections, streams:
- Are lazy: Intermediate operations (map, filter) don't execute until a terminal operation is called.
- Are single-use: A stream can only be consumed once.
- Support parallel processing via
parallelStream(). - Do not store elements: They process elements on-the-fly.
// Imperative (collection-based)
List<String> result = new ArrayList<>();
for (Order o : orders) {
if (o.getStatus() == CONFIRMED) {
result.add(o.getId().toString());
}
}
// Declarative (stream)
List<String> result = orders.stream()
.filter(o -> o.getStatus() == CONFIRMED)
.map(o -> o.getId().toString())
.collect(Collectors.toList());
Intermediate operations (lazy): filter, map, flatMap, distinct, sorted, limit, skip.
Terminal operations (trigger execution): collect, forEach, count, findFirst, anyMatch, reduce.
Q12 — What is the CompletableFuture API and how does it enable async programming? senior
Answer:
CompletableFuture<T> represents an async computation that can be composed, chained, and combined without blocking.
// Async user lookup + product lookup in parallel, then combine
CompletableFuture<UserDto> userFuture = CompletableFuture.supplyAsync(
() -> userService.getUser(userId));
CompletableFuture<List<ProductDto>> productsFuture = CompletableFuture.supplyAsync(
() -> productService.getProducts(productIds));
CompletableFuture<OrderSummary> summary = userFuture
.thenCombine(productsFuture, (user, products) ->
new OrderSummary(user, products, calculateTotal(products)))
.thenApply(this::enrichWithShippingInfo)
.exceptionally(ex -> {
log.error("Failed to build order summary", ex);
return OrderSummary.empty();
});
// Non-blocking: don't call .get() on the event loop!
Key methods:
thenApply: Transform result (likemap).thenCompose: Chain async operations (likeflatMap).thenCombine: Combine two independent futures.allOf: Wait for all futures to complete.exceptionally: Handle errors.
vs. Virtual Threads: Virtual threads make blocking code scale like async code. Prefer virtual threads + blocking code for readability; use CompletableFuture when you genuinely need parallel fan-out.
Q13 — What is the memory model in Java and what does volatile mean? senior
Answer: The Java Memory Model (JMM) defines how threads share memory. Without synchronization, threads may work on local CPU cache copies of variables, leading to visibility and ordering issues.
volatile: Ensures:
- Visibility: Every read of a
volatilevariable sees the latest write by any thread (no caching). - Ordering: Reads and writes to
volatilecannot be reordered relative to other operations.
// Without volatile: thread B may never see thread A's write
private boolean running = true;
// With volatile: safe flag for single-writer, multiple-reader
private volatile boolean running = true;
// Thread A
running = false;
// Thread B
while (running) { doWork(); } // reliably sees running = false
volatile limitations: Only safe for single-read/single-write operations. For compound operations (check-then-act, increment), use AtomicInteger, synchronized, or java.util.concurrent locks.
This system: OutboxPublisher.running flag for graceful shutdown could use volatile or AtomicBoolean.
Q14 — What is the difference between synchronized, ReentrantLock, and StampedLock? senior
Answer:
synchronized |
ReentrantLock |
StampedLock |
|
|---|---|---|---|
| Try-lock | No | Yes (tryLock()) |
Yes |
| Interruptible | No | Yes (lockInterruptibly()) |
Yes |
| Fairness | No | Optional | No |
| Read-write | No | ReadWriteLock companion |
Built-in optimistic read |
| Condition | wait/notify |
Condition API |
— |
StampedLock optimistic read (Java 8+):
StampedLock lock = new StampedLock();
// Optimistic read (no lock acquired — just a stamp)
long stamp = lock.tryOptimisticRead();
double x = this.x;
double y = this.y;
if (!lock.validate(stamp)) {
// Data changed — fall back to read lock
stamp = lock.readLock();
try { x = this.x; y = this.y; }
finally { lock.unlockRead(stamp); }
}
With Virtual Threads: synchronized can pin a virtual thread to its carrier thread. Prefer ReentrantLock in virtual-thread code to avoid carrier thread starvation.
Q15 — What is a ThreadLocal and what are the risks with virtual threads? senior
Answer:
ThreadLocal<T> stores a value per thread. Used for holding per-request context (user ID, trace ID, transaction) without passing it through method parameters.
// Old pattern: store user context in ThreadLocal
public class SecurityContext {
private static final ThreadLocal<UserContext> CONTEXT = new ThreadLocal<>();
public static void set(UserContext ctx) { CONTEXT.set(ctx); }
public static UserContext get() { return CONTEXT.get(); }
public static void clear() { CONTEXT.remove(); } // CRITICAL: always clear!
}
Risks with virtual threads:
- Memory leaks: Virtual threads are numerous and short-lived. If
ThreadLocalis not cleared, values linger until GC. - Excessive memory: 1 million virtual threads × 1 ThreadLocal value = 1 million copies.
InheritableThreadLocal: Child virtual threads inherit parent's ThreadLocal — can cause unexpected value leakage.
Java 21 alternative: Scoped Values (ScopedValue) — immutable, inherited, and automatically cleaned up when scope exits. Preferred over ThreadLocal for virtual thread compatibility.
static final ScopedValue<UserContext> USER_CTX = ScopedValue.newInstance();
ScopedValue.where(USER_CTX, ctx).run(() -> {
// USER_CTX.get() returns ctx within this scope only
service.processRequest();
});
Q16 — How does the Java garbage collector work and what are the main GC algorithms? senior
Answer: The JVM automatically manages memory via garbage collection. The GC identifies objects not reachable from any GC root (stack variables, static fields) and reclaims their memory.
Heap regions:
- Young generation: New objects allocated here. Most objects die young (Eden → Survivor).
- Old generation: Long-lived objects promoted from young gen.
Modern GC algorithms:
| GC | Latency | Throughput | Heap size | Use case |
|---|---|---|---|---|
| G1 (default, Java 9+) | Low-medium | High | Large | Most apps |
| ZGC (Java 15+) | Ultra-low (< 1 ms) | Lower | Very large (TB) | Latency-critical |
| Shenandoah | Ultra-low | Lower | Any | Latency-critical, OpenJDK |
| Serial | Poor | Medium | Small | Embedded |
For microservices (this system): G1GC is the default — good balance. For gateway/latency-sensitive services, consider ZGC: -XX:+UseZGC.
Tuning: -Xms512m -Xmx512m (fixed heap reduces GC overhead), -XX:MaxGCPauseMillis=50 (G1 target pause).
Q17 — What are generics and what is type erasure? senior
Answer:
Generics add compile-time type safety without runtime overhead. The compiler enforces type constraints; at runtime, generic type parameters are erased (replaced with Object or bounds).
// Generic repository interface
public interface Repository<T, ID> {
Optional<T> findById(ID id);
T save(T entity);
List<T> findAll();
}
// Wildcard — accepts Repository of any type
void process(Repository<?, ?> repo) { ... }
// Bounded wildcard — accepts Repository of Number or subtype
void sum(List<? extends Number> numbers) { ... }
Type erasure implications:
List<String>andList<Integer>have the same runtime type:List.instanceof List<String>is illegal — can only checkinstanceof List<?>.- Cannot create generic arrays:
new T[10]is illegal. - Reflection cannot access generic type parameters at runtime (without
ParameterizedType).
Practical: Spring's ParameterizedTypeReference works around erasure for HTTP responses:
ResponseEntity<List<OrderDto>> response = restTemplate.exchange(
url, GET, null, new ParameterizedTypeReference<List<OrderDto>>() {});
Q18 — What is the equals/hashCode contract and why is it important for collections? senior
Answer:
The contract: if a.equals(b) is true, then a.hashCode() == b.hashCode() must be true. The reverse is not required (hash collisions are allowed).
Why it matters:
HashMap,HashSet,Hashtable: UsehashCode()to find the bucket, thenequals()to find the key. If you overrideequals()withouthashCode(), equal objects may be placed in different buckets —HashMap.get()returnsnullfor a key that exists.TreeMap,TreeSet: UsecompareTo()/Comparator, notequals(). Breakingequals/compareToconsistency causes subtle bugs.
// Correct record implementation — auto-generates correct equals/hashCode
public record ProductId(UUID value) {}
// Manual implementation (if using class)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Product p)) return false;
return Objects.equals(id, p.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
JPA note: JPA entities should implement equals/hashCode based on the business key or database ID — NOT auto-generated @Id values (which are null before persist).
Q19 — What is try-with-resources and how does it improve resource management? junior
Answer:
Try-with-resources (Java 7+) automatically closes resources implementing AutoCloseable when the try block exits, whether normally or via exception.
// Old way — verbose, error-prone
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(path));
return reader.readLine();
} finally {
if (reader != null) {
try { reader.close(); } catch (IOException ignored) {}
}
}
// Try-with-resources — clean and safe
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
} // reader.close() called automatically
// Multiple resources (closed in reverse order)
try (var conn = dataSource.getConnection();
var stmt = conn.prepareStatement(sql)) {
// ...
}
Suppressed exceptions: If both the try body and close() throw, the close() exception is added as a suppressed exception to the primary exception — not silently swallowed.
Q20 — What is a WeakReference and when is it used? senior
Answer:
Java has four reference strengths: strong, soft, weak, and phantom. WeakReference<T> allows the GC to collect the referenced object when no strong references exist.
// Strong reference — prevents GC
Product product = new Product();
// Weak reference — GC can collect product even though weakRef exists
WeakReference<Product> weakRef = new WeakReference<>(product);
product = null; // now only weakRef holds a reference
// After GC: weakRef.get() may return null
Product p = weakRef.get();
if (p != null) {
// still alive
}
WeakHashMap: Keys are weak references. When a key is GC'd, the entry is automatically removed. Used for metadata/attribute maps where the key's lifetime defines the entry's lifetime.
SoftReference: Like weak, but GC only clears it under memory pressure. Used for memory-sensitive caches (Caffeine uses soft references internally as an option).
This system: Caffeine cache uses strong references with size-based eviction. WeakReference would be used for per-request metadata attached to connection objects.
Q21 — How does String.intern() work and what are its performance implications? senior
Answer:
String.intern() returns a canonical representation from the JVM's string pool. Two interned strings that are equal (equals()) return the same object reference, enabling == comparison.
String a = new String("order");
String b = new String("order");
a == b; // false — different heap objects
a.equals(b); // true
String c = a.intern();
String d = b.intern();
c == d; // true — same pool entry
Performance implications:
- Benefit: Reduces memory for many duplicate strings. Useful for status codes, enum-like strings.
- Risk: String pool is in the heap (since Java 7 — previously PermGen). Large pools cause GC pressure and longer pause times.
- Modern alternative: Use
enumorrecordfor constant values. UseString.intern()only when profiling confirms it helps.
Q22 — What are the key differences between HashMap and ConcurrentHashMap? senior
Answer:
HashMap |
ConcurrentHashMap |
|
|---|---|---|
| Thread safety | Not safe | Safe — lock striping |
| Null keys/values | Allows one null key | Does NOT allow null key or value |
| Performance | Fastest single-threaded | Comparable single-threaded, better multi-threaded |
| Java 8 structure | Array of linked lists/trees | Segment-based to CAS-based |
ConcurrentHashMap concurrency:
- Read operations: Lock-free (CAS).
- Write operations: Lock per bucket (not the whole map).
compute,computeIfAbsent,merge: Atomic compound operations.
// Safe count per status
ConcurrentHashMap<OrderStatus, Long> counts = new ConcurrentHashMap<>();
orders.forEach(o -> counts.merge(o.getStatus(), 1L, Long::sum));
// Idiomatic cache pattern
Map<UUID, Product> cache = new ConcurrentHashMap<>();
Product p = cache.computeIfAbsent(id, k -> productRepo.findById(k).orElseThrow());
Java 21 alternative: HashMap + Virtual Threads is fine if you use synchronized blocks. ConcurrentHashMap remains the default for shared mutable maps.
Q23 — How do you read and write files efficiently in Java? junior
Answer: Reading entire file (small files):
// Java 11+
String content = Files.readString(Path.of("config.json"));
List<String> lines = Files.readAllLines(Path.of("data.csv"));
Reading large files (streaming):
// Process line-by-line without loading all into memory
try (Stream<String> lines = Files.lines(Path.of("large-file.txt"))) {
lines.filter(l -> l.startsWith("ERROR"))
.forEach(System.out::println);
}
Writing files:
// Write string (overwrites)
Files.writeString(Path.of("output.txt"), content);
// Append
Files.writeString(Path.of("log.txt"), entry, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
Buffered I/O for performance (large binary files):
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest))) {
byte[] buffer = new byte[8192];
int n;
while ((n = bis.read(buffer)) != -1) bos.write(buffer, 0, n);
}
Q24 — What is instanceof pattern matching and how does it reduce boilerplate? junior
Answer:
Pattern matching for instanceof (Java 16+) combines the type check and cast into a single operation, eliminating the redundant explicit cast.
// Old way — redundant cast
if (event instanceof OrderCreatedEvent) {
OrderCreatedEvent orderEvent = (OrderCreatedEvent) event;
processOrder(orderEvent.getOrderId());
}
// Java 16+ pattern matching
if (event instanceof OrderCreatedEvent orderEvent) {
processOrder(orderEvent.getOrderId());
}
// Combined with logical operators
if (event instanceof OrderCreatedEvent e && e.getStatus() == PENDING) {
processOrder(e.getOrderId());
}
In switch (Java 21):
void handleEvent(Object event) {
switch (event) {
case OrderCreatedEvent e -> processOrder(e);
case PaymentProcessedEvent e -> updateOrderStatus(e);
case null -> log.warn("Null event received");
default -> log.warn("Unknown event type: {}", event.getClass());
}
}
Q25 — What are the improvements in java.time API over Date and Calendar? junior
Answer:
The java.time API (Java 8+) replaced the legacy Date/Calendar which were mutable, thread-unsafe, and poorly designed.
Key types:
| Type | Description | Example |
|---|---|---|
LocalDate |
Date without time/timezone | 2026-05-31 |
LocalTime |
Time without date/timezone | 14:30:00 |
LocalDateTime |
Date + time, no timezone | 2026-05-31T14:30:00 |
ZonedDateTime |
Date + time + timezone | 2026-05-31T14:30:00+07:00[Asia/Bangkok] |
Instant |
Timestamp in UTC epoch | Best for event timestamps |
Duration |
Amount of time (hours, minutes) | Duration.ofSeconds(30) |
Period |
Amount of date (years, months, days) | Period.ofDays(7) |
This system: Order.createdAt uses Instant (UTC timestamp in DB). TTL calculations use Duration.
// Order expiry: 24 hours after creation
Instant expiry = order.getCreatedAt().plus(Duration.ofHours(24));
boolean isExpired = Instant.now().isAfter(expiry);
Q26 — What are Collectors in the Stream API and what are the most useful ones? junior
Answer: Collectors are terminal stream operations that accumulate stream elements into a result container.
List<Order> orders = getOrders();
// Group by status
Map<OrderStatus, List<Order>> byStatus = orders.stream()
.collect(Collectors.groupingBy(Order::getStatus));
// Count per status
Map<OrderStatus, Long> countByStatus = orders.stream()
.collect(Collectors.groupingBy(Order::getStatus, Collectors.counting()));
// Sum order totals
BigDecimal totalRevenue = orders.stream()
.map(Order::getTotal)
.collect(Collectors.reducing(BigDecimal.ZERO, BigDecimal::add));
// Partition into confirmed vs not
Map<Boolean, List<Order>> partition = orders.stream()
.collect(Collectors.partitioningBy(o -> o.getStatus() == CONFIRMED));
// Join strings
String orderIds = orders.stream()
.map(o -> o.getId().toString())
.collect(Collectors.joining(", ", "[", "]"));
// Collect to unmodifiable list (Java 10+)
List<OrderDto> dtos = orders.stream().map(this::toDto)
.collect(Collectors.toUnmodifiableList());
Q27 — What is Spliterator and how does parallel stream work? senior
Answer:
A Spliterator (splittable iterator) is used by the Stream API to partition a data source for parallel processing.
Parallel stream:
// Uses ForkJoinPool.commonPool() by default
long confirmedCount = orders.parallelStream()
.filter(o -> o.getStatus() == CONFIRMED)
.count();
When parallel streams help:
- Large datasets (> 10,000 elements).
- CPU-bound operations (no I/O, no blocking).
- Stateless, independent per-element processing.
When parallel streams hurt:
- Small datasets — thread coordination overhead exceeds benefit.
- I/O-bound operations — threads block, holding ForkJoin threads.
- Ordered operations (
findFirst,forEachOrdered) — synchronization overhead. - Shared mutable state — requires synchronization, negating parallelism benefit.
Custom thread pool (avoid monopolizing common pool):
ForkJoinPool customPool = new ForkJoinPool(4);
customPool.submit(() ->
orders.parallelStream().filter(...).collect(...)
).get();
Q28 — How does Java handle exceptions and what is the difference between checked and unchecked? junior
Answer:
| Checked | Unchecked | |
|---|---|---|
| Base class | Exception |
RuntimeException (subclass of Exception) |
| Handling | Compiler requires try-catch or throws |
Optional |
| Examples | IOException, SQLException |
NullPointerException, IllegalArgumentException |
| Use when | Recoverable, expected failure | Programming error, unrecoverable |
// Checked — must handle
public String readFile(Path path) throws IOException {
return Files.readString(path); // must declare or catch IOException
}
// Unchecked — optional handling
public Order findOrder(UUID id) {
return orderRepo.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id)); // RuntimeException
}
Modern practice: Prefer unchecked exceptions for domain errors. Checked exceptions add noise and often lead to catch (Exception e) { throw new RuntimeException(e); } anti-patterns.
Spring: DataAccessException hierarchy is all unchecked — wraps checked JDBC exceptions transparently.
Q29 — What is the @FunctionalInterface annotation and why is it important? junior
Answer:
@FunctionalInterface marks an interface as intended for lambda use. The compiler enforces exactly one abstract method, preventing accidental addition of a second abstract method that would break all existing lambda implementations.
@FunctionalInterface
public interface OrderValidator {
ValidationResult validate(Order order); // single abstract method
// Default methods are fine
default boolean isValid(Order order) {
return validate(order).isOk();
}
// Static helper methods are fine
static OrderValidator noOp() {
return order -> ValidationResult.ok();
}
}
// Usage
OrderValidator priceCheck = order -> order.getTotal().compareTo(BigDecimal.ZERO) > 0
? ValidationResult.ok()
: ValidationResult.error("Price must be positive");
OrderValidator combined = priceCheck
.andThen(stockCheck) // compose validators
.andThen(addressCheck);
Q30 — What are the performance benefits of using records over classes for DTOs in microservices? senior
Answer: Records provide several performance and design benefits for DTO/event objects:
Compile-time savings:
- Auto-generated
equals,hashCode,toString— no Lombok required. - Immutability — no defensive copying needed.
finalfields — JIT can optimize field access.
Memory:
- No field redundancy (records use compact object layout in modern JVMs).
- JVM optimizations: records are eligible for value-based optimization in future Java versions (Project Valhalla).
JSON serialization (Jackson 2.12+):
// Jackson handles records natively — no extra annotations
public record ProductDto(UUID id, String name, BigDecimal price) {}
// Works out of the box
objectMapper.readValue(json, ProductDto.class);
objectMapper.writeValueAsString(new ProductDto(...));
Kafka Avro: Records can implement Avro-generated interfaces if the schema fields match, simplifying event type usage.
Practical impact: Replacing 50-line POJOs + Lombok with 1-line records reduces codebase size, eliminates Lombok dependency issues, and leverages compiler-guaranteed correctness.
This system: Avro generated classes are not records (generated code), but application-layer DTOs (CreateOrderRequest, OrderDto, ProductDto) are ideal record candidates.