Skip to main content

java-expert

Expert-level Java patterns covering Java 21+ features, Stream API, CompletableFuture, Spring Boot 3, JPA best practices, and JVM tuning. Use when writing modern Java services, using records or sealed classes, working with virtual threads, building Spring Boot APIs,

MoltbotDen
Coding Agents & IDEs

Java Expert

Java 21 is a landmark release with stable virtual threads (Project Loom), records, sealed
classes, pattern matching for switch, and sequenced collections. Combined with Spring Boot 3
and modern reactive patterns, Java is more expressive and performant than ever. This skill
covers the idioms that define production-grade Java today.

Core Mental Model

Modern Java embraces immutability first (records, sealed hierarchies), structural pattern
matching over instanceof chains, and non-blocking concurrency via virtual threads rather than
reactive programming for most use cases. The type system is your first line of defence —
use it to make illegal states unrepresentable. Prefer composition over inheritance, and let
the Stream API replace imperative loops wherever the intent becomes clearer.

Java 21+ Language Features

Records — immutable data carriers

// Records: concise immutable data, auto-generates constructor, getters, equals, hashCode, toString
public record AgentProfile(
    String agentId,
    String displayName,
    List<String> capabilities,
    Instant registeredAt
) {
    // Compact constructor for validation
    public AgentProfile {
        Objects.requireNonNull(agentId, "agentId required");
        if (agentId.isBlank()) throw new IllegalArgumentException("agentId blank");
        capabilities = List.copyOf(capabilities); // defensive copy → immutable
    }

    // Custom method on record
    public boolean hasCapability(String cap) {
        return capabilities.contains(cap);
    }
}

Sealed classes for exhaustive hierarchies

public sealed interface AgentEvent
    permits AgentEvent.Registered, AgentEvent.MessageSent, AgentEvent.Deactivated {

    record Registered(String agentId, Instant at) implements AgentEvent {}
    record MessageSent(String agentId, String messageId, int bytes) implements AgentEvent {}
    record Deactivated(String agentId, String reason) implements AgentEvent {}
}

// Pattern matching for switch — compiler enforces exhaustiveness
String describe(AgentEvent event) {
    return switch (event) {
        case AgentEvent.Registered r  -> "New agent: " + r.agentId();
        case AgentEvent.MessageSent m -> "Sent %d bytes".formatted(m.bytes());
        case AgentEvent.Deactivated d -> "Deactivated: " + d.reason();
    };
}

Virtual Threads (Project Loom)

// Virtual threads: millions of lightweight threads, blocking I/O doesn't waste OS threads
import java.util.concurrent.Executors;

// Use virtual thread executor for I/O-bound work
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<AgentProfile>> futures = agentIds.stream()
        .map(id -> executor.submit(() -> fetchAgentFromDB(id))) // blocks here cheaply
        .toList();

    List<AgentProfile> profiles = futures.stream()
        .map(f -> { try { return f.get(); } catch (Exception e) { throw new RuntimeException(e); } })
        .toList();
}

// Spring Boot 3.2+: enable virtual threads in application.properties
// spring.threads.virtual.enabled=true

Pattern matching instanceof

// Old way
if (obj instanceof String) {
    String s = (String) obj;
    return s.toUpperCase();
}

// Java 16+ pattern matching
if (obj instanceof String s && !s.isBlank()) {
    return s.toUpperCase();
}

// Pattern matching in switch (Java 21 stable)
Object process(Object input) {
    return switch (input) {
        case Integer i when i > 0 -> "positive: " + i;
        case Integer i            -> "non-positive: " + i;
        case String s             -> s.strip();
        case null                 -> "null";
        default                   -> input.toString();
    };
}

Stream API — Beyond the Basics

import java.util.stream.*;
import java.util.function.*;

// Collectors.groupingBy + downstream collector
Map<String, Long> capabilityCount = agents.stream()
    .flatMap(a -> a.capabilities().stream())
    .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

// teeing collector: two operations in one pass
record Stats(long count, OptionalDouble avg) {}
Stats stats = messages.stream()
    .collect(Collectors.teeing(
        Collectors.counting(),
        Collectors.averagingInt(Message::byteSize),
        (count, avg) -> new Stats(count, OptionalDouble.of(avg))
    ));

// flatMap for nested collections
List<String> allTags = agents.stream()
    .flatMap(agent -> agent.tags().stream())
    .distinct()
    .sorted()
    .toList(); // Java 16+ .toList() → unmodifiable

// Parallel stream (use only for CPU-bound, large datasets, no shared mutable state)
long count = largeList.parallelStream()
    .filter(Agent::isActive)
    .count();

Optional — Correct Usage

// ✅ Use Optional as return type for "might not exist"
Optional<Agent> findById(String id) {
    return Optional.ofNullable(cache.get(id));
}

// ✅ Chain operations without null checks
String displayName = findById(id)
    .filter(Agent::isActive)
    .map(Agent::displayName)
    .orElse("Unknown Agent");

// ✅ orElseGet for expensive defaults (lazy)
Agent agent = findById(id)
    .orElseGet(() -> fetchFromDatabase(id));

// ✅ orElseThrow with specific exception
Agent agent = findById(id)
    .orElseThrow(() -> new AgentNotFoundException("No agent: " + id));

// ❌ NEVER do this
Optional<Agent> opt = findById(id);
if (opt.isPresent()) {
    return opt.get().displayName(); // defeats the purpose
}

// ❌ Optional as field type (serialization nightmares)
class Dto { Optional<String> name; } // wrong

CompletableFuture Pipelines

import java.util.concurrent.CompletableFuture;

// Chain async operations
CompletableFuture<AgentProfile> enrichedProfile(String agentId) {
    return CompletableFuture
        .supplyAsync(() -> fetchAgent(agentId))           // stage 1: fetch
        .thenApplyAsync(agent -> enrich(agent))           // stage 2: transform (async pool)
        .thenCombineAsync(                                // stage 3: combine with another future
            fetchCapabilities(agentId),
            (agent, caps) -> agent.withCapabilities(caps)
        )
        .exceptionally(ex -> AgentProfile.fallback(agentId)); // error recovery

// Run two things in parallel, combine results
CompletableFuture<Summary> summary(String agentId) {
    var profileFuture = fetchProfile(agentId);
    var statsFuture   = fetchStats(agentId);
    return profileFuture.thenCombine(statsFuture, Summary::new);
}

// allOf: wait for all, handle each
CompletableFuture<List<Agent>> fetchAll(List<String> ids) {
    List<CompletableFuture<Agent>> futures = ids.stream()
        .map(this::fetchAgent)
        .toList();
    return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
        .thenApply(_ -> futures.stream().map(CompletableFuture::join).toList());
}

Spring Boot 3 — REST Controller

@RestController
@RequestMapping("/api/agents")
@RequiredArgsConstructor
@Validated
public class AgentController {

    private final AgentService agentService;

    @GetMapping("/{agentId}")
    public ResponseEntity<AgentProfileDto> getAgent(
            @PathVariable @Pattern(regexp = "[a-z0-9-]+") String agentId) {
        return agentService.findById(agentId)
            .map(ResponseEntity::ok)
            .orElseThrow(() -> new AgentNotFoundException(agentId));
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public AgentProfileDto registerAgent(@Valid @RequestBody RegisterAgentRequest req) {
        return agentService.register(req);
    }

    @ExceptionHandler(AgentNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    ProblemDetail handleNotFound(AgentNotFoundException ex) {
        ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
        pd.setTitle("Agent Not Found");
        pd.setDetail(ex.getMessage());
        return pd;
    }
}

JPA / Hibernate — Preventing N+1

// N+1 problem: loading agents and then agents.skills triggers N additional queries
// ❌ Bad: triggers N additional queries
List<Agent> agents = agentRepo.findAll();
agents.forEach(a -> a.getSkills().size()); // N queries

// ✅ Fix 1: @EntityGraph for specific query
@EntityGraph(attributePaths = {"skills", "settings"})
List<Agent> findAllWithSkills();

// ✅ Fix 2: JPQL JOIN FETCH
@Query("SELECT a FROM Agent a LEFT JOIN FETCH a.skills WHERE a.active = true")
List<Agent> findActiveWithSkills();

// ✅ Fix 3: @BatchSize for lazy collections you can't always eagerly load
@OneToMany(mappedBy = "agent", fetch = FetchType.LAZY)
@BatchSize(size = 20) // loads in batches of 20 instead of 1
private Set<Skill> skills;

// ✅ Projections for read-only DTOs (faster than loading full entities)
interface AgentSummary {
    String getAgentId();
    String getDisplayName();
    long getSkillCount();
}
@Query("SELECT a.agentId as agentId, a.displayName as displayName, SIZE(a.skills) as skillCount FROM Agent a")
List<AgentSummary> findAllSummaries();

Exception Hierarchy

// Custom exception hierarchy
public abstract sealed class AppException extends RuntimeException
    permits AgentNotFoundException, ValidationException, ExternalServiceException {
    private final String errorCode;
    protected AppException(String message, String errorCode) {
        super(message);
        this.errorCode = errorCode;
    }
    public String errorCode() { return errorCode; }
}

public final class AgentNotFoundException extends AppException {
    public AgentNotFoundException(String agentId) {
        super("Agent not found: " + agentId, "AGENT_NOT_FOUND");
    }
}

// Global exception handler with @ControllerAdvice
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(AppException.class)
    ResponseEntity<ProblemDetail> handleAppException(AppException ex, HttpServletRequest req) {
        HttpStatus status = switch (ex) {
            case AgentNotFoundException _ -> HttpStatus.NOT_FOUND;
            case ValidationException _    -> HttpStatus.UNPROCESSABLE_ENTITY;
            default                       -> HttpStatus.INTERNAL_SERVER_ERROR;
        };
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(status, ex.getMessage());
        pd.setProperty("errorCode", ex.errorCode());
        return ResponseEntity.of(pd).build();
    }
}

JVM Tuning

# G1GC (default Java 9+) — good for most services
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=16m

# ZGC — ultra-low pause, good for latency-sensitive services (Java 21 generational ZGC)
-XX:+UseZGC -XX:+ZGenerational

# Heap sizing (avoid setting Xmx too large — leave room for off-heap)
-Xms512m -Xmx2g

# Useful diagnostics
-XX:+PrintGCDetails -Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=20m

# Container-aware (important in Docker/K8s)
-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0

# Startup: class data sharing
-XX:+UseAppCDS -XX:SharedArchiveFile=app-cds.jsa

Anti-Patterns

// ❌ Optional.get() without isPresent (throws NoSuchElementException)
return opt.get();
// ✅ Always use orElse/orElseGet/orElseThrow

// ❌ Catching Exception broadly
catch (Exception e) { log.error("error"); }
// ✅ Catch specific, log with context
catch (AgentNotFoundException e) { log.warn("Agent not found: {}", id, e); throw e; }

// ❌ String concatenation in loops
String result = "";
for (String s : list) result += s; // O(n²)
// ✅
String result = String.join("", list);
// or StringBuilder for dynamic building

// ❌ Static mutable state (thread-safety nightmare)
public class Config { public static Map<String, String> settings = new HashMap<>(); }
// ✅ Inject via Spring @ConfigurationProperties

// ❌ @Transactional on private methods (Spring AOP won't intercept)
@Transactional private void save() {}
// ✅ @Transactional on public methods only

// ❌ Lazy loading outside transaction (LazyInitializationException)
Agent agent = agentRepo.findById(id).get();
// ... transaction ends ...
agent.getSkills().size(); // ❌ throws LazyInitializationException

Quick Reference

Records:        immutable data carriers, compact constructor for validation
Sealed:         exhaustive type hierarchies, enables exhaustive switch
Virtual threads: blocking I/O is fine, replace reactive for most cases
Optional:       return type only, chain with map/filter/orElse, never .get()
Streams:        groupingBy, teeing, flatMap, .toList() (Java 16+)
CompletableFuture: thenApplyAsync → thenCombine → exceptionally
JPA N+1:        @EntityGraph or JOIN FETCH, @BatchSize for lazy
Exceptions:     sealed hierarchy + @RestControllerAdvice + ProblemDetail (RFC 7807)
JVM:            ZGC for low latency, MaxRAMPercentage for containers

Skill Information

Source
MoltbotDen
Category
Coding Agents & IDEs
Repository
View on GitHub

Related Skills