agentleFS
base58edbase58ed/claude_code_files/skills/java-skill/SKILL.md

java

Skill1 starsChanged 3 months agoMITView on GitHub

Modern Java patterns. ALWAYS load for .java files, Java code, JVM work, Spring, Quarkus, Maven, pom.xml, or Gradle. Trigger vocabulary includes virtual threads, structured concurrency, records, sealed interfaces, Undertow, Jackson, Log4j2, HikariCP, Apache HttpClient, Failsafe, Micrometer, JUnit, AssertJ, Testcontainers, jqwik — even when the user doesn't say "Java" explicitly.

  • Reads credentials

11 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use node.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • mcollinaSkill

    node

    Provides domain-specific best practices for Node.js development with TypeScript, covering type stripping, async patterns, error handling, streams, modules, testing, performance, caching, logging, and more. Use when setting up Node.js projects with native TypeScript support, configuring type stripping (--experimental-strip-types), writing Node 22+ TypeScript without a build step, or when the user mentions 'native TypeScript in Node', 'strip types', 'Node 22 TypeScript', '.ts files without compilation', 'ts-node alternative', or needs guidance on error handling, graceful shutdown, flaky tests, profiling, or environment configuration in Node.js. Helps configure tsconfig.json for type stripping, set up package.json scripts, handle module resolution and import extensions, and apply robust patterns across the full Node.js stack.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

12 rules. 0 are common, 4 are distinctive, and 8 are very specific to this project.

  • 0

    Common

    Most of the 9 edited versions we read keep these.

  • 4

    Distinctive

    This author's own choices. Worth a look.

  • 8

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use import module java.base.

    line 14
  2. Distinctive:

    Delete aggressively — git preserves history

    line 29
  3. Distinctive:

    No global variables or shared mutable state

    line 32
  4. Distinctive:

    When clarity must be sacrificed for a measured hot path, document the reasoning.

    line 55
  5. Distinctive:

    When code review flags long-method, deep-nesting, or too-many-args, these are the canonical shapes:

    line 79
  6. Very specific:

    Prefer switch guard clauses that return Result.err() over .unwrap() / .expect() which throw.

    line 131
  7. Very specific:

    Prefer lock-free over locked.

    line 262
  8. Very specific:

    Every int → Integer in a generic collection allocates.

    line 454
  9. Very specific:

    Avoid String.format() / .formatted() — parses format string at runtime

    line 471
  10. Very specific:

    When working with Quarkus (@ApplicationScoped, @Path, @Inject, QuarkusTest, Flyway, SmallRye, CDI, ArC), read references/quarkus.md in this …

    line 492
  11. Very specific:

    When working with Spring Boot 4 / Spring Framework 7 (Spring imports, @SpringBootApplication, @RestController, @Service, Spring Data, …

    line 494
  12. Very specific:

    Before committing or completing a review, read references/checklist.md in this skill's directory for the full checklist.

    line 517

“Common” is measured: kept by at least 6 in 10 of the 9 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Version Policy

Respect the project's pinned Java version first. Work/production projects default to the current LTS (Java 25) without preview flags — never add --enable-preview to a production service unprompted. Greenfield and personal projects: Java 26+ with --enable-preview.

Finalized features to use freely: virtual threads, pattern matching for switch, record patterns, unnamed variables (_), sealed interfaces, sequenced collections, FFM API, stream gatherers, scoped values, module import declarations, flexible constructor bodies (statements before super() — validate and transform args first).

Preview features (where the policy allows preview; API may shift between releases): structured concurrency, primitive types in patterns, lazy constants.

Use import module java.base; to reduce import noise (finalized JDK 25).

Philosophy

  • Modular, clear, readable code with care for performance and efficiency
  • Obvious over clever — if it needs a comment to be readable, simplify it first (mandated WHY and big-O comments are the exception, not a smell)
  • Result<T> over exceptions — exceptions only at system boundaries
  • Pattern-matching and data-oriented over object-oriented
  • Immutable by default, final on every class (prohibit inheritance unless designed for it)
  • Static factory methods over constructors when multiple creation paths exist
  • Each module does ONE thing well — clear, minimal interfaces
  • Interfaces at collaborator seams that need substitution (tests, multiple impls); concrete final classes elsewhere
  • Compose small pieces into larger systems — no inheritance hierarchies
  • Guard clauses over .unwrap() — return Result.err(), don't throw
  • Solve the actual problem, not a generalized version
  • Delete aggressively — git preserves history
  • Google Code-Style (2-space indent), comments in markdown
  • Objects.requireNonNull() for null checks; AppException (stacktrace-free) for validation failures — assert is only for debug-only preconditions (disabled in prod without -ea). All exceptions should be lightweight — Result<T> is the primary error path.
  • No global variables or shared mutable state
  • var when the RHS makes the type obvious (constructor, factory, literal); explicit type when it doesn't (method return, arithmetic, chains)
  • Named constants with units — no magic numbers, no commented-out code
  • Declarative state for APIs — owned collections (children, tags, items) ride in the parent payload; the backend replaces atomically (delete-all + insert-new). The submitted state IS the truth — no diffing, no sub-resource endpoints, no PATCH semantics for collections.

Clean Surface, Efficient Internals

The interface boundary is where you switch from "read nice" to "run fast":

  • Surface (callers see): interfaces, records, Result<T>, sealed hierarchies, streams, method references — optimize for clarity
  • Internals (implementors write): pre-sized collections, plain loops, primitive arrays, reused objects, allocation-conscious code — optimize for throughput

This is habitual efficiency, not premature optimization — pre-sizing a list costs zero readability, and the caller never knows. The canonical JDBC shape lives in references/jdbc.md.

Performance

  • Correct first, then clear, then fast
  • Measure before optimizing — intuition is often wrong
  • Optimize hot paths only — 90% of code doesn't matter
  • Simple code is often fast code (cache-friendly, predictable)
  • Allocation-aware: every new is GC work later — free in cold paths, matters in hot paths
  • Complexity for performance is a last resort, with comments explaining WHY

When clarity must be sacrificed for a measured hot path, document the reasoning.

Function Discipline

  • Max ~40 lines per method, 3-4 parameters, 2-3 nesting levels — a deliberate two-tier override of the global 2-level default: if-depth stays ≤ 2 (enforced by checkstyle NestedIfDepth), while construct depth may reach 3 because try-with-resources and switch-with-guards are idiomatic Java
  • Single responsibility: one reason to change
  • Early returns over nested conditionals

Comment Doctrine

Comments are for the person reading and reviewing the code: the public API, anything that is not obvious, and the reason behind the code written as a plain sentence (no WHY: label). Keep the work plan out of comments — no phases, plan steps, ticket numbers, or words like for now / temporary / later. Those go stale and someone has to find and delete them. Unfinished work belongs in plan.md.

Any method that does real work gets a header comment above it: one line saying what it is for, plus its big-O for compute AND memory, derived DFS through the call tree: callees count toward the bound, and recursion counts its activation frames as memory. Tiny obvious helpers and trivially O(1)/O(n) glue are exempt. The big-O lives in the header (/// doc comment), not on a line inside the body.

/// Index orders by customer for O(1) lookup.
/// Compute: O(n) one pass. Memory: O(n) for the map.
Map<Long, Order> indexByCustomer(List<Order> orders) { ... }

When you edit a method, bring its comments to the same bar: keep the comments already there, fix any your change made wrong, and add the missing header / big-O / reason. Do this only for code you add or change, not for nearby code you did not touch.

Three Refactor Shapes (north star)

When code review flags long-method, deep-nesting, or too-many-args, these are the canonical shapes:

PatternTriggerFix
1 — Phase ExtractionMethod exceeds ~40 linesExtract validation / side-effect / verification phases into self-naming helpers; the entry method reads as a 7-line narrative
2 — Params Record + Value RecordsSame 4+ args in 3+ signatures (rule of three — don't preemptively bag)Merge cohesive params into reused records (13 args → 3)
3 — Hoist Invariant Work + Map IndexingLoop-invariant work recomputed per iteration, or stream().filter().findFirst() scans inside loopsHoist invariants above the loop; pre-build Map indexes — O(N²) → O(N)

The three patterns cascade: too-many-args produces long methods; long methods produce deep nesting; deep nesting hides quadratic work. Fix the signature first and the rest unravels. Full before/after pairs (with real codebase examples) live in references/decomposition.md.

Records for Data

public record PaymentResult(BigDecimal amount, PaymentStatus status, Instant processedAt) {
  public PaymentResult {
    Objects.requireNonNull(amount, "amount required");
    if (amount.compareTo(BigDecimal.ZERO) < 0) throw new AppException("amount must be non-negative");
    Objects.requireNonNull(status, "status required");
  }
}

Sealed Interfaces for Domain Modeling

Sealed interface + records = the house ADT. Switches over them must be exhaustive — no default arm, so the compiler flags every new variant.

sealed interface PaymentStatus {
  record Pending(Instant createdAt) implements PaymentStatus {}
  record Confirmed(Instant confirmedAt) implements PaymentStatus {}
  record Cancelled(String reason, Instant cancelledAt) implements PaymentStatus {}
}

String describe(PaymentStatus status) {
  return switch (status) {
    case Pending(var at) -> "Pending since " + at;
    case Confirmed(var at) -> "Confirmed at " + at;
    case Cancelled(var reason, _) -> "Cancelled: " + reason;
  };
}

Result<T> Pattern (No Exceptions)

Exceptions only at system boundaries (Result.of()). Internal code returns Result<T>. When creating Result.java, Failure.java, or AppException.java in a project, you MUST read references/result.md in this skill's directory for the canonical implementation — do not recreate from memory.

Key API: Result.ok(v), Result.err(code, msg), Result.of(() -> ...), .map(), .flatMap(), .orElse(), .orElseThrow(), .unwrap().

Allocation note: Each Result.ok(value) allocates a wrapping record. Fine for request-level code. In tight batch loops over millions of rows, consider plain returns with error codes instead.

Guard Clauses over .unwrap()

Prefer switch guard clauses that return Result.err() over .unwrap() / .expect() which throw. Guard clauses give callers a Result they can handle — .unwrap() gives them an exception they probably can't.

// GOOD: guard clause — caller gets Result.err(), can decide what to do
Result<Order> processOrder(String userId) {
  User user;
  switch (fetchUser(userId)) {
    case Ok(var u) -> user = u;
    case Err(var f) -> { return new Err<>(f); }
  }
  return Result.ok(createOrder(user));
}

// BAD: unwrap in production — throws AppException, no recovery
var user = fetchUser(userId).unwrap();  // crashes if user not found

Null → Result at Boundaries

  • Eliminate null at system boundaries (API, DB, files); empty collections over null (List.of())
  • Objects.requireNonNull(x, "x") at public API entry points
  • Convert Optional/null to Result immediately:
Result<User> getUser(long id) {
  return findById(id)
      .map(Result::ok)
      .orElse(Result.err("NOT_FOUND", "user " + id + " not found"));
}

Make Invalid States Unrepresentable

record PositiveAmount(BigDecimal value) {
  public PositiveAmount {
    Objects.requireNonNull(value, "value required");
    if (value.compareTo(BigDecimal.ZERO) <= 0) throw new AppException("value must be positive");
  }
}

// Newtypes make the signature self-validating — no defensive checks at use sites
Result<Unit> sendInvoice(EmailAddress to, PositiveAmount amount) { ... }

Configuration (Layered)

Precedence: env var > .env file > TOML file > defaults. Uses jackson-dataformat-toml. When adding layered config to a project, read references/config.md in this skill's directory for the canonical implementation.

// Typed config with records — deserialize TOML directly
public record DbConfig(String url, @JsonProperty("pool_size") int poolSize) {}
var db = Config.loadAs(Path.of("app.toml"), AppConfig.class).unwrap().db();

Streams & Collections

House deltas only — stream syntax is assumed knowledge:

  • Immutable factories (List.of, Map.of, Set.of) and .toList() by default
  • Gatherers.windowSliding / windowFixed / scan (JDK 24) fill the gap between map/filter and collect — reach for them before hand-rolling intermediate ops

Hot-path note: Streams allocate pipeline objects (Spliterator + stage per intermediate op). flatMap creates a new Stream per element. In tight loops over large datasets, plain loops with pre-sized collections are cheaper:

// Hot path: plain loop avoids stream + flatMap allocation overhead
var items = new ArrayList<Item>(orders.size() * AVG_ITEMS_PER_ORDER);
for (var order : orders) { items.addAll(order.items()); }

Virtual Threads + Structured Concurrency

When to Use Which

UseWhenWhy
Virtual threadsI/O-bound (HTTP, DB, file, queue)~1-2KB stack each; millions possible. Carrier threads stay busy while virtual threads park on I/O.
Platform threadsCPU-bound (compute, hashing, compression)Virtual threads gain nothing — the carrier is busy anyway. Use ForkJoinPool or fixed platform pool sized to core count.

Thread-Per-Task Style

Virtual threads are cheap — don't pool them, spawn them. One virtual thread per task is the intended model.

Structured Concurrency (Preview — API still evolving)

Structured concurrency ties child task lifetimes to the parent scope. Still preview in JDK 25 (JEP 505, fifth preview) and JDK 26 (JEP 525, sixth preview) — requires --enable-preview. The API shifts between releases; pin snippets to a JDK and verify against its Javadoc.

import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;
import java.util.concurrent.StructuredTaskScope.Subtask;

/// Concurrent fetch — all-or-nothing. JDK 25 shape (JEP 505).
Result<List<Data>> fetchAll(List<String> urls) {
  return Result.of(() -> {
    // Type witness on the joiner is needed: nothing else pins T for open().
    try (var scope = StructuredTaskScope.open(Joiner.<Data>allSuccessfulOrThrow())) {
      urls.forEach(url -> scope.fork(() -> fetch(url).unwrap()));
      return scope.join()      // JDK 25: Stream<Subtask<Data>>
          .map(Subtask::get)
          .toList();
    }
  });
}

JDK 26 delta (JEP 525): allSuccessfulOrThrow() becomes Joiner<T, List<T>> — join() returns List<T> directly and the .map(Subtask::get).toList() step disappears; anySuccessfulResultOrThrow() is renamed anySuccessfulOrThrow(). The two shapes do not cross-compile.

For plain virtual-thread concurrency without structured scoping:

/// Bounded concurrency with semaphore.
List<Result<Data>> fetchBounded(List<String> urls, int maxConcurrent) {
  var sem = new Semaphore(maxConcurrent);
  try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    return urls.stream()
        .map(url -> executor.submit(() -> {
          sem.acquire();
          try { return fetch(url); }
          finally { sem.release(); }
        }))
        .toList().stream()
        .map(future -> Result.of(() -> future.get()))
        .map(r -> r.flatMap(Function.identity()))
        .toList();
  }
}

Concurrency Primitives — Lock-Free First

Prefer lock-free over locked. When locking is unavoidable, use ReentrantLock. Never use synchronized.

ApproachWhenExamples
Immutable dataAlways first choiceRecords, List.of(), Map.copyOf()
Atomic / VarHandle*Single values, counters, CAS loopsAtomicReference, AtomicLong, LongAdder
ConcurrentHashMapShared maps with atomic computecomputeIfAbsent, merge
ReentrantLockTrue mutual exclusion neededCritical sections, resource guards
// Lock-free: CAS loop for atomic state transitions
private final AtomicReference<State> state = new AtomicReference<>(State.IDLE);

boolean tryStart() {
  return state.compareAndSet(State.IDLE, State.RUNNING);
}

// When locking is needed: ReentrantLock with timeout
private static final Duration LOCK_TIMEOUT = Duration.ofSeconds(5);
private final ReentrantLock lock = new ReentrantLock();

Result<Response> executeExclusive(Request request) {
  try {
    if (!lock.tryLock(LOCK_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)) {
      return Result.err("TIMEOUT", "could not acquire lock within " + LOCK_TIMEOUT);
    }
    try { return Result.ok(client.execute(request, handler)); }
    finally { lock.unlock(); }
  } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return Result.err("INTERRUPTED", e);
  }
}

ScopedValues over ThreadLocal

ThreadLocal with virtual threads = memory explosion. ScopedValue (final since Java 25, JEP 506 — no preview flag) is immutable, cheap, and bounded to the dynamic scope. Bindings are inherited by child threads forked through StructuredTaskScope — not by threads spawned via a plain executor.

// BAD: ThreadLocal — leaks, expensive with many virtual threads
private static final ThreadLocal<RequestContext> CTX = new ThreadLocal<>();

// GOOD: ScopedValue — fluent-only API (runWhere/callWhere were removed in JDK 24)
private static final ScopedValue<RequestContext> CTX = ScopedValue.newInstance();

ScopedValue.where(CTX, new RequestContext(traceId, userId)).run(() -> handleRequest());

// Value-returning variant; call() rethrows the op's checked exception type
String body = ScopedValue.where(CTX, ctx).call(() -> render(template));

// Read inside the dynamic scope (chain .where(k2, v2) for multiple bindings):
RequestContext ctx = CTX.get();

Graceful Shutdown

Shutdown hook: stop the server, then close the pool — reverse construction order. HikariDataSource is Closeable; register the hook in main right after construction.

Resilience

Retries: exponential backoff + max attempts + a retryable-error filter; timeouts on every external call. Failsafe is the house resilience library (dev.failsafe:failsafe) — RetryPolicy, CircuitBreaker, Timeout, composed via Failsafe.with(...). Hand-roll retry loops only in zero-dependency contexts. Architecture-level policy (when to circuit-break, retry budgets) belongs to distributed-systems-skill.

Project Setup (Maven)

Properties: maven.compiler.release matches the project's pinned Java version; enable-preview in compiler and surefire plugins only where the Version Policy allows preview.

Entry points: always public static void main(String[] args). Instance/compact main methods work with the java CLI but break exec-maven-plugin, which locates main via reflection on the static main(String[]) signature.

Before adding a dependency, check if it's already provided transitively: mvn dependency:tree | grep <artifact>. Duplicate direct dependencies cause version conflicts and false pinning.

Key dependencies: tools.jackson.core:jackson-databind (Jackson 3), tools.jackson.dataformat:jackson-dataformat-toml, undertow-core, httpclient5, com.zaxxer:HikariCP, log4j-core + disruptor (via log4j-bom), dev.failsafe:failsafe.

Dev dependencies: junit-jupiter (JUnit 6.x), assertj-core, testcontainers-junit-jupiter (Testcontainers 2.x), jqwik, wiremock, awaitility.

Serialization (Jackson 3)

Jackson 3 (tools.jackson.core:jackson-databind, 3.2.0 as of June 2026) — java.time support and MethodHandle-based access are built into jackson-databind. No JavaTimeModule, no Blackbird module. JsonMapper is immutable — configure via builder; JsonMapper.shared() is the process-wide default.

Coordinates: databind is tools.jackson.core:jackson-databind; dataformats are tools.jackson.dataformat:*. Annotations deliberately stay at Jackson 2.x coordinates: com.fasterxml.jackson.core:jackson-annotations (package com.fasterxml.jackson.annotation).

Dates: ISO-8601 textual output is the Jackson 3 default. WRITE_DATES_AS_TIMESTAMPS left SerializationFeature — date/time toggles live in tools.jackson.databind.cfg.DateTimeFeature.

import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.cfg.DateTimeFeature;

private static final JsonMapper MAPPER = JsonMapper.builder()
    .enable(SerializationFeature.INDENT_OUTPUT)        // default false in 3.x
    .enable(DateTimeFeature.WRITE_DATES_WITH_ZONE_ID)  // default false in 3.x
    .build();

// Records serialize/deserialize naturally
public record UserResponse(long id, String email, String displayName) {}

<T> Result<T> parseJson(String json, Class<T> type) {
  return Result.of(() -> MAPPER.readValue(json, type));
}

HTTP Client (Apache HttpClient 5)

Connection pooling, granular timeouts, response handlers with automatic resource cleanup. ALWAYS set all three timeouts; wrap in Result at the boundary. Connect timeout lives on ConnectionConfig (the RequestConfig setter is deprecated since HC 5.5). For the canonical fetchJson helper and the timeout table, read references/http-client.md.

import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.core5.util.Timeout;

private static final Timeout CONNECT_TIMEOUT = Timeout.ofSeconds(5);       // TCP handshake
private static final Timeout RESPONSE_TIMEOUT = Timeout.ofSeconds(10);     // waiting for response data
private static final Timeout POOL_ACQUIRE_TIMEOUT = Timeout.ofSeconds(3);  // waiting for pooled connection

private static final CloseableHttpClient CLIENT = HttpClients.custom()
    .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
        .setDefaultConnectionConfig(ConnectionConfig.custom()
            .setConnectTimeout(CONNECT_TIMEOUT)
            .build())
        .build())
    .setDefaultRequestConfig(RequestConfig.custom()
        .setResponseTimeout(RESPONSE_TIMEOUT)
        .setConnectionRequestTimeout(POOL_ACQUIRE_TIMEOUT)
        .build())
    .build();

Web Server (Undertow)

Fastest embedded Java server — composition-based handler chains, Result-to-status mapping at the edge (Failure.code() → StatusCodes). For the canonical routing/sendJson/sendErr/startup wiring, read references/undertow.md. Servlet async + SSE deployments have hard-won gotchas documented there (no header auto-flush on startAsync() — call flushBuffer(); setAsyncSupported(true) on every filter or 500 UT010026) — read it BEFORE writing any SSE or async servlet code.

Database Access (HikariCP + JDBC)

HikariCP for pooling, plain JDBC with prepared statements, wrap in Result.of() at the boundary. Connection acquired late, released early — one try-with-resources per unit of work. For canonical pool config (named constants, maxLifetime/keepaliveTime rules), query helpers, and the transaction pattern, read references/jdbc.md.

Logging (Log4j2 — Garbage-Free)

Log4j2 in garbage-free mode: zero steady-state allocation, LMAX Disruptor async, direct ByteBuffer encoding. log4j-bom for versions; log4j-core + disruptor. Three rules for zero-allocation call sites: box() every primitive (Unbox.box), () -> supplier for expensive args, never string-concatenate — always {} placeholders. For dependency XML, log4j2.xml, async-logger setup, garbage-free properties, what breaks garbage-free mode, and JSON layouts, read references/log4j2.md.

Observability (logs + metrics + traces, zero-overhead)

Three pillars joined by one correlation id: a log line, a metric window, and a trace of the same request must pivot to each other through that id.

  • Logs: correlation id in ThreadContext (MDC) on every line — set at the request boundary, propagated across virtual-thread hops, echoed in the response header. Garbage-free rules above still apply.
  • Metrics: Micrometer facade (Timer/Counter at seams: request boundaries, external calls, error arms) with the Prometheus registry. Bounded label values only (channel, model, provider, status) — per-user or per-request ids never become tags; identity belongs in traces and logs.
  • Traces: OpenTelemetry SDK (or agent) with W3C traceparent propagation. A shorter service correlation id adopts the OTel trace id by zero-padding to 128 bit (W3C-sanctioned), via one shared pad function. LLM/agent calls carry gen_ai.* attributes and export to Langfuse over OTLP; prompt/completion capture sits behind a config flag, default off.
  • Zero-overhead posture: batch exporters on background threads, pull-based scrape, no observability I/O on request threads; a misconfigured backend logs a warning and is skipped — it never blocks startup or a request.
  • A feature is done when its seams carry spans + metrics — instrumented on landing, not retrofitted.

Dependency Injection

Interface-based injection at collaborator seams for testability; constructor injection wired at the composition root (main); Objects.requireNonNull on every injected dependency. No global service locator.

interface UserRepo {
  Result<User> find(long id);
  Result<Unit> save(User user);
}
// Production: JdbcUserRepo implements UserRepo
// Tests: InMemoryUserRepo implements UserRepo

Allocation-Aware Defaults

The GC reclaims everything, but allocation rate drives GC frequency and CPU. These defaults reduce allocation pressure without hurting readability.

Thread-safe expensive objects → static final (JsonMapper, CloseableHttpClient, Pattern, DateTimeFormatter).

JVM Helps — Know When NOT to Optimize

Escape analysis (EA): The JVM stack-allocates objects that don't escape the method. Short-lived records, iterators, and lambdas in tight loops are often free — EA eliminates them. Don't manually "optimize" what EA already handles.

Compact Object Headers: product feature since JDK 25 (JEP 519), enable with -XX:+UseCompactObjectHeaders — ~10-22% heap reduction on object-heavy workloads. NOT on by default as of JDK 26 (default-on arrives via JEP 534).

Autoboxing — The Biggest Hidden Allocator

Every int → Integer in a generic collection allocates. Fine for small collections, dominates in batch processing.

// Normal code: clarity wins
var ids = users.stream().map(User::id).toList();  // List<Long>, boxing is fine

// Hot path: primitive streams avoid boxing entirely
long sum = transactions.stream().mapToLong(Transaction::amount).sum();

Lambda Capture — Non-Capturing Is Free

Method references and lambdas that capture nothing are cached by the JVM as singletons. Lambdas that capture locals (i -> i.price().multiply(taxRate)) allocate per invocation. Prefer method references in hot paths.

Strings

  • + for simple concatenation (compiler optimizes); StringBuilder in loops
  • Avoid String.format() / .formatted() — parses format string at runtime
  • Log4j2's log.info("x={}", x) skips string building entirely when the level is disabled

Collections

  • Size when known: new ArrayList<>(size), HashMap.newHashMap(size)
  • toList() over .collect(Collectors.toList()) — returns compact immutable list
  • Map.computeIfAbsent over check-then-put — single hash lookup
  • List.of() / Map.of() / Set.of() for small immutable constants
  • EnumMap / EnumSet when keys are enum values — array-backed, no hashing

Testing (JUnit 6 + AssertJ + Testcontainers)

Workflow: global TDD doctrine — red, green, vacuous mutation check; the regression test lands before the fix.

Behavioral over interaction testing — verify state and output, not method calls. Doubles hierarchy: real > fake > stub > mock (mock only external APIs you don't control, via WireMock). Testcontainers for infrastructure — never mock DB/queues/caches. JUnit 6 = unified versioning over the Jupiter model (artifact stays org.junit.jupiter:junit-jupiter; migration from 5.14 is a version bump); Testcontainers 2.x renamed all modules (testcontainers-junit-jupiter, testcontainers-postgresql).

For the doubles table, AssertJ idioms, Testcontainers tuning (tmpfs/reuse/static), fixtures, jqwik property tests, Awaitility for concurrent code, and the anti-pattern pairs, read references/testing.md.

Framework References

When working with Quarkus (@ApplicationScoped, @Path, @Inject, QuarkusTest, Flyway, SmallRye, CDI, ArC), read references/quarkus.md in this skill's directory for Quarkus CDI patterns — the single bean pattern (@ApplicationScoped + final + constructor injection), REST endpoints, SSE streaming, health probes, config, testing, and dev seed data.

When working with Spring Boot 4 / Spring Framework 7 (Spring imports, @SpringBootApplication, @RestController, @Service, Spring Data, Spring Security), read references/spring.md in this skill's directory for Spring-specific patterns — controllers, security, JPA, testing, observability.

Production readiness reviews ("prod review", "production review", "make production ready") are owned by the sibling java-prod-skill — load it; do not re-derive the checklist here.

Lint Configuration

Copy assets/checkstyle.xml (from this skill's directory) to the project root or config/checkstyle/. It turns the skill's signature rules into build failures instead of review findings: the synchronized ban (regexp check), MethodLength 40, ParameterNumber 4, NestedIfDepth 2, FinalClass, MagicNumber. catch (Exception) is deliberately legal — it is the sanctioned boundary-adapter pattern inside Result.of.

Wire it via maven-checkstyle-plugin bound to the validate phase:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-checkstyle-plugin</artifactId>
  <configuration><configLocation>checkstyle.xml</configLocation></configuration>
  <executions>
    <execution><phase>validate</phase><goals><goal>check</goal></goals></execution>
  </executions>
</plugin>

Before Committing (Self-Review)

Before committing or completing a review, read references/checklist.md in this skill's directory for the full checklist. Quick top-of-mind checks:

  • Result<T> for all fallible ops, no bare exceptions
  • Objects.requireNonNull() for null checks; AppException (stacktrace-free) for validation — no assert in production code
  • No synchronized — lock-free or ReentrantLock
  • Non-trivial methods carry big-O comments (compute and memory, derived through the call tree)
  • Collections sized when known, primitive streams in hot paths
  • Timeouts on ALL external calls
  • Tests preceded code (red → green → vacuous mutation check)
  • Tests verify state/behavior, not interactions

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.