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(), notgetOrderHashMap(). - N3 Use standard nomenclature: domain terms, pattern names,
find/create/deleteconsistently. - N4 Unambiguous.
rename(source, target)beatsrename(a, b). - N5 Length matches scope.
varshifts the weight onto the name. - N6 No encodings: no
strName, nom_count, noIUserRepository, noUserServiceImpl. - N7 The name describes every side effect. A getter that loads is
getOrLoad. - N8 No noise words:
Manager,Helper,Data,Info,Utildistinguish nothing. - N9 Conventions:
PascalCasenoun types,camelCaseverb methods,UPPER_SNAKEconstants, predicate booleans (isActive,hasExpired). Record-style accessors (order.total()) overgetTotal()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
Optionalon 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
BigDecimalor a dedicated type. Neverdouble. - G4 Immutable by default: records,
finalfields, 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
switchoverinstanceofchains. 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(), notorder.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.
finalon 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
@ParameterizedTestfor 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
@Disabledwithout 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.