Writing Clean and Readable Java Code — Best Practices

4 minute read

Writing code that works is just the beginning. Writing code that others (and future you) can read and maintain is what separates beginners from professionals.


Why Clean Code Matters

  • You read code 10× more than you write it
  • Most bugs come from misunderstanding existing code
  • Clean code is easier to debug, test, and extend
  • Teams move faster with readable codebases

Naming Conventions

Java Standard Naming

Element Convention Example
Classes PascalCase BankAccount, HttpClient
Methods camelCase calculateTotal(), getUserName()
Variables camelCase firstName, totalAmount
Constants UPPER_SNAKE MAX_SIZE, DEFAULT_TIMEOUT
Packages lowercase com.example.utils

Good Names Are Self-Documenting

// Bad
int d; // elapsed time in days
int s; // student count
List<int[]> list1;

// Good
int elapsedTimeInDays;
int studentCount;
List<int[]> flaggedCells;

Boolean Naming

Use is, has, can, should prefixes:

boolean isActive;
boolean hasPermission;
boolean canEdit;
boolean shouldRetry;

Method Naming

Use verbs that describe the action:

// Bad
data();
process();
handle();

// Good
fetchUserData();
calculateShippingCost();
validateEmailAddress();

Formatting and Style

Consistent Indentation

Use 4 spaces (Java standard). Never mix tabs and spaces.

Braces on the Same Line

// Java convention (K&R style)
if (condition) {
    doSomething();
} else {
    doOther();
}

One Statement Per Line

// Bad
int a = 1; int b = 2; int c = 3;

// Good
int a = 1;
int b = 2;
int c = 3;

Blank Lines for Readability

public class OrderService {

    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    public Order findById(long id) {
        return repository.findById(id)
                .orElseThrow(() -> new OrderNotFoundException(id));
    }

    public void cancel(Order order) {
        order.setStatus(Status.CANCELLED);
        repository.save(order);
    }
}

Method Design

Keep Methods Short

A method should do one thing and fit on one screen (~20 lines max).

// Bad — does too many things
public void processOrder(Order order) {
    // validate (10 lines)
    // calculate totals (15 lines)
    // apply discounts (10 lines)
    // save to database (5 lines)
    // send confirmation email (10 lines)
}

// Good — each step is a method
public void processOrder(Order order) {
    validate(order);
    calculateTotals(order);
    applyDiscounts(order);
    save(order);
    sendConfirmation(order);
}

Limit Parameters

// Bad — too many parameters
public void createUser(String name, String email, String phone,
                       String address, String city, String country) { }

// Good — use an object
public void createUser(UserRequest request) { }

Return Early

// Bad — deep nesting
public double calculateDiscount(Customer c) {
    if (c != null) {
        if (c.isPremium()) {
            if (c.getOrders() > 10) {
                return 0.20;
            } else {
                return 0.10;
            }
        } else {
            return 0.05;
        }
    }
    return 0;
}

// Good — guard clauses
public double calculateDiscount(Customer c) {
    if (c == null) return 0;
    if (!c.isPremium()) return 0.05;
    if (c.getOrders() > 10) return 0.20;
    return 0.10;
}

Avoid Magic Numbers

// Bad
if (age > 17) { }
Thread.sleep(86400000);

// Good
final int LEGAL_AGE = 18;
if (age >= LEGAL_AGE) { }

final long ONE_DAY_MS = 24 * 60 * 60 * 1000L;
Thread.sleep(ONE_DAY_MS);

DRY — Don’t Repeat Yourself

// Bad — duplicated validation
public void saveUser(User user) {
    if (user.getName() == null || user.getName().isBlank()) {
        throw new ValidationException("Name required");
    }
    // save...
}

public void updateUser(User user) {
    if (user.getName() == null || user.getName().isBlank()) {
        throw new ValidationException("Name required");
    }
    // update...
}

// Good — extract common logic
private void validateUser(User user) {
    if (user.getName() == null || user.getName().isBlank()) {
        throw new ValidationException("Name required");
    }
}

Code Smells to Avoid

Smell Problem Fix
Long methods Hard to understand Extract smaller methods
Deep nesting Hard to follow Guard clauses, extract methods
Magic numbers Unclear meaning Named constants
Commented-out code Clutter Delete it (use Git)
God class Does everything Split into focused classes
Duplicate code Maintenance nightmare Extract shared method/class

Comments: When and How

// BAD — states the obvious
i++; // increment i

// GOOD — explains WHY
// Retry 3 times because the external API has intermittent failures
for (int attempt = 0; attempt < 3; attempt++) { ... }

// BEST — code explains itself, no comment needed
final int MAX_API_RETRIES = 3;
for (int attempt = 0; attempt < MAX_API_RETRIES; attempt++) { ... }

Use Modern Java Features

// Old
String result;
if (value != null) {
    result = value;
} else {
    result = "default";
}

// Modern
String result = Objects.requireNonNullElse(value, "default");

// Old
List<String> filtered = new ArrayList<>();
for (String s : list) {
    if (s.startsWith("A")) {
        filtered.add(s.toUpperCase());
    }
}

// Modern
List<String> filtered = list.stream()
        .filter(s -> s.startsWith("A"))
        .map(String::toUpperCase)
        .toList();

Quick Checklist

Before committing code, ask yourself:

  • Are my names descriptive?
  • Are methods under 20 lines?
  • Is there duplicated code I can extract?
  • Are there magic numbers?
  • Can I reduce nesting?
  • Would a new reader understand this?

Summary

Principle Action
Meaningful names Self-documenting code
Short methods One responsibility each
Guard clauses Reduce nesting
Named constants No magic numbers
DRY Extract common code
Modern features Use streams, records, var

What’s Next?

The final chapter of Core Java Basics — learn how to find and fix bugs effectively: Debugging in Java.

Updated: