Clean methods in Java
One thing, one level of abstraction
A method should read as a single idea. Mixed levels — orchestration next to byte fiddling — is the clearest sign it is doing two jobs.
// Bad — orchestration, HTTP, parsing, and persistence in one place
public void syncOrders() {
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create(url)).build();
var response = client.send(request, BodyHandlers.ofString());
var node = mapper.readTree(response.body());
for (var item : node.get("orders")) {
var order = new Order(item.get("id").asText(), item.get("total").asDouble());
jdbc.update("INSERT INTO orders ...", order.id(), order.total());
}
}
// Good — each step is nameable and testable
public void syncOrders() {
var payload = fetchOrders();
var orders = parseOrders(payload);
orderRepository.saveAll(orders);
}
The stepdown rule: the class reads top-down
A class should read like a narrative. Public API first, then the private methods it calls, in call order — the reader descends one level of abstraction at a time and stops when they know enough.
// Good — the story, then the details underneath it
public final class OrderExporter {
public String export(List<Order> orders) {
return orders.stream().map(this::toRow).collect(joining("\n"));
}
private String toRow(Order order) {
return order.fields().stream().map(this::escapeQuotes).collect(joining(","));
}
private String escapeQuotes(String value) {
return value.replace("\"", "\"\"");
}
}
Fields at the top, constructors next, then public methods, then the private helpers each one calls. A private helper used by two public methods goes below both.
This is why one-level-of-abstraction matters practically: a method mixing orchestration with character-level detail cannot be placed in the ordering, because it belongs at two levels at once. That is the signal to split it. If a whole class resists the ordering, it has more than one responsibility.
Three parameters, then stop
// Bad
public Reservation book(String guest, LocalDate from, LocalDate to,
int guests, boolean breakfast, String notes) { ... }
// Good — the arguments were an object all along
public Reservation book(BookingRequest request) { ... }
public record BookingRequest(
String guest, DateRange stay, int guests, boolean breakfast, String notes) {}
Records make parameter objects cheap: one line, immutable, with equals, hashCode, and toString supplied. Grouping parameters that always travel together is not overhead, it is the missing concept.
Constructors with many required fields are the one place a builder still earns its keep — but check first whether the object is doing too much.
No boolean flags
A flag parameter says the method has two behaviours.
// Bad — the call site is unreadable: report(data, true, false)
public Report generate(Data data, boolean detailed, boolean includeArchived)
// Good
public Report generateSummary(Data data)
public Report generateDetailed(Data data)
If the flag genuinely selects a mode, an enum names it: generate(data, Detail.FULL).
Guard clauses over nesting
// Bad
public void process(Order order) {
if (order != null) {
if (order.isValid()) {
if (!order.isProcessed()) {
doWork(order);
}
}
}
}
// Good
public void process(Order order) {
if (!order.isValid()) {
throw new IllegalArgumentException("invalid order: " + order.id());
}
if (order.isProcessed()) {
return;
}
doWork(order);
}
Handle the exceptional path first and return; keep the real work at one indentation level.
Never return null
// Bad — every caller must remember to check
public User findUser(String id) { return null; }
// Good
public Optional<User> findUser(String id) { ... }
public List<Order> ordersFor(String id) { return List.of(); } // empty, not null
Optional belongs on return types. Do not use it for fields or parameters — an overload or a required argument is clearer. And do not accept null arguments as a design: an overload beats a nullable parameter.
Command-query separation
A method either returns a value or changes state, not both.
// Bad — did it set something, or ask something?
if (setAttribute("username", "alice")) { ... }
// Good
if (hasAttribute("username")) {
setAttribute("username", "alice");
}
Fluent builders returning this are the accepted exception.
No output parameters
Mutating an argument to communicate a result hides the effect from the signature.
// Bad
public void appendFooter(StringBuilder report)
// Good
public String withFooter(String report)
Prefer returning a new value. Records and List.copyOf make immutable results cheap.
Prefer exceptions to error codes, and be specific
Throw a meaningful exception rather than returning a status the caller can ignore. Unchecked exceptions for programmer error and unrecoverable conditions; a checked exception only when the caller has a genuine recovery path. Never swallow one:
// Bad
try { risky(); } catch (Exception e) { }
// Good
try {
risky();
} catch (IOException e) {
throw new SyncFailedException("sync orders from " + url, e); // cause preserved
}
Delete dead methods
Unused private methods, methods kept "in case", and code reachable only from a deleted feature all go. Version control remembers them.