Chapter 5: Formatting
clean-code formatting readability
Status: Notes complete
Difficulty: Easy
Time to complete: ~35 min read
Overview
Formatting is not cosmetic. The purpose of formatting is communication, and communication is a professional developer’s first obligation. You will not always be the person reading your own code — a future colleague (or your future self) will. When that person reads your code, do you want them to absorb the intent quickly, or fight through visual clutter before they even understand what the code does?
Robert Martin’s core claim: code formatting is too important to leave to chance. A team must agree on a single formatting style and enforce it consistently. The IDE auto-formatter is better than any style debate. Once formatting is automated, developers stop arguing and start shipping.
Key insight: The original code you write will be refactored, rewritten, and replaced. But the communication style — the way thoughts are organized — persists and shapes how the next person reasons about the system.
Related chapters: ch02-meaningful-names (naming as communication), ch03-functions (function size as a formatting dimension), ch04-comments (comments as formatting noise when overused).
The Problem: What Bad Code Looks Like
Consider a class that has been written with no attention to formatting. Everything is crammed together, variables declared far from use, functions ordered randomly, and lines that scroll off the right edge of the screen. The reader must hold the entire file in their head to understand any single piece of it.
// BAD — OrderProcessor with no formatting discipline
public class OrderProcessor{
private static final double TAX_RATE=0.08;private List<Order> pendingOrders;private PaymentGateway gateway;private InventoryService inventory;private NotificationService notifier;private Logger logger;
public OrderProcessor(PaymentGateway gateway,InventoryService inventory,NotificationService notifier,Logger logger){this.gateway=gateway;this.inventory=inventory;this.notifier=notifier;this.logger=logger;this.pendingOrders=new ArrayList<>();}
public boolean processOrder(Order order){logger.info("Processing order "+order.getId());if(order==null){return false;}boolean inStock=inventory.checkStock(order.getItems());if(!inStock){notifier.sendOutOfStock(order.getCustomerEmail());return false;}double total=0;for(OrderItem item:order.getItems()){total+=item.getPrice()*item.getQuantity();}total=total*(1+TAX_RATE);PaymentResult result=gateway.charge(order.getPaymentToken(),total);if(result.isSuccess()){inventory.reserve(order.getItems());pendingOrders.add(order);notifier.sendConfirmation(order.getCustomerEmail(),order.getId());logger.info("Order "+order.getId()+" processed successfully");return true;}else{logger.warn("Payment failed for order "+order.getId());notifier.sendPaymentFailed(order.getCustomerEmail());return false;}}
public List<Order> getPendingOrders(){return Collections.unmodifiableList(pendingOrders);}
private double applyDiscount(Order order,double total){if(order.hasPromoCode()){return total*0.9;}return total;}}This is almost unreadable at a glance. There are no visual boundaries between concepts. You cannot tell where one method ends and another begins without counting braces.
Core Principles
1. Vertical Formatting — File Size
WHY: Small files are easier to understand than large ones. A file that fits on one or two screens allows the reader to hold the whole thing in their mental model. When a file grows to 800 or 1000 lines, it is a signal that it is doing too many things.
The FitNesse project (a large, successful open-source system) has most files well under 200 lines; the longest are around 500. This is not a coincidence — it is the result of disciplined decomposition.
Target: 200–500 lines per file. Files over 500 lines are candidates for splitting.
// BAD — monolithic 800-line class doing too much
public class CustomerManager {
// ... 800 lines of mixed concerns:
// validation, persistence, email sending, reporting, authentication
}
// GOOD — responsibilities split across cohesive classes
public class CustomerValidator { ... } // ~80 lines
public class CustomerRepository { ... } // ~120 lines
public class CustomerNotificationService { ... } // ~90 lines
public class CustomerReportGenerator { ... } // ~110 lines// C++ equivalent — split large translation units
// BAD: customer_manager.cpp — 900 lines
// GOOD: split into:
// customer_validator.cpp ~80 lines
// customer_repository.cpp ~120 lines
// customer_notification.cpp ~90 lines# Python equivalent
# BAD: customer_manager.py — 800+ lines, one class doing everything
# GOOD: split into separate modules
# customer/validator.py
# customer/repository.py
# customer/notifications.py
# customer/reports.py2. Vertical Openness Between Concepts
WHY: Each blank line is a visual cue. Lines that form a group with a common purpose belong together; a blank line signals a conceptual boundary. Removing all blank lines forces the reader to parse structure manually instead of using visual grouping.
// BAD — no vertical openness; concepts blurred together
public class BankAccount {
private String accountId;
private double balance;
private List<Transaction> transactions;
public BankAccount(String accountId, double initialBalance) {
this.accountId = accountId;
this.balance = initialBalance;
this.transactions = new ArrayList<>();
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
balance += amount;
transactions.add(new Transaction(TransactionType.DEPOSIT, amount));
}
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Withdrawal must be positive");
if (amount > balance) throw new InsufficientFundsException(balance, amount);
balance -= amount;
transactions.add(new Transaction(TransactionType.WITHDRAWAL, amount));
}
public double getBalance() { return balance; }
public List<Transaction> getTransactions() { return Collections.unmodifiableList(transactions); }
}// GOOD — blank lines separate constructor, mutating operations, and queries
public class BankAccount {
private String accountId;
private double balance;
private List<Transaction> transactions;
public BankAccount(String accountId, double initialBalance) {
this.accountId = accountId;
this.balance = initialBalance;
this.transactions = new ArrayList<>();
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
balance += amount;
transactions.add(new Transaction(TransactionType.DEPOSIT, amount));
}
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Withdrawal must be positive");
if (amount > balance) throw new InsufficientFundsException(balance, amount);
balance -= amount;
transactions.add(new Transaction(TransactionType.WITHDRAWAL, amount));
}
public double getBalance() { return balance; }
public List<Transaction> getTransactions() {
return Collections.unmodifiableList(transactions);
}
}// C++ — same principle; blank lines separate declaration groups in headers
class BankAccount {
public:
BankAccount(std::string accountId, double initialBalance);
void deposit(double amount);
void withdraw(double amount);
double getBalance() const;
std::vector<Transaction> getTransactions() const;
private:
std::string accountId_;
double balance_;
std::vector<Transaction> transactions_;
};# Python — PEP 8 mandates two blank lines between top-level definitions,
# one blank line between methods inside a class
class BankAccount:
def __init__(self, account_id: str, initial_balance: float) -> None:
self.account_id = account_id
self._balance = initial_balance
self._transactions: list[Transaction] = []
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
self._transactions.append(Transaction(TransactionType.DEPOSIT, amount))
def withdraw(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Withdrawal must be positive")
if amount > self._balance:
raise InsufficientFundsError(self._balance, amount)
self._balance -= amount
self._transactions.append(Transaction(TransactionType.WITHDRAWAL, amount))
@property
def balance(self) -> float:
return self._balance3. Vertical Density (Closely Related Lines Should Be Close)
WHY: Visual proximity signals conceptual relatedness. When lines that belong together are separated by blank lines or unrelated comments, the reader must work harder to group them mentally. Conversely, when logically separate things are packed together without space, the reader cannot tell what groups with what.
A particularly harmful anti-pattern: inserting useless comments between related variable declarations and their use.
// BAD — useless comments split apart related declarations
public class ReportGenerator {
// The title of the report
private String reportTitle;
// Tracks when the report was generated
private LocalDate generationDate;
// The output format (PDF, CSV, etc.)
private String outputFormat;
// The list of data rows
private List<ReportRow> dataRows;// GOOD — related fields are vertically dense; no noise comments
public class ReportGenerator {
private String reportTitle;
private LocalDate generationDate;
private String outputFormat;
private List<ReportRow> dataRows;The same rule applies to instance variable declarations scattered throughout a class body. They should be grouped at the top (in Java/C++) or initialized together in __init__ (Python).
// BAD — instance variables spread across the class
public class OrderService {
private OrderRepository orderRepo;
public void createOrder(Order o) { ... }
private PaymentGateway gateway; // ← declared in the middle of the class
public void cancelOrder(String id) { ... }
private NotificationService notifier; // ← buried at the bottom
}// GOOD — all instance variables together at the top
public class OrderService {
private OrderRepository orderRepo;
private PaymentGateway gateway;
private NotificationService notifier;
public void createOrder(Order o) { ... }
public void cancelOrder(String id) { ... }
}4. Vertical Distance — Variable Declarations and Dependent Functions
WHY: When a variable is declared far from its use, the reader must scroll up and down to understand what it is. Cognitive load increases with distance. Minimizing the gap between declaration and first use is a courtesy to every future reader.
Variable declarations: local variables should be declared as close to their first use as possible.
// BAD — local variable declared far from use
public double calculateOrderTotal(List<OrderItem> items) {
double total = 0.0; // declared here
String currency = "USD";
boolean hasDiscount = checkDiscountEligibility(items);
List<OrderItem> taxableItems = filterTaxable(items);
// ... 20 lines of other logic ...
for (OrderItem item : taxableItems) {
total += item.getPrice() * item.getQuantity(); // used here
}
return total;
}
// GOOD — declare total right before the loop that uses it
public double calculateOrderTotal(List<OrderItem> items) {
String currency = "USD";
boolean hasDiscount = checkDiscountEligibility(items);
List<OrderItem> taxableItems = filterTaxable(items);
double total = 0.0;
for (OrderItem item : taxableItems) {
total += item.getPrice() * item.getQuantity();
}
return total;
}Dependent functions (caller above callee): When function A calls function B, A should appear above B in the file. This creates a natural reading flow — the high-level logic comes first, and the reader descends into details only when they want to.
// GOOD — caller (processPayment) is above callee (validateCard, chargeCard)
public PaymentResult processPayment(PaymentRequest request) {
validateCard(request.getCard());
return chargeCard(request.getCard(), request.getAmount());
}
private void validateCard(CreditCard card) {
if (card.isExpired()) throw new ExpiredCardException(card);
if (!luhnCheck(card.getNumber())) throw new InvalidCardException(card);
}
private PaymentResult chargeCard(CreditCard card, Money amount) {
return paymentGateway.charge(card.getToken(), amount.getAmount());
}Conceptual affinity: Functions that are conceptually related should appear near each other in the file, even if they do not call each other. Static utility functions that operate on the same type, or assertion helpers that all validate the same domain object, belong together.
5. Vertical Ordering — The Newspaper Metaphor
WHY: A newspaper article is structured with the most important information first and the details last. Readers can stop at any point and still have learned something useful. Code should work the same way: the most important concepts at the top of the file, implementation details at the bottom.
This means:
- Public API methods at the top
- Private helper methods at the bottom
- The “story” of what the class does is readable at the top level; implementation details are below
// GOOD — top-down ordering in an e-commerce checkout service
public class CheckoutService {
// HIGH LEVEL: what this class does (public interface)
public Receipt checkout(Cart cart, PaymentMethod payment) {
validateCart(cart);
Pricing pricing = calculatePricing(cart);
PaymentResult result = processPayment(payment, pricing.getTotal());
return buildReceipt(cart, pricing, result);
}
// MID LEVEL: operations that support checkout
private void validateCart(Cart cart) { ... }
private Pricing calculatePricing(Cart cart) { ... }
private PaymentResult processPayment(PaymentMethod payment, Money total) { ... }
private Receipt buildReceipt(Cart cart, Pricing pricing, PaymentResult result) { ... }
// LOW LEVEL: utility details
private Money applyTax(Money subtotal) { ... }
private Money applyPromoCode(Money subtotal, String promoCode) { ... }
}# Python — same principle
class CheckoutService:
# Public interface first
def checkout(self, cart: Cart, payment: PaymentMethod) -> Receipt:
self._validate_cart(cart)
pricing = self._calculate_pricing(cart)
result = self._process_payment(payment, pricing.total)
return self._build_receipt(cart, pricing, result)
# Supporting methods below
def _validate_cart(self, cart: Cart) -> None: ...
def _calculate_pricing(self, cart: Cart) -> Pricing: ...
def _process_payment(self, payment: PaymentMethod, total: Money) -> PaymentResult: ...
def _build_receipt(self, cart: Cart, pricing: Pricing, result: PaymentResult) -> Receipt: ...6. Horizontal Formatting — Line Length
WHY: Lines that require horizontal scrolling force the reader to lose their place in the code. The original 80-character limit came from punch cards and 80-column terminals, but the principle survives because most monitors display code vertically. Long lines also indicate that a function is doing too much or is nested too deeply.
Modern guideline: 80–120 characters per line. Anything beyond 120 is a code smell.
// BAD — forces horizontal scrolling, buries the logic
public boolean isEligibleForExpressShipping(Order order, Customer customer, ShippingConfiguration config) {
return customer.isPremiumMember() && order.getTotalWeight() < config.getExpressWeightLimitKg() && order.getDeliveryAddress().getCountry().equals(config.getDomesticCountryCode()) && !order.containsHazardousMaterials();
}
// GOOD — broken into readable segments
public boolean isEligibleForExpressShipping(
Order order,
Customer customer,
ShippingConfiguration config) {
boolean isPremiumCustomer = customer.isPremiumMember();
boolean isWithinWeightLimit = order.getTotalWeight() < config.getExpressWeightLimitKg();
boolean isDomesticDelivery = order.getDeliveryAddress().getCountry()
.equals(config.getDomesticCountryCode());
boolean hasNoHazardousMaterials = !order.containsHazardousMaterials();
return isPremiumCustomer && isWithinWeightLimit
&& isDomesticDelivery && hasNoHazardousMaterials;
}# Python — PEP 8 specifies 79 chars (E501); many teams use 88 (black default) or 120
# BAD
def is_eligible_for_express_shipping(order: Order, customer: Customer, config: ShippingConfiguration) -> bool:
return customer.is_premium_member and order.total_weight < config.express_weight_limit_kg and order.delivery_address.country == config.domestic_country_code and not order.contains_hazardous_materials
# GOOD
def is_eligible_for_express_shipping(
order: Order,
customer: Customer,
config: ShippingConfiguration,
) -> bool:
is_premium = customer.is_premium_member
within_weight = order.total_weight < config.express_weight_limit_kg
is_domestic = order.delivery_address.country == config.domestic_country_code
no_hazmat = not order.contains_hazardous_materials
return is_premium and within_weight and is_domestic and no_hazmat7. Horizontal Openness and Density
WHY: Spaces around operators communicate grouping and precedence. No space between a function name and its opening parenthesis signals that the name and arguments are one syntactic unit. Spaces around assignment operators make the = stand out as a statement boundary.
// BAD — no spaces; operator precedence obscured
double discriminant=b*b-4*a*c;
double result=(-b+Math.sqrt(discriminant))/(2*a);
lineWidthHistogram (line); // ← wrong: space before paren on a call
// GOOD — spaces around assignment and low-precedence operators;
// no space between function name and paren
double discriminant = b*b - 4*a*c;
double result = (-b + Math.sqrt(discriminant)) / (2*a);
lineWidthHistogram(line);The logic: multiplication binds tighter than subtraction; b*b and 4*a*c are tighter terms, and the subtraction operator separates them. Writing b*b - 4*a*c instead of b * b - 4 * a * c mirrors the mathematical precedence visually.
// C++ equivalent
double discriminant = b*b - 4*a*c;
double result = (-b + std::sqrt(discriminant)) / (2*a);# Python — PEP 8 follows the same rule
# BAD
discriminant=b*b-4*a*c
result=(-b+math.sqrt(discriminant))/(2*a)
# GOOD
discriminant = b*b - 4*a*c
result = (-b + math.sqrt(discriminant)) / (2*a)8. Horizontal Alignment — Why Aligned Columns Are Misleading
WHY: Aligning variable names or assignment operators in a column looks tidy at a glance but is actively misleading. The eye naturally follows the column of equals signs or names, grouping them visually as if they belong together in a way that has nothing to do with their actual relationships. It also forces artificial changes — adding a longer variable name means re-aligning every other line.
// BAD — aligned columns look tidy but mislead the eye
private String accountId;
private double balance;
private List<Transaction> transactions;
private LocalDate openedDate;
// assignments aligned
this.accountId = accountId;
this.balance = balance;
this.transactions = new ArrayList<>();
this.openedDate = LocalDate.now();// GOOD — no alignment; each declaration stands independently
private String accountId;
private double balance;
private List<Transaction> transactions;
private LocalDate openedDate;
// natural assignment
this.accountId = accountId;
this.balance = balance;
this.transactions = new ArrayList<>();
this.openedDate = LocalDate.now();The “good” version looks less visually structured, but it communicates that these are independent declarations with no special grouping. If you feel the urge to align them, it is a signal that you have too many variables — split the class.
9. Indentation
WHY: Indentation is not optional decoration — it is the visual representation of hierarchical scope. Without indentation, a reader cannot tell what is inside a loop, what is inside a conditional, or what belongs to a class. The visual hierarchy is the map.
Never collapse indentation: even for short if/while bodies, keep the braces and indentation. The one-liner collapses a visual cue that will matter when someone adds a second statement later.
// BAD — collapsed indentation hides structure
if (order.isPaid()) return true;
while (items.hasNext()) processItem(items.next());
// GOOD — always expand; the visual scope is the contract
if (order.isPaid()) {
return true;
}
while (items.hasNext()) {
processItem(items.next());
}// C++ — same rule; Allman and K&R both keep the brace on a visible line
// BAD (K&R with collapsed body)
if (order.isPaid()) return true;
// GOOD
if (order.isPaid()) {
return true;
}# Python — indentation is syntactically enforced, but the same spirit applies:
# never write if-else on one line in production code
# BAD
result = "paid" if order.is_paid else "unpaid" # fine for trivial expressions
# BAD for complex logic
if order.is_paid: process_paid_order(order)
# GOOD
if order.is_paid:
process_paid_order(order)Indent size: 4 spaces in Java (Oracle/Google convention), 2 spaces in some JS shops, tabs in some C++ shops. What matters is team consistency. Never mix tabs and spaces.
10. Team Rules — The Most Important Formatting Principle
WHY: A team of developers should produce code that looks like it was written by a single person, not a committee. This requires an explicit agreement on formatting rules, enforced by tooling. Style debates waste engineering time. An automated formatter eliminates the debate.
The principle: pick a style and automate it. No manual enforcement. No PR comments about brace placement.
Java — Google Java Format / Checkstyle / spotless:
<!-- Maven: spotless plugin enforces formatting on build -->
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<configuration>
<java>
<googleJavaFormat/>
</java>
</configuration>
</plugin>C++ — clang-format:
# .clang-format in project root
---
BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 120
BreakBeforeBraces: Attach
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: falseRun: clang-format -i src/**/*.cpp src/**/*.h
Python — black + isort:
# pyproject.toml
[tool.black]
line-length = 88
target-version = ["py311"]
[tool.isort]
profile = "black"Run: black . && isort .
PEP 8 is Python’s de facto “team rule”. black is the formatter that enforces it without configuration debates. isort handles import ordering. Both run in CI.
Concrete Before/After: OrderProcessor
Here is the original badly-formatted OrderProcessor from the problem section, fully reformatted according to all the principles above.
// GOOD — properly formatted OrderProcessor
public class OrderProcessor {
private static final double TAX_RATE = 0.08;
private final List<Order> pendingOrders;
private final PaymentGateway gateway;
private final InventoryService inventory;
private final NotificationService notifier;
private final Logger logger;
public OrderProcessor(
PaymentGateway gateway,
InventoryService inventory,
NotificationService notifier,
Logger logger) {
this.gateway = gateway;
this.inventory = inventory;
this.notifier = notifier;
this.logger = logger;
this.pendingOrders = new ArrayList<>();
}
public boolean processOrder(Order order) {
logger.info("Processing order " + order.getId());
if (!inventory.checkStock(order.getItems())) {
notifier.sendOutOfStock(order.getCustomerEmail());
return false;
}
double total = calculateTotal(order);
PaymentResult result = gateway.charge(order.getPaymentToken(), total);
if (result.isSuccess()) {
confirmOrder(order);
return true;
} else {
handlePaymentFailure(order);
return false;
}
}
public List<Order> getPendingOrders() {
return Collections.unmodifiableList(pendingOrders);
}
private double calculateTotal(Order order) {
double subtotal = 0.0;
for (OrderItem item : order.getItems()) {
subtotal += item.getPrice() * item.getQuantity();
}
return applyDiscount(order, subtotal) * (1 + TAX_RATE);
}
private double applyDiscount(Order order, double total) {
return order.hasPromoCode() ? total * 0.9 : total;
}
private void confirmOrder(Order order) {
inventory.reserve(order.getItems());
pendingOrders.add(order);
notifier.sendConfirmation(order.getCustomerEmail(), order.getId());
logger.info("Order " + order.getId() + " processed successfully");
}
private void handlePaymentFailure(Order order) {
logger.warn("Payment failed for order " + order.getId());
notifier.sendPaymentFailed(order.getCustomerEmail());
}
}What changed:
- Constants at the top, fields
finaland grouped - Constructor broken across multiple lines (one param per line)
processOrdersplit intocalculateTotal,confirmOrder,handlePaymentFailure- Null check removed (callers are responsible for not passing null; see ch07-error-handling)
- Blank lines separate constructor, public methods, and private helpers
- Callers (public methods) above callees (private helpers) — newspaper metaphor
C++ version (same class, demonstrating clang-format style):
// GOOD — order_processor.h
#pragma once
#include <memory>
#include <vector>
#include "order.h"
#include "payment_gateway.h"
#include "inventory_service.h"
#include "notification_service.h"
class OrderProcessor {
public:
OrderProcessor(
std::shared_ptr<PaymentGateway> gateway,
std::shared_ptr<InventoryService> inventory,
std::shared_ptr<NotificationService> notifier);
bool processOrder(const Order& order);
std::vector<Order> getPendingOrders() const;
private:
static constexpr double kTaxRate = 0.08;
std::shared_ptr<PaymentGateway> gateway_;
std::shared_ptr<InventoryService> inventory_;
std::shared_ptr<NotificationService> notifier_;
std::vector<Order> pendingOrders_;
double calculateTotal(const Order& order) const;
double applyDiscount(const Order& order, double total) const;
void confirmOrder(const Order& order);
void handlePaymentFailure(const Order& order);
};Python version:
# GOOD — order_processor.py (formatted with black)
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from inventory_service import InventoryService
from notification_service import NotificationService
from order import Order
from payment_gateway import PaymentGateway
TAX_RATE = 0.08
class OrderProcessor:
def __init__(
self,
gateway: PaymentGateway,
inventory: InventoryService,
notifier: NotificationService,
) -> None:
self._gateway = gateway
self._inventory = inventory
self._notifier = notifier
self._pending_orders: list[Order] = []
def process_order(self, order: Order) -> bool:
if not self._inventory.check_stock(order.items):
self._notifier.send_out_of_stock(order.customer_email)
return False
total = self._calculate_total(order)
result = self._gateway.charge(order.payment_token, total)
if result.is_success:
self._confirm_order(order)
return True
else:
self._handle_payment_failure(order)
return False
@property
def pending_orders(self) -> list[Order]:
return list(self._pending_orders)
def _calculate_total(self, order: Order) -> float:
subtotal = sum(item.price * item.quantity for item in order.items)
return self._apply_discount(order, subtotal) * (1 + TAX_RATE)
def _apply_discount(self, order: Order, total: float) -> float:
return total * 0.9 if order.has_promo_code else total
def _confirm_order(self, order: Order) -> None:
self._inventory.reserve(order.items)
self._pending_orders.append(order)
self._notifier.send_confirmation(order.customer_email, order.id)
def _handle_payment_failure(self, order: Order) -> None:
self._notifier.send_payment_failed(order.customer_email)Comparison / Summary Table
| Principle | Rule | Tool/Enforcement |
|---|---|---|
| File size | 200–500 lines; split beyond 500 | Code review; SonarQube |
| Vertical openness | Blank line between distinct concepts | Auto-formatter |
| Vertical density | Related lines together; no noise comments between them | Code review |
| Variable declarations | Declare as close to first use as possible | Linters (Checkstyle, pylint) |
| Dependent functions | Caller above callee | Convention; code review |
| Vertical ordering | Most important (public API) at top | Convention |
| Line length | 80–120 chars max | clang-format, black, Google Java Format |
| Horizontal openness | Spaces around = and +/-; no space before ( on calls | Auto-formatter |
| Alignment | No column alignment of variables/assignments | Auto-formatter removes it |
| Indentation | Always expand blocks; consistent indent size | Auto-formatter |
| Team rules | Single automated style; no manual debates | CI gate on formatter check |
When to Apply / Common Exceptions
Always apply:
- Blank lines between methods
- Indentation — never collapse
- Line length limits
- Variable declarations near first use
Exceptions and nuance:
- Short utility classes (under 50 lines) can be more compact — fewer separators needed when everything fits on one screen
- Generated code (JAXB, protobuf, ORM entities) is exempt — do not reformat auto-generated files
- Test data builders sometimes use intentional horizontal alignment for readability when constructing test fixtures with many named fields
- Configuration constants at the top of a file may be column-aligned for genuine readability in simple cases — use judgment, not dogma
Checklist
Use this when reviewing a PR or self-reviewing a new file:
- File is under 500 lines; over 300 lines triggers a “can I split this?” question
- Blank lines separate distinct concepts (constructor, mutators, queries, helpers)
- Related lines are grouped without noise comments between them
- Local variables declared immediately before first use
- Instance variables all at the top of the class
- Public methods (callers) appear above private methods (callees)
- No line exceeds the team-agreed limit (120 chars common)
- Spaces around
=,-=,+=, etc.; no space between function name and( - No aligned columns in declarations or assignments
- All
if/while/forbodies use braces and proper indentation - Project auto-formatter has been run (verified by CI)
Key Takeaways
- Formatting is communication — every formatting decision is a message to the next reader about what belongs together and what doesn’t.
- Blank lines are visual cues — they separate distinct thoughts the same way paragraphs separate ideas in prose.
- Related code should be close — vertically dense for related lines, vertically open for distinct concepts; horizontal density signals tight binding.
- Newspaper metaphor — public API (the headline) at the top; private implementation (the details) at the bottom; the reader chooses how deep to go.
- The 200–500 line guideline is a forcing function for decomposition, not a hard rule — but crossing 500 lines almost always means the class is doing too much.
- Never collapse indentation — the one-liner
ifsaves two lines but destroys the visual scope contract. - Column alignment is misleading — it creates false visual grouping; let the auto-formatter remove it.
- Automate formatting —
black,clang-format, Google Java Format. A CI gate is better than any style discussion. - Team consistency beats individual preference — the worst formatting style consistently applied beats the best style applied inconsistently.
- Clean formatting is not the most important thing in clean code — but it is a signal of professionalism. Messy formatting signals messy thinking.
Related Resources
- ch03-functions — function length and single responsibility; the same “do one thing” principle applies to files
- ch04-comments — comments are noise when used to compensate for poor formatting
- ch06-objects-and-data-structures — class organization and what goes where
- ch10-classes — class-level organization, SRP, and cohesion
- ch17-smells-and-heuristics — Heuristic G30 (Functions should do one thing) and formatting-related smells
External:
Last Updated: 2026-04-14