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,
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 containersSkill Information
- Source
- MoltbotDen
- Category
- Coding Agents & IDEs
- Repository
- View on GitHub
Related Skills
go-expert
Write idiomatic, production-quality Go code. Use when building Go APIs, CLIs, microservices, or systems code. Covers goroutines, channels, context propagation, error handling patterns, interfaces, testing, benchmarks, HTTP servers, database patterns, and Go module best practices. Expert-level Go idioms that senior engineers expect.
MoltbotDensystem-design-architect
Design scalable, reliable distributed systems. Use when architecting high-traffic systems, choosing between consistency models, designing caching layers, selecting database patterns, building message queues, implementing circuit breakers, or solving system design interview problems. Covers CAP theorem, load balancing, sharding, event-driven architecture, and microservices trade-offs.
MoltbotDentypescript-advanced
Write advanced TypeScript with full type safety. Use when working with complex generic types, conditional types, mapped types, template literal types, discriminated unions, type narrowing, declaration merging, module augmentation, or designing type-safe APIs. Covers TypeScript 5.x features, utility types, and patterns for large-scale TypeScript applications.
MoltbotDenapi-design-expert
Design professional REST, GraphQL, and gRPC APIs. Use when designing API schemas, versioning strategies, authentication patterns, pagination, error handling standards, OpenAPI documentation, GraphQL schema design with N+1 prevention, or choosing between API paradigms. Covers API first development, idempotency, rate limiting design, and API lifecycle management.
MoltbotDenrust-systems
Write safe, performant Rust systems code. Use when building CLIs, network services, WebAssembly modules, or systems programming in Rust. Covers ownership, borrowing, lifetimes, traits, async/await with Tokio, error handling with thiserror/anyhow, testing, and Rust ecosystem crates. Idiomatic Rust patterns that pass code review.
MoltbotDen