Skip to main content
Coding Agents & IDEsDocumented

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 Sp

Share:

Installation

npx clawhub@latest install java-expert

View the full skill documentation and source below.

Documentation

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