RizTech Academy logo
RizTech Academy
Best Practices: Code Others Can ReadLesson 2 of 535 min

How big is too big

Every rule you will read about size is somebody's guess dressed as a law. "Methods under twenty lines." "Classes under two hundred." "No more than three parameters." They are not useless — they are usually right — but the number is never the reason.

The reason is this: a method should do one thing, at one level of abstraction. Size is a symptom.

One level of abstraction

Here is the most useful idea in this lesson. A method that mixes what with how is hard to read even when it is short.

public void recordDelivery(String dateText, String customer, String tiffinsText) {
    LocalDate date;
    try {
        date = LocalDate.parse(dateText);
    } catch (DateTimeParseException e) {
        throw new IllegalArgumentException("bad date [" + dateText + "]", e);
    }
    int tiffins = Integer.parseInt(tiffinsText);
    if (tiffins < 1 || tiffins > 10) {
        throw new IllegalArgumentException("tiffins must be 1 to 10");
    }
    String line = date + "," + escape(customer) + "," + tiffins;
    Files.writeString(path, line + "\n", CREATE, APPEND);
    for (DeliveryListener l : listeners) {
        l.onDelivery(new Delivery(date, customer, tiffins));
    }
}

That is not long. It is still hard to read, because four different altitudes are stacked in one place: parsing text, validating a business rule, formatting CSV, and notifying listeners. A reader looking for the business rule has to step over file-writing to find it.

public void recordDelivery(String dateText, String customer, String tiffinsText) {
    Delivery delivery = parseDelivery(dateText, customer, tiffinsText);
    store.append(delivery);
    publish(delivery);
}

Same work. Now the method reads as a summary, and each detail is one step down if you want it. That is the test: can you read the method and understand what it does without reading anything it calls?

What to extract, and what to leave

Extracting is not free — every extracted method is a name to invent and a jump for the reader. Extract when:

  • The block needs a comment to explain what it does. The comment is the method name you have not written yet.
  • It is at a different altitude from its neighbours, as above.
  • It is duplicated, or nearly.
  • You want to test it separately.

Leave it alone when:

  • It is used once, is three lines, and is obvious.
  • Extracting would need four parameters to carry the context across.
  • The name you would give it is just a restatement — doTheLoop.

That last one is the giveaway. If you cannot name the extracted method better than the code it replaces, the extraction is not earning anything.

Parameters

Zero is best, one is good, two is fine, three is a smell, four means something is wrong.

new Subscriber("Priya", "411207", Plan.VEG, LocalDate.of(2026, 9, 1), true, false, 3)

What is true? What is 3? Two things fix this, and module 12 covered both: a builder when there are many optional fields, and a parameter object when several always travel together:

// three parameters that are really one idea
void deliver(String line1, String city, String pincode)

// one
void deliver(Address address)

A boolean parameter is nearly always wrong. save(order, true) tells the reader nothing. Two named methods — save(order) and saveAndNotify(order) — or an enum, which at least reads at the call site.

Return early

Deep nesting is usually a chain of conditions that could each have exited.

// arrow-shaped
public long billPaise(String customer, int tiffins) {
    if (customer != null) {
        if (!customer.isBlank()) {
            Subscriber s = subscribers.get(customer);
            if (s != null) {
                if (tiffins > 0) {
                    return s.pricePerTiffinPaise() * tiffins;
                } else {
                    throw new IllegalArgumentException("tiffins must be positive");
                }
            } else {
                throw new IllegalArgumentException("no subscriber " + customer);
            }
        } else {
            throw new IllegalArgumentException("customer must not be blank");
        }
    } else {
        throw new NullPointerException("customer");
    }
}
// flat
public long billPaise(String customer, int tiffins) {
    Objects.requireNonNull(customer, "customer");
    if (customer.isBlank()) throw new IllegalArgumentException("customer must not be blank");
    if (tiffins <= 0) throw new IllegalArgumentException("tiffins must be positive");

    Subscriber s = subscribers.get(customer);
    if (s == null) throw new IllegalArgumentException("no subscriber " + customer);

    return s.pricePerTiffinPaise() * tiffins;
}

Same behaviour. The guards are at the top where a reader can absorb them, the real work is at the bottom with no indentation, and the else branches are gone — every one of which was a place to make a mistake.

Classes: one reason to change

A class should have one reason to change. That is more useful than a line count, because it is about why edits arrive.

Look at the capstone:

Class Changes when…
Money the currency formatting changes
Delivery the fields of a delivery change
DeliveryStore the storage format changes
BillingService the pricing rules change
Main the commands change

Five classes, five unrelated reasons. A price change touches BillingService and nothing else. Moving from CSV to SQLite touches DeliveryStore and nothing else — which is the actual payoff, and it is why DeliveryStore exists at all rather than Files.writeString being called from Main.

The test to apply: if you can describe a class's job with an "and", it is probably two classes. "It stores deliveries and calculates bills" is two.

Fields are the real size signal

A class with fifteen fields is nearly always doing several jobs, regardless of its line count. Look for fields that are used together by one group of methods and never by the others — that grouping is the class trying to split itself.

Do not gold-plate

The counterweight, because the advice above can be taken too far.

A class with one method and one field, wrapped in an interface with one implementation, behind a factory, is not well-designed. It is four files to read before you learn what happens. Module 12 said it: refactor to structure when the code asks for it, not in anticipation.

The right size for a first version is usually "the obvious thing". The second time you touch it, the seams will be visible, and they will be in places you would not have guessed.

Check your work

Why size rules are symptoms: the reason is one thing at one level of abstraction; the line count just correlates.

The readability test: can you read a method and know what it does without reading what it calls?

When to extract: it needs a comment, it is at a different altitude, it is duplicated, or you want to test it.

When not to: three obvious lines, or a name that just restates the code.

What a boolean parameter means: two named methods you did not write.

What return-early buys: guards at the top, the real work unindented, and no else branches to get wrong.

The class test: one reason to change — and an "and" in the description means two classes.

Why fields signal size better than lines: fields used by one group of methods and not another are a split waiting to happen.

The counterweight: four files to learn one fact is not good design.

Practice

  1. Find the longest method in your capstone. List the levels of abstraction in it.
  2. Extract one block from it and name the new method. If the name is a restatement, put it back.
  3. Find a method with a comment inside the body. Turn that comment into a method name.
  4. Find a method with four or more parameters. Decide between a parameter object and a builder.
  5. Find a boolean parameter anywhere in your code. Replace it with two methods and see how the call sites read.
  6. Take a nested method of yours and flatten it with early returns. Count the else branches removed.
  7. For each class in the capstone, write the one reason it would change. Find any needing an "and".
  8. Count the fields on your largest class. Group them by which methods use them.
  9. Find an interface with one implementation in your own code and argue both sides.
  10. Write a method deliberately doing two things, then split it, and say which version you would rather debug.

Next: comments, and the very small number that are worth writing.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship