Scroll, Squint, Surrender: Why Your Methods Should Fit on One Screen
What the 15 to 20 line guideline is really getting at, why long methods cost more than they look, and the over-extraction failure mode nobody warns you about.

Have you ever opened a method, scrolled for a bit, and realised that half an hour later you still could not say what the thing actually does? Not because the code is clever. There is just so much of it, and every part leans on something that happened two hundred lines earlier.
Most of us have a specific method in mind right now.
Picture a checkout service with a method called processOrder(). The ticket is tiny: add a promo code field. Inside sit validation, inventory holds, tax rules, payment retries, a confirmation email, and audit logging, all sharing the same pile of local variables. Somewhere around line 280 you lose track of whether total was rounded before or after tax.
So the change goes in at the bottom, in its own if, as far from everything else as possible, because that is the only spot anyone feels sure will not break something.
Which is how a method gets to 340 lines. Everyone who touched it before made the same call, for the same reason, and each of them was right to.
So where does 15 to 20 lines come from?
It is one of those numbers that gets passed around without much explanation. The usual phrasing is that a method should fit on one screen, which lands around 15 to 20 lines. Robert Martin argues for something stricter in Clean Code (book link), suggesting functions should rarely run to twenty lines at all. It is nearly two decades old and still one of the few books that changes how you write code the same week you read it.
None of this is a hard limit, and nobody should be failing a review over line twenty-one. Treat it as a convention: a number low enough that crossing it makes you stop and look.
What it stands in for is harder to measure directly, which is why we lean on the number. Does the method do one thing, at one level of abstraction?
That phrase “level of abstraction” is doing most of the work here, so here is a plainer version of it. Think about giving someone directions. “Drive to the airport, check in, clear security” is one kind of instruction. “Turn the key, check your mirror, indicate left” is another. Each list is fine on its own, and mixing them reads strangely: “Drive to the airport, indicate left, check in.”
Methods work the same way. chargeCard(order) and sendReceipt(order) are both business steps, so they sit together comfortably. Drop a hand-rolled loop packing bytes into a buffer between them and the reader has to climb down into the plumbing and back out again to follow what is going on.
There is a quicker check than counting anything. Try naming the method honestly. If the truthful name needs an “and”, you already know:
validateAndPriceAndChargeAndNotify(order) // the name is the bug report
Line count is what gets your attention. The naming test tells you whether there is a real problem.
What this looks like in practice
1. The method nobody actually wrote
Nobody sits down and writes 340 lines. It arrives one small ticket at a time.
public OrderResult processOrder(Order order, User user) {
// validate the order
if (order.items().isEmpty()) return OrderResult.error("Empty cart");
for (Item item : order.items()) {
if (item.quantity() <= 0) return OrderResult.error("Bad quantity");
// ... 15 more lines of validation
}
// reserve inventory
for (Item item : order.items()) {
// ... 30 lines of stock checks and holds
}
// calculate tax
BigDecimal tax = BigDecimal.ZERO;
// ... 40 lines of jurisdiction rules
// charge the card
// ... 35 lines of gateway calls and retries
// send confirmation
// ... 25 lines of template building and SMTP
// write the audit log
// ... 20 lines
}
Look at what those comments are doing. Each one labels the block underneath it. When somebody writes a comment to explain a section inside a method, they have usually just named a method out loud and then not written it.
Which makes the cleanup mechanical. Every // reserve inventory becomes reserveInventory(order), and processOrder collapses into six readable calls at the same level.
2. The method where the real work is buried
The second pattern is about nesting rather than length, and honestly it causes more pain.
public ShippingQuote quote(Order order) {
if (order != null) {
if (order.address() != null) {
if (order.address().isDomestic()) {
if (order.weight() < MAX_WEIGHT) {
if (carrier.isAvailable(order.address())) {
return carrier.quote(order); // the actual job, 5 levels deep
} else { ... }
} else { ... }
} else { ... }
} else { ... }
}
return ShippingQuote.unavailable();
}
The one line that does the work is pushed so far right it is easy to miss, and every closing brace on the way out makes you reconstruct which condition you just left.
The fix is a guard clause, which sounds more formal than it is. A guard clause is just an early return that deals with an awkward case immediately and gets it out of the way, so the rest of the method does not need wrapping in an else. You handle the exceptions first, one per line, and whatever survives all of them is the normal path:
public ShippingQuote quote(Order order) {
if (order == null || order.address() == null) return ShippingQuote.unavailable();
if (!order.address().isDomestic()) return internationalQuote(order);
if (order.weight() >= MAX_WEIGHT) return freightQuote(order);
if (!carrier.isAvailable(order.address())) return ShippingQuote.unavailable();
return carrier.quote(order);
}
Same behaviour, except now you read it top to bottom and the interesting line sits in plain sight.
What long methods actually cost you
The obvious cost is that they are unpleasant to read. The real ones are quieter:
- Nobody can hold them in their head. We keep a handful of things in working memory at once, and a 300 line method with fifteen locals blows past that. People stop reasoning about it and start pattern-matching on the bit they are touching, hoping the rest is unrelated.
- Testing becomes all or nothing. To test one tax rule you need a valid cart, a stocked warehouse, a payment gateway, and a mail server. That is expensive enough that the test never gets written, so the riskiest code ends up with the least coverage.
- Reviews quietly stop working. A one line change inside a 340 line method shows up as a one line diff. Your reviewer sees the line, not the 200 lines above it that decide whether it is correct.
- Bugs hide between the blocks. A field set at line 40 and read at line 280 is a dependency with nothing marking it. Someone reorders two blocks that look independent, production breaks, and the diff looks harmless.
- Reuse turns into copy-paste. That tax logic is welded into a method that also charges cards, so when you need it elsewhere you copy it. Now you have the problem from Copy, Paste, Regret: one business rule in two places, quietly drifting apart.
The honest caveat: you can overdo this
Before you go and split everything, the opposite mistake is real and gets discussed far less.
Take a 40 line method and chop it into eight 5 line methods, each called once, none meaningful on its own. The complexity has not gone anywhere. It is spread across the file, and the reader reassembles it by jumping between definitions. Spaghetti code is tangled; this is ravioli code, where every piece is small and tidy and the overall shape is still impossible to see.
So the better question is whether the piece you pulled out has a name that means something by itself. reserveInventory(order) is a real concept in the business. doStep3(order) is line-count management. If you cannot name the piece without pointing at where it sat in the caller, leave it alone.
Length is also fine in places: a straight run of configuration with no branching, or a switch covering twenty cases, where splitting it would scatter one decision across twenty places.
Keeping methods small when an LLM writes the first draft
Most new code is drafted by an assistant now, and the default output runs long.
Ask for “a function that handles checkout” and you get exactly that: one function with validation, pricing, payment, and notification stacked inside it. The model is answering the question you asked, and your question described one task. Decomposition is something you have to request.
A second effect makes it harder to spot. Generated code looks tidy, and that polish survives a skim even when the method is 150 lines doing six jobs. The old tell, where bad code looked bad, is gone.
Ask for the shape, not just the behaviour.
❌ Don’t prompt like this:
Write a method that processes an order.
✅ Do this instead:
Write order processing as a coordinator method under 20 lines that delegates
to named helpers for validation, inventory, pricing, payment, and notification.
Each helper should be independently testable and named for what it does in the
domain, not for its position in the sequence.
Put it in the file the model reads. Most assistants pick up a project instruction file such as CLAUDE.md or AGENTS.md, and that reaches every generation instead of every prompt:
- Methods stay under about 20 lines and do one thing.
- Use guard clauses instead of nesting; no more than two levels deep.
- Prefer several named methods over one method with section comments.
Read the signatures before the bodies. When a diff comes back, look at the method names on their own first. If you cannot work out roughly what the code does from the names, the decomposition is wrong no matter how clean the implementation looks.
In the tooling’s favour: models are reliable at the mechanical part once a human has picked the seams. “Extract the tax calculation into its own method, keep the behaviour identical” is well-scoped and easy to verify. Working out where to cut is the judgement, and that stays yours.
The takeaway
The 15 to 20 line guideline is a proxy, and taking it literally gets you ravioli. What matters is whether the method does one nameable thing at one level of abstraction. Length is just the cheapest symptom to notice.
Three things worth carrying into the next file you open:
- A comment inside a method body is usually a method name waiting to be written. Name the block, move it out, delete the comment.
- Nesting hurts more than length. Guard clauses are the cheapest win available and almost always safe.
- Name it honestly. If the true name needs an “and”, it does too much.
If you would rather a machine held the line, Checkstyle’s MethodLength sets a hard ceiling in a Java build and is the easiest thing to drop into CI. PMD’s design rules cover length and complexity together. SonarQube’s cognitive complexity score beats raw line count, because it measures how hard code is to follow rather than how far it scrolls.
One last thing. Keeping a method short while you write it costs nothing. Cutting down a 340 line method that has been in production for three years is a different job, because you do not yet know which of its behaviours something else depends on. For that, use the playbook from Diff, Merge, Breathe, which transfers almost directly: write characterization tests first, recording what the method does today including anything you suspect is wrong; extract one block at a time and run the suite after each; and if the method sits on a critical path, run the old and new versions side by side in production and log every disagreement before you delete the original.