# Java Clean Code

> Complete Clean Code reference for Java 21+ in one skill — naming, methods, comments, general quality, and tests, with modern idioms (records, sealed types, pattern matching, Optional, streams). Use when writing, reviewing, or refactoring any Java code and you want the whole catalog at once rather than one focused area.

- Skill: `caslubbers/java-clean-code` (Agent Skill)
- Install (CLI): `npx skillmds@latest add caslubbers/java-clean-code`
- Raw SKILL.md: https://api.skillmd.com/api/skills/caslubbers/java-clean-code/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: CasLubbers (https://skillmd.com/u/caslubbers)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/caslubbers/java-clean-code

---


# Clean Java: complete reference

The full catalog. For depth on one area use the focused skills: `java-clean-names`, `java-clean-functions`, `java-clean-comments`, `java-clean-general`, `java-clean-tests`, `java-boy-scout`.

## Names

- **N1** Names reveal intent. If it needs a comment, rename it.
- **N2** Name at the caller's level of abstraction — `ordersByCustomer()`, not `getOrderHashMap()`.
- **N3** Use standard nomenclature: domain terms, pattern names, `find`/`create`/`delete` consistently.
- **N4** Unambiguous. `rename(source, target)` beats `rename(a, b)`.
- **N5** Length matches scope. `var` shifts the weight onto the name.
- **N6** No encodings: no `strName`, no `m_count`, no `IUserRepository`, no `UserServiceImpl`.
- **N7** The name describes every side effect. A getter that loads is `getOrLoad`.
- **N8** No noise words: `Manager`, `Helper`, `Data`, `Info`, `Util` distinguish nothing.
- **N9** Conventions: `PascalCase` noun types, `camelCase` verb methods, `UPPER_SNAKE` constants, predicate booleans (`isActive`, `hasExpired`). Record-style accessors (`order.total()`) over `getTotal()` in new domain types.

## Methods

- **F1** One thing, one level of abstraction.
- **F2** Small. If you cannot name it precisely, it does more than one thing.
- **F3** Three parameters maximum. Group the rest into a record.
- **F4** No boolean flag arguments — split the method, or pass an enum.
- **F5** Guard clauses over nesting. Return early.
- **F6** Never return null. `Optional<T>` for absence, `List.of()` for empty.
- **F7** `Optional` on return types only — not fields, not parameters.
- **F8** Command-query separation: return a value or change state, not both.
- **F9** No output parameters. Return a new value.
- **F10** Throw meaningful exceptions, preserve the cause, never swallow.
- **F11** Delete dead methods.
- **F12** The stepdown rule: public API first, helpers below in call order, one level of abstraction per method. A class that resists the ordering has more than one responsibility.

## Comments

- **C1** No metadata: no `@author`, no dates, no ticket history. Git owns that.
- **C2** No commented-out code. Ever.
- **C3** No redundant Javadoc that restates the signature.
- **C4** Javadoc documents the contract: preconditions, exceptions and when, thread safety, nullability, units.
- **C5** TODOs carry an owner and an issue reference, or they are permanent.
- **C6** Comments explain *why*, never what.
- **C7** A comment that contradicts the code is worse than no comment. Update it in the same commit.

## General

- **G1** DRY — but only for logic that is genuinely the same rule, not coincidentally equal.
- **G2** No magic numbers or strings. Named constants.
- **G3** Money is `BigDecimal` or a dedicated type. Never `double`.
- **G4** Immutable by default: records, `final` fields, defensive copies, `List.copyOf`.
- **G5** Enforce invariants in the compact constructor. An object that cannot be built invalid never needs revalidating.
- **G6** Sealed interfaces plus exhaustive `switch` over `instanceof` chains. Polymorphism when the behaviour belongs to the type.
- **G7** Tell, don't ask. Move behaviour onto the class that owns the data.
- **G8** Law of Demeter — one dot. `order.shippingCountryCode()`, not `order.getCustomer().getAddress().getCountry().getCode()`.
- **G9** Streams where they clarify; a loop where a stream needs a comment.
- **G10** Validate at the boundary, then trust the core.
- **G11** Composition over inheritance — inherit only where the subtype is substitutable, compose for reuse. `final` on classes not designed for extension.
- **G12** One public class per file, private fields, callers above callees.
- **G13** Delete dead code — unused fields, unreachable branches, obsolete flags.

## Tests

- **T1** Test names state the behaviour: `withdrawFailsWhenBalanceIsInsufficient`.
- **T2** One reason to fail per test.
- **T3** Arrange / act / assert, visibly separated.
- **T4** AssertJ for failure messages that name expected and actual.
- **T5** `@ParameterizedTest` for repeated shapes.
- **T6** Test the boundaries: empty, zero, negative, maximum, off-by-one, duplicates.
- **T7** Fast and isolated — no real I/O, inject a `Clock`, no shared static state.
- **T8** Mock at the boundary, and only what you own. Assert results over interactions.
- **T9** No `@Disabled` without a reason and a ticket.
- **T10** Flaky means broken. Fix the race, do not retry it.
- **T11** Coverage is a map of untested code, not a target.

## Quick reference

| Don't | Do |
|---|---|
| `IUserRepository` / `UserServiceImpl` | `UserRepository` / `JdbcUserRepository` |
| `return null;` | `return Optional.empty();` / `List.of()` |
| `process(data, true, false)` | `processDetailed(data)` |
| `double price = 19.99;` | `Money price = Money.euros("19.99");` |
| `if (x instanceof A) … else if (x instanceof B)` | `sealed interface` + exhaustive `switch` |
| `catch (Exception e) { }` | `throw new DomainException("context", e);` |
| `getBalance()` then `setBalance()` | `account.withdraw(amount)` |
| `a.getB().getC().getD()` | `a.d()` |
| 6-parameter constructor | a `record` parameter object |
| `@Disabled` | `@Disabled("PLAT-1182: flaky clock")` |
| Class of only getters and setters | Behaviour moved onto the class |
| `class Notifier extends SmtpClient` | `class Notifier` holding an `SmtpClient` |
| Private helpers above the public method | Public API first, helpers below in call order |
| Comment explaining a condition | A named method: `isEligible()` |

## Applying this

Fix what you touch, not the whole file. Behaviour changes and cleanups go in separate commits. Every change runs the test suite before it counts as done, and a cleanup that breaks a test was not a cleanup.

