Testing & Code Quality

[Code Smell #3] Ravioli code: code whose flow nobody understands

Every function is five lines long and has one responsibility, yet nobody can explain what happens when a button is pressed. This article covers why decomposition goes too far, the cost revealed by jump counts, and how to recover the flow.

5 min read
Cover image for [Code Smell #3] Ravioli code: code whose flow nobody understands

Here is a common code-review situation: every function in the file is five lines or fewer.

This continues from the previous article Code Smell #2.

The names are good, and every class has exactly one responsibility. There is nothing obvious to criticize.

Yet nobody can immediately answer, “What happens when this button is pressed?”

This kind of code is called Ravioli Code (C2 original).

It was not always an insult

Interestingly, the term was not always used negatively. Ravioli is pasta made of small pieces that neatly wrap a filling.

Well-encapsulated small objects—the very ideal object-oriented programming aimed for. Ravioli is even used as the opposite of spaghetti code.

The problem is the count.

Each piece is perfect, but 200 are on the plate, with no record of their order or connections.

It looks like this.

final class CheckoutCoordinator {
    func start() { validator.validate(cart) }
}

final class CartValidator {
    func validate(_ cart: Cart) { stockChecker.check(cart.items) }
}

final class StockChecker {
    func check(_ items: [Item]) { priceCalculator.calculate(items) }
}

final class PriceCalculator {
    func calculate(_ items: [Item]) { paymentPreparer.prepare(items) }
}
// ... There are sixteen more classes like this

Every class is beyond reproach. Its name is precise, and it does one thing.

But no file knows the entire payment flow.

The flow exists only in the calls between classes, and you can discover it only by attaching a debugger, not by reading the code.

Why does decomposition go too far?

The rule “shorter functions are better.” Clean Code even says functions should be two or three lines, four at most.

The advice was really about keeping one abstraction level per function. Once converted into a line-count rule, its purpose disappears.

The result is thirty five-line functions, twenty-eight of which are called from exactly one place.

When the name does not summarize the content. Names such as handleUserAction, processData, and updateState do not tell you what they do.

When code is split into functions with such names, readers still have to open the bodies, and the number of places to inspect increases with every split.

A good extraction should make the body unnecessary to read; this does the opposite.

When abstraction levels are mixed. A function may mix policy-level statements such as “validate the order” with detail-level statements such as “increment the index by 1.” The reader’s perspective keeps moving up and down.

Mindlessly splitting code in this state creates fragments with inconsistent abstraction levels.

Extracting one-use code for possible reuse. It is the same mindset that creates lasagna code; only the direction changes from vertical to horizontal.

Diagram comparing a chain of Ravioli classes with an orchestrator function
A single place to document the flow solves much of the problem

The cost appears as the number of jumps

The cost of Ravioli code can be summarized in one sentence: how many files must you open to answer one question?

When reading code, people hold the flow in short-term memory. It starts to fade beyond three or four items.

After jumping through six files, you lose sight of what you were looking for and return to the beginning.

When this repeats, running the code to verify it becomes faster than reading it.

Other side effects follow.

  • When adding a feature, you cannot find the existing fragment, so you create another similar one. Duplication quietly grows.
  • Unable to find where to fix a bug, you attach a temporary workaround at the end of the flow—the UI side.
  • Because the entire flow is not recorded in code, it survives only in documents or people’s heads. When that person leaves the team, it disappears too.

How to recover the flow

Create one place to document the flow. This is the highest-impact action.

Have one function that shows the entire order at a glance, and make it state only the order.

func checkout(_ cart: Cart) async throws -> Receipt {
    try validate(cart)
    try await reserveStock(cart.items)
    let amount = calculateTotal(cart)
    let payment = try await charge(amount)
    return try await confirm(cart, payment)
}

The details remain where they belong. The difference is that the flow is written on one screen.

Such a function is sometimes called an orchestrator. This is exactly the place Ravioli code is missing.

Keep one abstraction level per layer. All five lines of the function above are statements at the same level. If a detail such as items.count > 0 intrudes, the level is broken.

The habit of checking whether the sentences are at the same eye level is more useful than a decomposition rule.

Decide whether to extract based on the name. The criterion is not line count but this question: “Does the name attached to this fragment tell me more than its body?”

Extracting if user.age >= 19 as isAdult(user) reveals intent, so it helps. Extracting array.append(item) as addItem gains nothing.

Keep nearby things nearby. Put code that changes together in the same file and folder.

If types that are always opened together are scattered merely to follow the convention of one type per file, proximity is better than convention.

The same criterion applies at the service level. A state where microservices are split too finely is called nanoservices.

A structure that passes through twelve services to handle one request is Ravioli code extended beyond the network. It adds latency and failure points to the jump cost.

Image comparing the structures of three code smells: spaghetti, lasagna, and ravioli
All three score perfectly at the fragment level

Lasagna, Ravioli, and Spaghetti

Placed side by side, the three can be summarized as follows.

Form State of fragments Problem
Spaghetti Large and tangled Execution flow cannot be predicted
Lasagna Layered vertically A single change must cross multiple layers
Ravioli Small and tidy The flow between fragments exists nowhere

All three have a high cost of understanding the whole. Only the method differs.

If code quality is measured only by the beauty of individual fragments, lasagna and ravioli remain invisible. Both score perfectly at the fragment level.

Summary

  • Ravioli code has so many small, tidy fragments that nobody can explain the overall flow.
  • It has also been used positively for well-encapsulated code. The problem is the number of pieces.
  • Line-count rules, vague names, and mixed abstraction levels are the main causes.
  • The key remedy is a place where the flow is written down.
  • Extract based on the name, not the length. Extract only when the name says more than the body.

The next installment is the opposite extreme: not too many fragments, but only one.

We look at the God Object, where one class knows the entire app.

Sources and verification criteria

  • Ravioli Code — C2 Wiki · author’s original text · verified 2026-08-17 · basis: the Ravioli Code metaphor of small, encapsulated objects becoming excessively fragmented

Continue reading