The Stepdown Rule
Open a well-organized newspaper. The headline tells you the story. The first paragraph gives you the summary. Details follow for those who want them.
Code should work the same way.
The Newspaper Metaphor
Robert C. Martin introduced this idea: code should read like a newspaper article.
- The name of the function is the headline
- The first few lines tell you what happens
- Details are pushed into lower-level functions
- You can stop reading at any point and have learned something
This is the Stepdown Rule: each function should be followed by the functions it calls, and those should be at the next level of abstraction.
What This Looks Like
Top-Down Reading
// Level 1: The headline
async function processOrder(orderId) {
const order = await loadOrder(orderId);
validateOrder(order);
const payment = await processPayment(order);
await fulfillOrder(order);
await notifyCustomer(order, payment);
}
// Level 2: Supporting functions
async function loadOrder(orderId) {
const order = await db.orders.findById(orderId);
if (!order) throw new OrderNotFoundError(orderId);
return order;
}
function validateOrder(order) {
validateItems(order.items);
validateShippingAddress(order.shippingAddress);
validatePaymentMethod(order.paymentMethod);
}
async function processPayment(order) {
const amount = calculateTotal(order);
return await paymentGateway.charge(order.paymentMethod, amount);
}
// Level 3: More specific functions
function validateItems(items) {
if (!items?.length) throw new EmptyOrderError();
items.forEach(validateItem);
}
function validateItem(item) {
if (item.quantity <= 0) throw new InvalidQuantityError(item);
if (item.price < 0) throw new InvalidPriceError(item);
}
function calculateTotal(order) {
const subtotal = calculateSubtotal(order.items);
const shipping = calculateShipping(order);
const tax = calculateTax(order, subtotal);
return subtotal + shipping + tax;
}
Notice how you can stop at any level and understand what's happening:
- Level 1: Order is loaded, validated, paid, fulfilled, and customer notified
- Level 2: Loading checks existence, validation checks items and addresses, payment calculates amount
- Level 3: Item validation checks quantity and price, total is subtotal plus shipping plus tax
The Scanning Benefit
Developers spend most of their time scanning code, not reading it deeply.
When code follows the stepdown rule, scanning works:
- Read the top function → understand the flow
- Find the relevant sub-function → dive deeper
- Repeat until you find what you need
When code violates the stepdown rule, scanning fails:
- Read some code → confused by details
- Jump to a random function → more confusion
- Try to build mental model → give up and read everything
Implementation: TO-Paragraphs
Uncle Bob suggests thinking in terms of "TO-paragraphs." Each function is a "TO" statement:
TO process an order, we load it, validate it, process payment, fulfill it, and notify the customer.
TO validate an order, we validate items, shipping address, and payment method.
TO validate items, we check that items exist and each item is valid.
TO validate an item, we check quantity and price.
If your function can't be described as a single TO-paragraph, it's doing too much.
Organizing Files
The stepdown rule also applies to how you organize functions within a file.
Bad: Random Order
function calculateTax() { }
function processOrder() { }
function validateShipping() { }
function loadOrder() { }
function validateItems() { }
function notifyCustomer() { }
To understand processOrder, you have to jump around the file.
Good: Stepdown Order
// High-level orchestration
function processOrder() { }
// Direct dependencies of processOrder
function loadOrder() { }
function validateOrder() { }
function processPayment() { }
function fulfillOrder() { }
function notifyCustomer() { }
// Dependencies of validateOrder
function validateItems() { }
function validateShipping() { }
// Dependencies of processPayment
function calculateTotal() { }
// And so on...
Now you can read top-to-bottom and the story unfolds naturally.
The Trade-off: Locality vs. Hierarchy
Sometimes you want to see related code together, even if it breaks strict stepdown order.
For example, you might group all validation functions together:
// Validation
function validateOrder(order) { ... }
function validateItems(items) { ... }
function validateItem(item) { ... }
function validateShipping(address) { ... }
// Calculation
function calculateTotal(order) { ... }
function calculateSubtotal(items) { ... }
function calculateTax(order, subtotal) { ... }
This is a reasonable compromise. The key is consistency: pick an organization strategy and stick with it.
Classes and the Stepdown Rule
In classes, the stepdown rule suggests:
- Public methods first — These are your "headlines"
- Private methods below — These are the "details"
- Order private methods by call order — When possible
class OrderProcessor {
// Public API (headlines)
async process(orderId: string): Promise<void> {
const order = await this.loadOrder(orderId);
this.validate(order);
await this.charge(order);
await this.fulfill(order);
}
// Private implementation (details)
private async loadOrder(orderId: string): Promise<Order> { }
private validate(order: Order): void { }
private async charge(order: Order): Promise<void> { }
private async fulfill(order: Order): Promise<void> { }
}
Testing the Stepdown Rule
A good test: can a new developer understand what a file does by reading only the first 20 lines?
If yes, you're following the stepdown rule.
If no, your abstractions need work.
Key insight: Code should read like a newspaper: headlines first, details later. Organize functions so high-level orchestration appears first, and each function is followed by the functions it calls. Readers should be able to stop at any level and understand what's happening.