Test frameworks grow into large codebases, and they need the same discipline as product code. This guide gathers the Java best practices that matter most: clean naming and small functions, SOLID, logging, the most useful Effective Java rules, immutable objects, a code review checklist, and the anti-patterns to remove when you see them.

Common Java Anti-Patterns to Avoid

// ❌ ANTI-PATTERN 1: Returning null instead of Optional
public User findUser(int id) { return null; }  // callers forget null check!
// ✅ Better:
public Optional<User> findUser(int id) { return Optional.ofNullable(...); }

// ❌ ANTI-PATTERN 2: Catching and ignoring exceptions
try { riskyOp(); } catch (Exception e) {}  // silent failure is deadly!
// ✅ Better: log it, rethrow, or handle meaningfully
try { riskyOp(); } catch (Exception e) { log.error("Failed", e); throw e; }

// ❌ ANTI-PATTERN 3: String concatenation in loop
String result = "";
for (int i=0; i<1000; i++) result += i;  // 1000 String objects!
// ✅ Better:
StringBuilder sb = new StringBuilder();
for (int i=0; i<1000; i++) sb.append(i);

// ❌ ANTI-PATTERN 4: Using raw types
List list = new ArrayList();    // no type safety!
list.add("string"); list.add(42); // mixed types = runtime error
// ✅ Better:
List<String> list = new ArrayList<>();

// ❌ ANTI-PATTERN 5: == for String comparison
if (name == "Alice") { }   // might work by coincidence (pool)!
// ✅ Better:
if ("Alice".equals(name)) { }  // null-safe & value-correct

// ❌ ANTI-PATTERN 6: Not closing resources
FileReader fr = new FileReader("f.txt");  // never closed = memory leak!
// ✅ Better:
try (FileReader fr = new FileReader("f.txt")) { }

// ❌ ANTI-PATTERN 7: Mutable static state
class Config { public static String env = "prod"; } // shared mutable = threading nightmare
// ✅ Better: immutable config, dependency injection

// ❌ ANTI-PATTERN 8: Premature optimization
// Don't optimize before profiling! First make it correct, then fast.

// ❌ ANTI-PATTERN 9: God class (one class doing everything)
// ✅ Better: Single Responsibility Principle — one class, one job

// ❌ ANTI-PATTERN 10: Deep inheritance hierarchies
// ✅ Better: Favor composition over inheritance
Advertisement

Clean Code Principles

Naming Conventions

// ❌ BAD naming
int d;                    // what is d?
void proc(List l) {}      // proc? l?
boolean flag = true;
String temp;

// ✅ GOOD naming
int daysSinceLastLogin;
void sendPasswordResetEmail(List<User> expiredUsers) {}
boolean isEmailVerified = true;
String currentUserEmail;

// Classes: Noun phrases
class UserService {}         // ✅
class Manager {}             // ❌ — manager of what?
class DataProcessor {}       // ❌ — too generic
class OrderItemProcessor {}  // ✅ — specific

// Methods: Verb phrases
getUserById()     // ✅
sendEmail()       // ✅
isValid()         // ✅ (boolean predicate)
process()         // ❌ — process what?
doStuff()         // ❌ — never!

// Constants: UPPER_SNAKE_CASE
static final int MAX_RETRY_ATTEMPTS = 3;
static final String DEFAULT_ENCODING = "UTF-8";

// Boolean naming: is/has/can/should
boolean isActive = true;
boolean hasPermission = false;
boolean canDelete = false;
boolean shouldSendEmail = true;

Function Design

// ✅ Rule: Functions should do ONE thing
// ❌ BAD: does many things
public void processUser(User user) {
    // validate
    if (user.getName() == null) throw new Exception("name required");
    if (user.getEmail() == null) throw new Exception("email required");
    // save to db
    userRepo.save(user);
    // send email
    emailService.sendWelcome(user.getEmail());
    // update stats
    statsService.incrementUserCount();
}

// ✅ GOOD: each function does one thing
public void registerUser(User user) {
    validateUser(user);
    userRepo.save(user);
    onUserRegistered(user);
}
private void validateUser(User user) { /* validation only */ }
private void onUserRegistered(User user) {
    emailService.sendWelcome(user.getEmail());
    statsService.incrementUserCount();
}

// ✅ Limit parameters (max 3, ideally 0-2)
// ❌ BAD
void createOrder(String userId, String productId, int qty, String address,
    String paymentMethod, String coupon, boolean express) {}

// ✅ GOOD: use a request object
void createOrder(CreateOrderRequest request) {}

// ✅ Avoid side effects
// ❌ BAD: name suggests it just checks but also modifies state
boolean checkAndUpdatePassword(String oldPass, String newPass) {
    boolean valid = passwordEncoder.matches(oldPass, user.getPassword());
    if (valid) user.setPassword(passwordEncoder.encode(newPass)); // SIDE EFFECT!
    return valid;
}
// ✅ GOOD: separate concerns
boolean isPasswordValid(String raw, String encoded) {
    return passwordEncoder.matches(raw, encoded);
}
void updatePassword(String newPassword) {
    user.setPassword(passwordEncoder.encode(newPassword));
}

SOLID Principles in Practice

// S — Single Responsibility
// ❌ BAD: Report does too many things
class Report {
    void generateData() {}
    void formatAsHTML() {}
    void saveToFile() {}
    void sendByEmail() {}
}
// ✅ GOOD: separate classes
class ReportDataGenerator { void generate() {} }
class HtmlFormatter { void format(ReportData data) {} }
class ReportSaver { void save(Report r, String path) {} }
class ReportEmailer { void send(Report r, String to) {} }

// O — Open/Closed Principle
// ❌ BAD: add new shape = modify existing code
double calculateArea(Object shape) {
    if (shape instanceof Circle) return Math.PI * ((Circle)shape).radius * ((Circle)shape).radius;
    if (shape instanceof Rectangle) return ((Rectangle)shape).width * ((Rectangle)shape).height;
    // must modify this method for every new shape!
}
// ✅ GOOD: add new shape = add new class (no modification)
interface Shape { double area(); }
class Circle implements Shape { double area() { return Math.PI * r * r; } }
class Rectangle implements Shape { double area() { return w * h; } }
// New shape: just add new class implementing Shape

// D — Dependency Inversion
// ❌ BAD: high-level depends on low-level
class OrderService {
    MySQLOrderRepository repo = new MySQLOrderRepository(); // hardcoded!
}
// ✅ GOOD: depend on abstraction
interface OrderRepository { Order save(Order o); }
class OrderService {
    private final OrderRepository repo; // abstraction
    OrderService(OrderRepository repo) { this.repo = repo; } // injected
}
// Can swap MySQL → MongoDB without changing OrderService

Java Logging Best Practices

// Use SLF4J API (facade) + Logback implementation
// Never use System.out.println in production!

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class UserService {
    // One logger per class (static final)
    private static final Logger log = LoggerFactory.getLogger(UserService.class);
    // Or with Lombok: @Slf4j annotation

    public User createUser(User user) {
        log.debug("Creating user: {}", user.getEmail()); // dev only
        try {
            User saved = repo.save(user);
            log.info("User created successfully: id={}, email={}",
                saved.getId(), saved.getEmail());
            return saved;
        } catch (DataIntegrityViolationException e) {
            log.warn("Duplicate email attempt: {}", user.getEmail());
            throw new DuplicateEmailException(user.getEmail());
        } catch (Exception e) {
            log.error("Failed to create user: {}", user.getEmail(), e); // include exception!
            throw e;
        }
    }
}

// Log levels (in order):
// TRACE: finest detail (method entry/exit) — rarely used
// DEBUG: developer info (queries, values) — dev env only
// INFO: key business events (user created, order placed)
// WARN: unexpected but recoverable (retry, fallback used)
// ERROR: failures that need attention (exceptions, data issues)

// application.properties
// logging.level.com.myapp=DEBUG
// logging.level.org.hibernate.SQL=DEBUG
// logging.file.name=logs/app.log
// logging.logback.rollingpolicy.max-file-size=10MB
// logging.logback.rollingpolicy.max-history=30

// ❌ NEVER log sensitive data
log.info("Login: user={} password={}", username, password); // NEVER!
log.info("Card: {}", cardNumber); // NEVER!

Code Review Checklist for Java

  • ✅ No raw types (use generics: List<String> not List)
  • ✅ Resources closed with try-with-resources
  • ✅ equals() and hashCode() both overridden when one is
  • ✅ Checked exceptions declared or handled
  • ✅ No System.out.println — use proper logger
  • ✅ No hardcoded credentials or URLs in code
  • ✅ Immutable objects where possible (final fields)
  • ✅ Thread-safe collections for shared mutable state
  • ✅ StringBuilder for string concatenation in loops
  • ✅ Input validated before processing
  • ✅ SQL uses PreparedStatement (never string concat)
  • ✅ No swallowed exceptions (empty catch blocks)
  • ✅ Collections returned as unmodifiable when appropriate
  • ✅ Magic numbers replaced with named constants
  • ✅ Methods < 20 lines, classes < 300 lines
  • ✅ Unit tests for all public methods
  • ✅ Meaningful variable/method/class names
  • ✅ No unused imports or dead code
  • ✅ Null-safe code or Optional used for nullable returns
  • ✅ Static utility methods in utility classes are final class + private constructor

Effective Java Best Practices (Bloch)

// ITEM 1: Use static factory methods instead of constructors
// ✅ Benefits: meaningful names, can return cached instances, can return subtype
public class Color {
    private static final Color RED = new Color(255, 0, 0);
    private Color(int r, int g, int b) { }
    public static Color red() { return RED; } // named, cached
    public static Color of(int r, int g, int b) { return new Color(r,g,b); }
}
// Boolean.valueOf(true) — doesn't create new object each time!

// ITEM 2: Builder for many parameters
// Already covered — always use Builder over telescoping constructors

// ITEM 3: Singleton with Enum
public enum Singleton { INSTANCE; }

// ITEM 4: Enforce noninstantiability
public class MathUtils {
    private MathUtils() { throw new AssertionError("Do not instantiate"); }
    public static int add(int a, int b) { return a + b; }
}

// ITEM 5: Prefer dependency injection over hardcoding resources
// Instead of: private final SpellChecker dict = new SpellChecker(DICTIONARY);
// Use: inject Lexicon as constructor parameter

// ITEM 6: Avoid unnecessary object creation
// ❌ BAD: creates new Pattern each call
boolean isRomanNumeral(String s) {
    return s.matches("^M{0,3}(CM|CD|D?C{0,3})...");
}
// ✅ GOOD: compile once, reuse
private static final Pattern ROMAN = Pattern.compile("^M{0,3}...");
boolean isRomanNumeral(String s) { return ROMAN.matcher(s).matches(); }

// ITEM 7: Eliminate obsolete object references
// Arrays that grow: null out elements on removal
public T pop() {
    T element = elements[--size];
    elements[size] = null; // eliminate obsolete reference!
    return element;
}

// ITEM 8: Avoid finalizers and cleaners
// Use try-with-resources (AutoCloseable) instead

// ITEM 9: Prefer try-with-resources to try-finally
// Already covered

// ITEM 10: Obey equals contract
// Reflexive: x.equals(x) = true
// Symmetric: x.equals(y) = y.equals(x)
// Transitive: x.equals(y) && y.equals(z) → x.equals(z)
// Consistent: multiple calls return same result
// Non-nullity: x.equals(null) = false

// ITEM 11: Always override hashCode with equals
@Override public int hashCode() {
    return Objects.hash(field1, field2, field3);
}

// ITEM 12: Always override toString
@Override public String toString() {
    return "Person{name='" + name + "', age=" + age + "}";
}

// ITEM 13: Override clone judiciously — prefer copy constructor
// ❌ clone() is tricky with inheritance and final fields
// ✅ copy constructor:
public Person(Person original) {
    this.name = original.name;
    this.skills = new ArrayList<>(original.skills); // deep copy
}

// ITEM 14: Consider implementing Comparable
// Already covered

// ITEM 15: Minimize accessibility (private by default)
// Make everything as private as possible
// Public only what's necessary for API

// ITEM 16: In public classes use accessor methods not public fields
// Always: private field + public getter/setter

// ITEM 17: Minimize mutability (immutable when possible)
// Use final, no setters, defensive copies

// ITEM 18: Favor composition over inheritance
// Inheritance breaks encapsulation (subclass depends on impl details)
// Composition: has-a relationship, more flexible

// ITEM 19: Design and document for inheritance or prohibit it
// If not designed for extension: make it final
public final class UtilClass { }

// ITEM 20: Prefer interfaces to abstract classes
// Interfaces: mixins, multiple inheritance, easy to retrofit
// Abstract class: when sharing implementation is required

Immutable Object Design

// Rules for immutable objects:
// 1. final class (no subclassing)
// 2. All fields private final
// 3. No setters
// 4. Deep copy mutable fields in constructor and getters
// 5. Don't leak 'this' in constructor

public final class ImmutablePerson {
    private final String name;
    private final int age;
    private final List<String> hobbies; // mutable — must deep copy!
    private final Date birthDate;       // mutable — must deep copy!

    public ImmutablePerson(String name, int age,
            List<String> hobbies, Date birthDate) {
        this.name = name;
        this.age = age;
        this.hobbies = List.copyOf(hobbies);    // unmodifiable copy
        this.birthDate = new Date(birthDate.getTime()); // defensive copy
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public List<String> getHobbies() { return hobbies; } // already unmodifiable
    public Date getBirthDate() { return new Date(birthDate.getTime()); } // copy on return

    // 'With' methods return NEW instance (like Records)
    public ImmutablePerson withAge(int newAge) {
        return new ImmutablePerson(this.name, newAge, this.hobbies, this.birthDate);
    }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ImmutablePerson p)) return false;
        return age == p.age && Objects.equals(name, p.name);
    }
    @Override public int hashCode() { return Objects.hash(name, age); }
    @Override public String toString() { return name + "(" + age + ")"; }
}

FAQs

What are the SOLID principles?

Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion: five guidelines for classes that are easy to change and test.

Why prefer composition over inheritance?

Inheritance couples a subclass to its parent's implementation; composition combines small objects through interfaces, so behaviour can change without deep class hierarchies.

How do you make a class immutable in Java?

Make the class final, fields private and final, set them only in the constructor, return copies of mutable fields, and provide no setters; or use a record for simple data.