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;
}
- 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 { }
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
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
};
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, notHTTPClient - 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
varwhen 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
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.
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.