Java Coding Standards
A comprehensive collection of Java coding standards and best practices. Designed for AI agents and LLMs to generate high-quality, idiomatic, and maintainable Java code.
Categories
Anti-Patterns [CRITICAL]
Recognize and eliminate recurring Java anti-patterns that erode correctness, encapsulation, and readability.
| Rule | Description |
|---|---|
| anti-patterns-no-catch-throwable | Never catch throwable or error |
| anti-patterns-no-god-classes | Split god classes along responsibility boundaries |
| anti-patterns-no-magic-numbers | Replace magic numbers and strings with named constants |
| anti-patterns-no-static-mutable-state | Never expose mutable static state |
| anti-patterns-no-utility-class-overuse | Don't default to static utility classes for behavior that belongs on a type |
Resource Management [CRITICAL]
Acquire and release JVM and I/O resources deterministically, without leaks or hand-written cleanup.
| Rule | Description |
|---|---|
| resource-management-autocloseable-implementation | Implement AutoCloseable correctly for custom resource-holding classes |
| resource-management-connection-pool-not-raw | Acquire database connections from a pool, never raw per call |
| resource-management-no-manual-finally-close | Never hand-write a finally block to close a resource |
| resource-management-try-with-resources | Always use try-with-resources for AutoCloseable values |
Concurrency [HIGH]
Coordinate shared state and background work safely across threads and virtual threads.
| Rule | Description |
|---|---|
| concurrency-avoid-double-checked-locking | Avoid double-checked locking — use the holder idiom or computeIfAbsent |
| concurrency-executor-service-lifecycle | Always shut down an ExecutorService |
| concurrency-immutable-shared-state | Prefer immutability over synchronization for shared state |
| concurrency-structured-concurrency | Use StructuredTaskScope for fan-out/fan-in tasks |
| concurrency-thread-safety-documentation | Document a class's thread-safety contract explicitly |
| concurrency-virtual-threads | Use virtual threads for blocking I/O-bound work |
API Design [HIGH]
Shape public class and method surfaces so they are hard to misuse.
| Rule | Description |
|---|---|
| api-design-builder-for-many-params | Use a builder when a constructor needs many parameters |
| api-design-fluent-method-chaining | Design fluent APIs for method chaining where the domain fits |
| api-design-minimal-visibility | Minimize the visibility of every class and member |
| api-design-no-boolean-parameter-trap | Avoid the boolean parameter trap — name the choice |
| api-design-return-interface-not-impl | Return the interface type, not the concrete implementation |
Error Handling [HIGH]
Propagate, translate, and represent failures explicitly instead of hiding or discarding them.
| Rule | Description |
|---|---|
| error-handling-checked-vs-unchecked | Choose checked vs. unchecked exceptions deliberately |
| error-handling-custom-exception-hierarchy | Design a purposeful custom exception hierarchy |
| error-handling-either-vavr | Use Vavr either for expected, recoverable failures |
| error-handling-exception-translation-at-boundary | Translate low-level exceptions at architectural boundaries |
| error-handling-no-swallowed-exceptions | Never write an empty catch block |
Immutability [HIGH]
Default to immutable state and defensive construction to eliminate whole classes of bugs.
| Rule | Description |
|---|---|
| immutability-builder-for-complex-construction | Use the builder pattern for objects with many optional fields |
| immutability-defensive-copy | Defensively copy mutable constructor inputs and getter outputs |
| immutability-final-fields | Make fields final by default |
| immutability-records-for-data | Prefer records over mutable POJOs for data carriers |
| immutability-unmodifiable-collections | Use List.copyOf/Collections.unmodifiableX at API boundaries |
Object-Oriented Design [HIGH]
Apply core object-oriented design principles for maintainable, extensible class hierarchies.
| Rule | Description |
|---|---|
| oo-design-avoid-deep-inheritance-hierarchies | Avoid deep inheritance hierarchies |
| oo-design-composition-over-inheritance | Favor composition over inheritance |
| oo-design-dependency-injection-over-static-singletons | Prefer dependency injection over static singletons |
| oo-design-favor-interfaces-for-abstraction | Favor interfaces over abstract classes for abstraction |
| oo-design-single-responsibility | Give each class a single responsibility |
Sealed Types & Pattern Matching [HIGH]
Model closed sets of variants with sealed types and exhaustive pattern matching.
| Rule | Description |
|---|---|
| sealed-pattern-no-instanceof-chains | Replace instanceof chains with exhaustive switch over a sealed type |
| sealed-pattern-permits-clause-explicit | Write the permits clause explicitly when it aids readability |
| sealed-pattern-record-patterns | Destructure sealed record hierarchies with record patterns |
| sealed-pattern-sealed-interface-over-enum-hierarchy | Model closed variant sets as a sealed interface, not an enum-and-field hack |
| sealed-pattern-switch-exhaustiveness | Switch exhaustively over sealed types — no default branch |
Javadoc [HIGH]
Document public API contracts precisely, without narrating the signature.
| Rule | Description |
|---|---|
| javadoc-no-signature-narration | Never narrate the signature in a doc comment |
| javadoc-param-return-tags-only-when-non-obvious | Write @param/@return tags only when they add information |
| javadoc-public-api-contract | Document the contract, not the implementation, on public APIs |
| javadoc-throws-documentation | Document every checked and meaningful unchecked throws |
Records [HIGH]
Use records correctly as immutable, validated data carriers.
| Rule | Description |
|---|---|
| records-canonical-constructor-validation | Validate invariants in the record's canonical constructor |
| records-compact-constructor-normalization | Normalize component values in the compact constructor |
| records-implement-interface-for-behavior | Implement an interface on a record to add shared behavior |
| records-no-mutable-fields | Never give a record a mutable component or backing field |
Reflection & Serialization [HIGH]
Keep reflection and serialization at the edges of the system, configured explicitly.
| Rule | Description |
|---|---|
| reflection-serialization-avoid-field-injection | Prefer constructor injection over reflective field injection |
| reflection-serialization-avoid-for-business-logic | Keep reflection out of business logic |
| reflection-serialization-jackson-explicit-config | Configure Jackson's ObjectMapper explicitly, never rely on defaults |
| reflection-serialization-no-default-java-serialization | Never use Java's built-in serialization for new code |
Modules [HIGH]
Manage module and package boundaries deliberately across Gradle and JPMS.
| Rule | Description |
|---|---|
| modules-api-vs-implementation-gradle-config | Declare Gradle dependencies as api or implementation deliberately |
| modules-avoid-split-packages | Never split one package across multiple modules |
| modules-jpms-explicit-exports | Export only packages that are part of the public API |
Collections & Streams [MEDIUM]
Use the Collections and Stream APIs idiomatically, without redundant materialization or hidden side effects.
| Rule | Description |
|---|---|
| collections-streams-avoid-redundant-materialization | Avoid redundant materialization between stream stages |
| collections-streams-collectors-tox | Pick the right collectors factory for the shape you need |
| collections-streams-list-of | Prefer List.of/Set.of/Map.of over mutable factory methods |
| collections-streams-no-side-effects | No mutation inside stream operations |
| collections-streams-parallel-stream-caution | Reserve parallelStream() for CPU-bound, sizable, independent work |
| collections-streams-prefer-stream-pipeline | Prefer stream pipelines over manual loops for transformations |
Generics [MEDIUM]
Use Java generics safely, without raw types or unchecked casts.
| Rule | Description |
|---|---|
| generics-avoid-generic-array-workarounds | Avoid generic array creation workarounds — use List instead |
| generics-bounded-wildcards | Use bounded wildcards at API boundaries (PECS) |
| generics-generic-method-over-class | Prefer a generic method over a generic class when only the method needs it |
| generics-no-raw-types | Never use raw generic types |
| generics-no-unchecked-casts | Avoid @SuppressWarnings("unchecked") casts outside factory internals |
Nullability [MEDIUM]
Represent absence explicitly with Optional at API boundaries, never as a field or parameter type.
| Rule | Description |
|---|---|
| nullability-no-optional-collection | Return an empty collection, never optional of a collection |
| nullability-no-optional-field | Never use optional as a field type |
| nullability-no-optional-param | Never accept optional as a method parameter |
| nullability-objects-requirenonnull | Fail fast with Objects.requireNonNull at API boundaries |
| nullability-optional-return | Return optional instead of null for absent values |
Performance [MEDIUM]
Apply measured, targeted performance techniques instead of premature optimization.
| Rule | Description |
|---|---|
| performance-array-vs-collection-hot-path | Prefer primitive arrays over boxed collections in verified hot paths |
| performance-avoid-autoboxing-hot-paths | Avoid autoboxing in hot paths |
| performance-avoid-premature-optimization | Measure before optimizing |
| performance-lazy-initialization | Defer expensive initialization until first use |
| performance-stringbuilder-for-loops | Use StringBuilder for string concatenation inside loops |
Naming [MEDIUM]
Choose names that reveal intention and avoid encoding type or scope.
| Rule | Description |
|---|---|
| naming-boolean-method-prefix | Prefix boolean methods and fields with is/has/can |
| naming-consistent-getter-avoidance | Avoid getX/setX naming for non-JavaBean types |
| naming-intention-revealing-names | Choose intention-revealing names |
| naming-no-hungarian-notation | Do not encode type or scope in names |
Quick Reference
Anti-Patterns
public void processMessage(Message message) {
try {
handler.handle(message);
} catch (MessageProcessingException e) { // specific, documented, recoverable
log.error("Failed to process message {}", message.id(), e);
deadLetterQueue.send(message);
}
// an Error propagates uncaught, as it should
}
Resource Management
public String readFirstLine(Path path) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(path)) {
return reader.readLine(); // reader.close() runs on every exit path
}
}
Concurrency
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (Request request : requests) {
executor.submit(() -> handle(request)); // one virtual thread per task
}
} // close() awaits completion of submitted tasks
API Design
public static final class Builder {
private final String url;
private final String method;
private int timeoutMs = 3000;
public Builder timeoutMs(int timeoutMs) { this.timeoutMs = timeoutMs; return this; }
public HttpRequest build() { return new HttpRequest(this); }
}
HttpRequest request = HttpRequest.builder("https://api.example.com", "GET")
.timeoutMs(5000)
.build();
Error Handling
public Either<ValidationError, Order> validate(OrderRequest request) {
if (request.items().isEmpty()) {
return Either.left(new ValidationError("order must have at least one item"));
}
return Either.right(new Order(request));
}
return validator.validate(request)
.map(orderRepository::save)
.fold(error -> ResponseEntity.badRequest().body(error.message()),
order -> ResponseEntity.ok(order));
Immutability
public record Money(BigDecimal amount, String currency) {
public Money {
Objects.requireNonNull(amount, "amount");
Objects.requireNonNull(currency, "currency");
if (amount.scale() > 2) {
throw new IllegalArgumentException("amount must have at most 2 decimal places");
}
}
}
Object-Oriented Design
public class InstrumentedList<E> {
private final List<E> delegate;
private int addCount = 0;
public boolean add(E e) {
addCount++;
return delegate.add(e); // composes List, independent of its internals
}
}
Sealed Types & Pattern Matching
sealed interface Shape permits Circle, Rectangle, Triangle {}
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
// No default: the compiler verifies every permitted Shape is covered.
};
}
Javadoc
/**
* Returns the sum of {@link Order#total()} across all given orders.
*
* @param orders the orders to total; must not be {@code null} or contain {@code null}
* @return the combined total, or {@link BigDecimal#ZERO} if {@code orders} is empty
*/
public BigDecimal totalValue(List<Order> orders) {
return orders.stream().map(Order::total).reduce(BigDecimal.ZERO, BigDecimal::add);
}
Records
public record Percentage(int value) {
public Percentage {
if (value < 0 || value > 100) {
throw new IllegalArgumentException("value must be 0-100, was " + value);
}
}
}
Percentage discount = new Percentage(150); // throws IllegalArgumentException immediately
Reflection & Serialization
public static final ObjectMapper DEFAULT = JsonMapper.builder()
.addModule(new JavaTimeModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.serializationInclusion(JsonInclude.Include.NON_ABSENT)
.build();
Modules
module com.example.billing {
exports com.example.billing.api;
requires java.sql;
// com.example.billing.internal is not exported — usable inside the
// module, invisible outside it
}
Collections & Streams
public List<String> activeAdminEmails(List<User> users) {
return users.stream()
.filter(User::isActive)
.filter(user -> user.getRole() == Role.ADMIN)
.map(User::getEmail)
.map(String::toLowerCase)
.toList();
}
Generics
// source only produces T (extends), destination only consumes T (super)
public static <T> void copy(List<? extends T> source, List<? super T> destination) {
for (T item : source) {
destination.add(item);
}
}
Nullability
public Optional<User> findById(Long id) {
return Optional.ofNullable(usersById.get(id));
}
repository.findById(id)
.map(User::getEmail)
.ifPresentOrElse(this::sendWelcomeEmail, () -> log.warn("No user found for id {}", id));
Performance
public String buildCsvLine(List<String> fields) {
StringBuilder line = new StringBuilder();
for (String field : fields) {
line.append(field).append(','); // amortized O(1) per call
}
return line.toString();
}
Naming
public class SubscriptionRenewalQueue {
private final List<Subscription> pendingRenewals = new ArrayList<>();
public void enqueueIfEligible(Subscription subscription, RenewalStatus status) {
if (status == RenewalStatus.ELIGIBLE) {
pendingRenewals.add(subscription);
}
}
}
See Also
- java-testing - Test-writing best practices for JUnit 5, AssertJ, Mockito, and Testcontainers
- java-tooling - Build, static analysis, CI quality gate, dependency management, and formatting rules