Clean Java is not magic, it is a pile of small choices you make every day.
With Java 11 now the long term release, teams are juggling upgrades while still living with code born in Java 8. Nothing wrong with that. The point is the same. Readability wins. Name things like you are writing for a teammate who just joined. Split big methods into smaller ones with clear verbs. Prefer guard clauses to nested conditionals. If you use var, use it when the type is obvious from the right side. If the type is not obvious, spell it out. Comments are fine, but code should say the thing without a novel beside it. Your future self will thank your present self when that ticket lands on your lap on a Friday afternoon.
Guard clauses cut nesting and make the happy path pop. Avoid the zen garden of braces and else blocks. A quick return keeps the method short and the eyes calm.
public Order loadOrder(UUID id) {
if (id == null) {
throw new IllegalArgumentException("id is required");
}
Order order = repo.findById(id);
if (order == null) {
return Order.empty();
}
return order;
}Null is still a thing in many codebases, but Optional helps make intent clear. Do not sprinkle Optional everywhere. Use it on return types to say maybe. Avoid it for fields and parameters unless it really makes the call site cleaner. Also keep streams tight. A stream chain with five operations can be lovely, a stream chain with fifteen is a maze. When a stream stops fitting on the screen, extract steps into named methods and tell a story. Short names for small scopes, full names for big ideas. If a boolean flag sneaks into a method, consider an enum or split the method. Booleans inside a call tend to hide meaning.
public Optional<User> findActiveUser(String email) {
if (email == null || email.isEmpty()) return Optional.empty();
return users.stream()
.filter(u -> u.getEmail().equalsIgnoreCase(email))
.filter(User::isActive)
.findFirst();
}Local variable type inference with var is here and it is nice when the type is screaming at you from the right side. Use var for builders, map entries, or test data, and keep explicit types when the method call chain hides the concrete type. The goal is signal over noise. Speaking of signal, keep objects small and stable. Immutability cuts surprises and makes reasoning easy. Java records are not in reach yet, so a tiny value object with finals and no setters still does the job. A builder reads well for bigger objects, but you can start simple and stay simple if the data is simple.
public final class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
if (amount == null || currency == null) {
throw new IllegalArgumentException("amount and currency are required");
}
this.amount = amount;
this.currency = currency;
}
public BigDecimal amount() { return amount; }
public Currency currency() { return currency; }
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("currency mismatch");
}
return new Money(amount.add(other.amount), currency);
}
}Small refactors take you far. Rename a fuzzy method. Inline a pointless variable. Extract a tiny method. Delete dead code like you are tidying a desk. Let your IDE guide you. Inspections in IntelliJ or Eclipse warnings are like a second pair of eyes. Write unit tests that read like examples, not novels. Keep assertions on the left, setup on the right, and avoid clever test logic. Clean code is not a style contest. It is about making change cheap. When people can scan a file and nod, you can ship with less stress.
Goal for this week: pick one class you touch daily and make it 10 percent clearer. Names, guards, small methods.
Cleaner Java starts with one pull request and a promise to keep the bar steady.