Java Code Conventions

Naming rules that actually have a reason behind them, package-by-layer vs package-by-feature, and where records changed what "DTO naming" even means

← Back to Index

Why Code Conventions Matter

A convention's value isn't the specific choice โ€” 4 spaces vs 2, braces on the same line or the next โ€” it's that every developer stops spending attention on it at all. Without one, every file silently tells the reader "a different person with different habits wrote this," which is cognitive overhead spent on formatting instead of on understanding what the code actually does.

// BEFORE โ€” technically correct, three different naming styles in one file
public class order_Service {
    public Order Get_order(int OrderId) { ... }
    private boolean checkstock;
}
// Every reader has to first decode "what does this project's style even
// mean" before they can think about the logic at all.

// AFTER โ€” one convention, applied consistently
public class OrderService {
    public Order getOrder(long orderId) { ... }
    private boolean stockAvailable;
}
Key Principles
  • Consistency within a project outranks personal preference
  • Readability outranks brevity
  • Follow the team's established style guide over your own habits โ€” including this page's, if your team has already decided differently

Naming Conventions

Classes and Interfaces

// Classes: PascalCase, nouns
public class CustomerAccount { }
public class OrderProcessingService { }

// Interfaces: PascalCase, often capabilities or adjectives
public interface PaymentProcessor { }
public interface Serializable { }

// Abstract classes: PascalCase, often prefixed with Abstract
public abstract class AbstractRepository { }

// Exceptions: PascalCase, suffixed with Exception
public class InsufficientInventoryException extends RuntimeException { }
Acronyms: treat them as words, not all-caps blocks

Java convention capitalizes only the first letter of an acronym embedded in a name โ€” HttpClient, not HTTPClient; XmlParser, not XMLParser. The reasoning is consistency with every other multi-word identifier: a class name is a sequence of words joined by capitalization, and an acronym is just one of those words. HTTPXMLParser becomes ambiguous about where one word ends and the next begins the moment you chain two acronyms โ€” HttpXmlParser never has that problem.

Methods

// Methods: camelCase, verbs or verb phrases
public BigDecimal calculateTotal() { }
public Order findById(Long id) { }
public void sendConfirmationEmail() { }

// Boolean-returning methods: is, has, can, should
public boolean isActive() { }
public boolean hasDiscount() { }
public boolean canCancel() { }

// Static factory methods
public static Customer of(String email) { }
public static Order createDraft() { }

Variables and Constants

// Variables: camelCase, descriptive
String customerEmail = "jane@shop.com";
List<Order> pendingOrders = new ArrayList<>();

// Constants: SCREAMING_SNAKE_CASE
public static final int MAX_RETRY_COUNT = 3;
private static final Logger LOGGER = LoggerFactory.getLogger(OrderService.class);

// Single-letter names: acceptable only for loop counters, nowhere else
for (int i = 0; i < items.size(); i++) { }   // fine
int x = calculateTotal();                     // not fine โ€” says nothing

Packages

// Packages: all lowercase, reverse domain name, no underscores
package com.shop.orderservice;
package com.shop.orderservice.pricing;

Package Organization โ€” By Layer vs By Feature

The default most tutorials teach is packaging by technical layer. It works fine for a small service, and quietly gets worse as the codebase grows.

// Package-by-layer โ€” the common default
com.shop.orderservice
โ”œโ”€โ”€ controller     // OrderController, CustomerController, ...
โ”œโ”€โ”€ service        // OrderService, PricingService, ...
โ”œโ”€โ”€ repository     // OrderRepository, CustomerRepository, ...
โ”œโ”€โ”€ model          // Order, Customer, OrderItem, ...
โ””โ”€โ”€ dto            // OrderResponse, CustomerRequest, ...
// Package-by-feature โ€” groups everything a single feature needs together
com.shop.orderservice
โ”œโ”€โ”€ order
โ”‚   โ”œโ”€โ”€ OrderController
โ”‚   โ”œโ”€โ”€ OrderService
โ”‚   โ”œโ”€โ”€ OrderRepository
โ”‚   โ””โ”€โ”€ Order
โ”œโ”€โ”€ pricing
โ”‚   โ”œโ”€โ”€ PricingService
โ”‚   โ””โ”€โ”€ DiscountRule
โ””โ”€โ”€ customer
    โ”œโ”€โ”€ CustomerController
    โ”œโ”€โ”€ CustomerService
    โ””โ”€โ”€ Customer
Layer-based packaging scales badly for one specific reason: everything in a package is public to every other layer package by construction

With package-by-layer, OrderRepository must be public for OrderService in a different package to use it โ€” which means it's equally visible and usable from CustomerService, PricingService, or anything else in the codebase, whether that access was intended or not. There's no package-private boundary around a feature's internals at all. Package-by-feature lets each feature keep its repository and internal helpers package-private, exposing only what genuinely needs to be public โ€” the compiler enforces the module boundary instead of a naming convention hoping nobody reaches across layers by accident. For a small, single-purpose microservice, package-by-layer's simplicity is a reasonable trade-off; for a growing monolith with several real feature boundaries, package-by-feature keeps those boundaries real rather than aspirational.

Formatting

Braces and Indentation

// Opening brace on the same line (K&R style) โ€” the Java convention
public class Example {
    public void method() {
        if (condition) {
            // ...
        } else {
            // ...
        }
    }
}

// Always use braces, even for a single statement
// GOOD
if (valid) {
    process();
}
// AVOID โ€” adding a second statement later without noticing the missing
// braces is a real, recurring source of bugs
if (valid)
    process();

Line Length and Wrapping

// Keep lines under 120 characters (100 is a common team default)
List<Order> orders = orderRepository
        .findByStatusAndCreatedAtAfter(
                OrderStatus.PENDING,
                Instant.now().minus(30, ChronoUnit.DAYS)
        );

List<String> activeEmails = customers.stream()
        .filter(Customer::isActive)
        .map(Customer::getEmail)
        .sorted()
        .toList();

Blank Lines

public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
                                              // blank line after fields
    public OrderService(OrderRepository repo, PaymentService payment) {
        this.orderRepository = repo;
        this.paymentService = payment;
    }
                                              // blank line between methods
    public Order createOrder(OrderRequest request) {
        Order order = orderRepository.save(new Order(request));
                                              // blank line for logical separation
        paymentService.charge(order);
        return order;
    }
}

Class Structure โ€” Recommended Member Order

public class WellOrganizedService {

    // 1. Static constants
    private static final Logger LOGGER = LoggerFactory.getLogger(WellOrganizedService.class);

    // 2. Instance fields
    private final OrderRepository orderRepository;

    // 3. Constructors
    public WellOrganizedService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    // 4. Static factory methods
    public static WellOrganizedService withDefaults() { ... }

    // 5. Public methods โ€” the public API, in the order a reader would use them
    public Order createOrder(OrderRequest request) { ... }

    // 6. Package-private / protected methods
    void internalMethod() { ... }

    // 7. Private helper methods
    private void validate(OrderRequest request) { ... }

    // 8. Nested classes
    private static class InternalHelper { }
}

Import Statements

// Order: java.*, jakarta.*, third-party, project packages
import java.util.List;

import jakarta.validation.Valid;

import org.springframework.stereotype.Service;

import com.shop.orderservice.model.Order;

// Avoid wildcard imports โ€” hides what's actually used and silently
// changes meaning if a new class is later added to that package
import java.util.*;   // avoid

// Static imports: fine for test assertions and true constants, not
// for regular business logic โ€” it hides where a method actually lives
import static org.junit.jupiter.api.Assertions.*;   // OK in tests

Comments and Documentation

Javadoc

/**
 * Service for managing customer orders.
 *
 * @since 1.0
 */
public class OrderService {

    /**
     * Finds an order by its unique identifier.
     *
     * @param id the order's unique identifier
     * @return the order if found
     * @throws OrderNotFoundException if no order exists with the given id
     */
    public Order findById(Long id) { ... }
}

Inline Comments

// GOOD โ€” explains WHY, which the code itself can't show
// Retry up to 3 times: the payment gateway has occasional transient timeouts
for (int attempt = 0; attempt < 3; attempt++) { ... }

// BAD โ€” restates what the next line already says
// increment the counter
counter++;

// TODO comments should reference a tracked ticket, not float unowned
// TODO: implement caching (ORD-1234)

Modern Java Style

// var โ€” for obviously-inferable types (Java 10+)
var orders = new ArrayList<Order>();
// Avoid var when the right-hand side doesn't make the type obvious at a glance:
var result = orderService.process(request);   // process() returns... what? Prefer explicit here.

// records โ€” the site's own convention: every DTO is a record
public record OrderSummary(Long id, BigDecimal total, OrderStatus status) { }

// text blocks โ€” multi-line strings (Java 15+)
String json = """
    {"status": "CREATED"}
    """;

// switch expressions (Java 14+)
String label = switch (status) {
    case ACTIVE -> "Active";
    case PENDING -> "Awaiting approval";
};

// pattern matching for switch + record patterns (Java 21+) โ€”
// destructure a sealed hierarchy directly in the switch itself
sealed interface PaymentResult permits Success, Declined {}
record Success(String transactionId) implements PaymentResult {}
record Declined(String reason) implements PaymentResult {}

String message = switch (result) {
    case Success(String txId) -> "Payment succeeded: " + txId;
    case Declined(String reason)  -> "Payment declined: " + reason;
    // sealed + exhaustive switch โ€” no default needed, and the compiler
    // errors if a new permitted type is added and this switch isn't updated
};
Naming DTOs as records: keep the "Dto"/"Request"/"Response" suffix as documentation, not decoration

A record is so lightweight to declare that some teams drop the traditional Dto suffix entirely once every data holder is a one-line record. The suffix still earns its place, though: OrderSummary vs Order tells a reader immediately whether they're holding the full JPA entity (with lazy associations, in a persistence context) or a flattened, serialization-safe projection of it โ€” a distinction that matters a great deal in exactly the N+1 and LazyInitializationException scenarios covered in Entity Relationships.

Enforcing Conventions Automatically

A convention that lives only in a wiki page gets followed inconsistently. The reliable version is enforced by tooling in CI โ€” covered in full in Code Quality Tools (Checkstyle, PMD, Spotless) and CI/CD Basics. The short version here: .editorconfig for cross-IDE basics, Checkstyle for naming rules like the ones on this page, and Spotless for auto-fixable formatting โ€” see those pages for the actual configuration.

Best Practices and Common Pitfalls

โœ… Do

  • Treat an acronym as a single word for casing purposes โ€” HttpClient, not HTTPClient
  • Choose package-by-feature once a codebase has real, separable feature boundaries โ€” it lets package-private actually mean something
  • Keep a "Dto"/"Request"/"Response" suffix even on a one-line record โ€” it documents that this is a projection, not the entity itself
  • Comment on why, not what โ€” the code already says what it does
  • Enforce naming and formatting via CI tooling, not a wiki page nobody re-reads

โŒ Don't

  • Don't use var when the inferred type isn't obvious from the right-hand side โ€” it should aid readability, not obscure it
  • Don't mix naming styles within one project, even if each individual choice is defensible on its own
  • Don't let package-by-layer's default public visibility become an accidental invitation for every service to reach into every repository
  • Don't write comments that restate the next line of code in English

Interview Questions

๐ŸŽ“ Junior level

Q: What casing convention does Java use for class names vs constants?
Classes use PascalCase (OrderService); constants use SCREAMING_SNAKE_CASE (MAX_RETRY_COUNT). Variables and methods use camelCase.

Q: Why should HttpClient be preferred over HTTPClient?
Java convention treats an embedded acronym as a single word for casing purposes, capitalizing only its first letter โ€” the same rule applied to every other word in the identifier. This keeps multi-word names unambiguous, especially when two acronyms appear back to back.

Q: Why is a wildcard import like import java.util.*; discouraged?
It hides which specific classes are actually used, and its meaning can silently change if a new class is later added to that package that happens to collide with a name already used elsewhere in the file.

๐Ÿ”ฅ Senior level

Q: A growing Spring Boot monolith organizes its code as controller/, service/, repository/ packages. What structural problem does this create as the codebase grows, and how does package-by-feature address it?
Package-by-layer forces every class that needs to be called from a different layer's package to be declared public โ€” there is no way to make OrderRepository visible to OrderService alone while hiding it from CustomerService or PricingService, because Java's access control operates at the package level, and all repositories live in the same repository package regardless of which feature they belong to. This means every feature's internals are equally reachable from every other feature's code from day one, with no compiler-enforced boundary โ€” any accidental cross-feature coupling is invisible until it causes a real problem. Package-by-feature groups each feature's controller, service, and repository together in one package, which lets internals that genuinely don't need to be public stay package-private, so the compiler itself โ€” not a naming convention or a code review comment โ€” enforces that a feature's internals stay encapsulated.

Q: Your team debates whether a record-based DTO should keep the "Response" suffix now that declaring one is a single line. Argue the case for keeping it.
The syntactic cost of declaring a record dropped to near zero, but the semantic distinction the suffix communicates didn't change at all: a JPA entity like Order carries lazy-loaded associations, is attached to a persistence context, and can throw LazyInitializationException if accessed outside a transaction, while OrderResponse is a flattened, already-resolved snapshot safe to serialize and safe to hold onto outside any transactional boundary. A reader scanning a method signature that returns Order needs to reason about transaction boundaries and fetch strategies; one returning OrderResponse doesn't. Dropping the suffix because the record itself got cheaper to write conflates two unrelated things โ€” the ceremony of declaring the type, and the meaning of what the type represents โ€” and removes a signal that costs nothing to keep and prevents a real category of mistake (returning or serializing a live entity by accident).

Q: Why does a sealed interface with an exhaustive pattern-matching switch (Java 21) provide a genuine safety guarantee that a regular interface with an if/else chain and a default case doesn't?
With a regular interface, nothing stops a new implementing class from being added anywhere in the codebase without the compiler ever flagging the places that switch on its type โ€” an if/else chain or switch with a default case will silently fall into that default branch for the new type, compiling cleanly while quietly handling the new case wrong or not at all. A sealed interface declares its complete, closed set of permitted implementations up front, and a pattern-matching switch over a sealed type can be exhaustive โ€” covering every permitted case with no default โ€” which means adding a new permitted type without updating every such switch is a compile error, not a silent runtime gap. This converts "did we remember to handle the new case everywhere" from a manual, error-prone code-review responsibility into something the compiler verifies automatically at every call site.