Clean Code Principles

Writing code that reads like well-organized prose — naming, function size, honest side effects, and knowing when a hand-rolled validation method should really be a Bean Validation annotation instead

← Back to Index

What is Clean Code?

Clean code is code that's easy to understand, easy to change, and easy to test — it communicates its intent directly, rather than requiring a reader to mentally execute it line by line to figure out what it's for.

Core Philosophy

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." — Martin Fowler

Meaningful Names

Use Intention-Revealing Names

// BAD — what does this even do?
int d;
List<int[]> list1;

public List<int[]> getThem() {
    List<int[]> list1 = new ArrayList<>();
    for (int[] x : theList) {
        if (x[0] == 4)
            list1.add(x);
    }
    return list1;
}

// GOOD — clear intent, no need to guess what "4" or "x[0]" mean
int daysSinceLastOrder;
List<Order> overdueOrders;

public List<Order> getOverdueOrders() {
    List<Order> overdueOrders = new ArrayList<>();
    for (Order order : allOrders) {
        if (order.getStatus() == OrderStatus.OVERDUE) {
            overdueOrders.add(order);
        }
    }
    return overdueOrders;
}

Avoid Encodings

// BAD — Hungarian notation and prefixes the type system already tells you
String strName;
ICustomerService m_customerService;
int iCount;

// GOOD — let the type system do its job
String name;
CustomerService customerService;
int count;

Class Names vs Method Names

// Classes: nouns — what it IS
public class Customer { }
public class OrderProcessor { }
public class DiscountValidator { }

// Methods: verbs — what it DOES
public void save() { }
public Order processOrder() { }
public boolean isValid() { }

Functions Should Be Small

One Level of Abstraction Per Function

// BAD — mixes business logic with raw JDBC details in the same method
public void processOrder(Order order) {
    validateOrder(order);   // high level

    Connection conn = DriverManager.getConnection(url);   // low level
    PreparedStatement stmt = conn.prepareStatement(
        "INSERT INTO orders (id, total) VALUES (?, ?)");
    stmt.setLong(1, order.getId());
    stmt.setDouble(2, order.getTotal());
    stmt.executeUpdate();

    sendConfirmation(order);   // high level again
}

// GOOD — every line in this method is at the same level of abstraction
public void processOrder(Order order) {
    validateOrder(order);
    saveOrder(order);
    sendConfirmation(order);
}

private void saveOrder(Order order) {
    orderRepository.save(order);
}

Do One Thing

// BAD — the name itself admits the function does multiple things
public void registerCustomerAndSendEmailAndLogActivity(Customer customer) { }

// GOOD — one coordinating method, each concern its own focused function
public void registerCustomer(Customer customer) {
    validate(customer);
    save(customer);
    notifyCustomer(customer);
    auditRegistration(customer);
}

private void validate(Customer customer) { /* validation only */ }
private void save(Customer customer) { /* persistence only */ }
private void notifyCustomer(Customer customer) { /* notification only */ }
private void auditRegistration(Customer customer) { /* logging only */ }

Function Arguments

// Ideal: zero arguments (niladic)
public void run() { }

// Good: one argument (monadic)
public void process(Order order) { }

// Acceptable: two arguments (dyadic)
public BigDecimal calculateTax(BigDecimal subtotal, String region) { }

// AVOID — three or more positional arguments
public void registerCustomer(String name, String email,
                              String address, String phone) { }

// GOOD — group related arguments into a record
public record CustomerRegistration(String name, String email,
                                     String address, String phone) { }
public void registerCustomer(CustomerRegistration registration) { }

// Avoid flag (boolean) arguments — the call site doesn't tell you what "true" means
// BAD
public void renderReport(boolean isSummary) { }
renderReport(true);   // true... meaning what, exactly?

// GOOD — two named methods, self-documenting at the call site
public void renderSummaryReport() { }
public void renderDetailedReport() { }

Avoid Hidden Side Effects

// BAD — a method named "check" that also silently starts a session
public boolean isAuthenticated(String email, String hashedPassword) {
    Customer customer = customerGateway.findByEmail(email);
    if (customer != null && passwordEncoder.matches(hashedPassword, customer.getPasswordHash())) {
        Session.initialize();   // hidden side effect — the name promised a yes/no answer, not a session
        return true;
    }
    return false;
}

// GOOD — explicit and predictable: one function answers, the caller decides what to do
public boolean isAuthenticated(String email, String rawPassword) {
    Customer customer = customerRepository.findByEmail(email);
    return customer != null && passwordEncoder.matches(rawPassword, customer.getPasswordHash());
}

public void login(String email, String rawPassword) {
    if (isAuthenticated(email, rawPassword)) {
        sessionManager.initialize();   // explicit, at the call site that actually decides to do it
    }
}
Note the passwordEncoder.matches(...) above, not .equals(...)

The clean-code lesson here is about hidden side effects, but it's worth flagging explicitly: comparing passwords with String.equals() against a plaintext stored password is a separate, serious security defect — passwords must be hashed at rest and compared via a constant-time, algorithm-aware matcher like Spring Security's PasswordEncoder. See Password Hashing for the full mechanics and why a plain equals() check is a timing-attack and data-breach risk on top of being bad practice.

Comments: When and When Not

Bad Comments

// BAD — redundant, restates the next line
// increment i
i++;

// BAD — noise, adds nothing a reader doesn't already know
/** Default constructor */
public Customer() { }

// BAD — commented-out code, a Git history already tracks old versions
// customerService.oldMethod();

// BAD — actively misleading; the comment lies about what the code does
// Returns the customer's loyalty points balance in cents
public int getLoyaltyPoints() {
    return points;   // actually whole points, not cents!
}

Good Comments

// GOOD — explains WHY, which the code can't show on its own
// Insertion sort here, not the default: this list is nearly sorted
// and small (< 20 items), so insertion sort is measurably faster

// GOOD — warns of a real consequence a reader needs before running it
// Warning: this integration test suite takes ~15 minutes (Testcontainers startup)

// GOOD — clarifies genuinely obscure code, doesn't restate obvious code
// Format: YYMMDD + 4-digit sequence number
String orderId = dateFormat.format(date) + sequence;

Error Handling — Null and Optional

// BAD — returning null forces every caller to remember to check
public List<Order> getOrdersForCustomer(Long customerId) {
    if (noOrdersFound) {
        return null;
    }
    return orders;
}

// GOOD — an empty collection is a value, not a special case to remember
public List<Order> getOrdersForCustomer(Long customerId) {
    if (noOrdersFound) {
        return Collections.emptyList();
    }
    return orders;
}

// GOOD — Optional makes "might not exist" part of the method's type
public Optional<Customer> findById(Long id) {
    return Optional.ofNullable(customerMap.get(id));
}

// GOOD — fail loudly and immediately at the boundary, not silently later
public BigDecimal calculateDistance(Point origin, Point destination) {
    Objects.requireNonNull(origin, "origin must not be null");
    Objects.requireNonNull(destination, "destination must not be null");
    // safe to proceed
}

DRY — Don't Repeat Yourself

// BAD — the same validation logic duplicated across two unrelated methods
public void validateCustomer(Customer customer) {
    if (customer.getFullName() == null || customer.getFullName().isEmpty()) {
        throw new ValidationException("Name required");
    }
    if (customer.getFullName().length() > 100) {
        throw new ValidationException("Name too long");
    }
}

public void validateProduct(Product product) {
    if (product.getName() == null || product.getName().isEmpty()) {
        throw new ValidationException("Name required");
    }
    if (product.getName().length() > 100) {
        throw new ValidationException("Name too long");
    }
}

// BETTER — extract the shared logic once
private void validateName(String name, String fieldName) {
    if (name == null || name.isEmpty()) {
        throw new ValidationException(fieldName + " required");
    }
    if (name.length() > 100) {
        throw new ValidationException(fieldName + " too long");
    }
}
In a Spring/Jakarta app, this specific duplication usually shouldn't be hand-rolled at all

Extracting a shared validateName() method is the right refactoring technique to know — but for a plain field-level constraint like "required, max 100 characters," the more idiomatic modern answer is declaring it once on the DTO and letting the framework enforce it everywhere that DTO is used:

public record CustomerRequest(
    @NotBlank(message = "Name required")
    @Size(max = 100, message = "Name too long")
    String fullName
) { }

@PostMapping
public ResponseEntity<Customer> create(@Valid @RequestBody CustomerRequest request) { ... }

Reach for a hand-written validation method when the rule is a genuine business rule that spans multiple fields or requires a database lookup (an order total that must match its line items, an email that must be unique) — that's logic Bean Validation's declarative annotations can't express cleanly, and it's exactly where DRY's extract-a-method advice above still applies directly.

Code Organization — the Newspaper Metaphor

// A class read top to bottom like a newspaper article:
// - Headline (class name) tells you what it's about
// - High-level summary first (public methods)
// - Details come later (private methods)

public class OrderService {

    // PUBLIC — the "headline": high-level operations a caller reasons about
    public Order createOrder(OrderRequest request) {
        validate(request);
        Order order = buildOrder(request);
        return save(order);
    }

    public void cancelOrder(Long orderId) {
        Order order = findOrder(orderId);
        markAsCancelled(order);
        notifyCustomer(order);
    }

    // PRIVATE — the "body": implementation details, only relevant once
    // you've decided you care how createOrder() actually works
    private void validate(OrderRequest request) { ... }
    private Order buildOrder(OrderRequest request) { ... }
    private Order save(Order order) { ... }
    private Order findOrder(Long id) { ... }
    private void markAsCancelled(Order order) { ... }
    private void notifyCustomer(Order order) { ... }
}

Best Practices and Common Pitfalls

✅ Do

  • Name things by intent — a reader shouldn't need the implementation to understand a variable or method's purpose
  • Keep every line in a function at the same level of abstraction — extract low-level details into their own named method
  • Group three or more related arguments into a record instead of a long parameter list
  • Return an empty collection or Optional instead of null — make "might be absent" part of the type, not a caller's responsibility to remember
  • Use Bean Validation annotations for simple field constraints; hand-write validation only for genuine cross-field or lookup-based business rules

❌ Don't

  • Don't use Hungarian notation or type-encoding prefixes — the compiler already tracks the type
  • Don't hide a side effect (starting a session, sending an email) inside a method whose name promises only a check or a calculation
  • Don't leave commented-out code in the codebase — Git history already preserves it, and stale commented code actively misleads readers
  • Don't write a comment that just restates the line below it in English
  • Don't compare passwords with .equals() — always via a proper password encoder, see Password Hashing

Interview Questions

🎓 Junior level

Q: Why is getFlaggedOrders() a better method name than getThem()?
A good name lets a reader understand what a method returns without reading its implementation. getThem() reveals nothing; getFlaggedOrders() tells the caller exactly what to expect, which is what "intention-revealing" means in practice.

Q: Why should a method avoid returning null for a collection?
It forces every single caller to remember to null-check before iterating, and a caller who forgets gets a NullPointerException at runtime. Returning an empty collection means "nothing found" is just a normal, always-safe-to-iterate value.

Q: What's wrong with a boolean "flag" argument like render(boolean isSummary)?
At the call site, render(true) gives the reader no idea what true actually means without looking up the method signature. Two separately named methods (renderSummary(), renderDetailed()) are self-documenting at every call site.

🔥 Senior level

Q: A method named isAuthenticated() also silently initializes a session as a side effect. Explain precisely why this is dangerous beyond just being "unclear," using a concrete scenario.
A method whose name promises a pure yes/no check creates an implicit contract that calling it is safe to repeat, safe to call speculatively, and free of consequences beyond the returned value — which is exactly what a reader, and more importantly a future caller who didn't read the implementation, will assume. If a second piece of code later calls isAuthenticated() purely to check status before showing a UI element, it silently triggers a second session initialization as an unintended side effect — potentially resetting session state, duplicating audit log entries, or interacting badly with concurrent requests, none of which is visible at that call site or discoverable without reading isAuthenticated()'s full implementation. The fix isn't just "add a comment" — it's separating the query (isAuthenticated(), pure) from the command (login(), which explicitly performs the session side effect), so the method name is never lying about what calling it does.

Q: A team extracts a shared validateName(String, String) helper to avoid duplicating a required/max-length check across five DTOs. Is this good DRY, and when would Bean Validation annotations be the better answer instead?
It's a reasonable mechanical fix for the duplication as described, but for a plain field-level constraint with no cross-field or external-lookup logic, a hand-rolled helper method duplicates something the framework already provides declaratively: @NotBlank and @Size(max = 100) on the field itself, enforced automatically via @Valid at every controller boundary that accepts the DTO, with zero custom code to maintain and a validation error format the framework already handles consistently. The helper-method approach becomes the right tool specifically once the rule stops being a pure field constraint — validating that an order's total matches the sum of its line items, or that an email address is unique in the database — because those require logic and data access that a declarative field annotation structurally cannot express. The DRY principle applies in both cases; which mechanism correctly avoids the repetition depends on whether the rule is a static constraint on one field or genuine business logic.

Q: The "one level of abstraction per function" principle says processOrder() shouldn't mix business logic with raw JDBC calls. Why does this matter beyond aesthetics — what's the concrete cost of ignoring it?
Mixed abstraction levels make a function's essential logic and its incidental implementation detail equally prominent, which has a real, measurable cost when the code changes: a reader trying to understand what processOrder() does at a business level has to visually filter out SQL and JDBC boilerplate to find the three lines that actually matter, every single time the method is read — and it will be read far more times than it's written. It also directly increases the blast radius of unrelated changes: swapping the persistence library, or moving to a repository abstraction, now means editing the same method that contains the business validation and confirmation-sending logic, increasing the chance a change meant to touch only persistence accidentally breaks something adjacent it didn't need to touch. Extracting the low-level detail into its own well-named method isn't cosmetic — it's the same containment benefit the Single Responsibility Principle provides at the class level, applied one level down, inside a single method's own internal structure.