Skip to main content

Writing Simple Code

· 6 min read

"There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult." — Tony Hoare

Simple code is not a consolation prize for teams that can't handle complexity. It's the goal.

What Simple Means

"Simple" gets misused. It gets applied to code that's short, code that avoids advanced language features, code that a junior developer wrote. None of those are what it means.

Simple code is code where the model is visible. Read it, and the business rules are apparent. The cases the system handles, the conditions it branches on, the decisions it makes — legible directly from the source. Not inferred from the behavior of a chain of abstractions.

The previous articles in this series argued that software is fundamentally a modeling activity, and that code is an encoding of the model. Simple code is what it looks like to take those ideas seriously in practice. A good encoding makes the model easy to read back out. That's simplicity.

Decisions Are the Content

Business code is mostly decisions. If the customer is tax-exempt, skip the tax. If the order exceeds the threshold, apply the discount. If the account is suspended, reject the request. The model is a collection of rules, and rules are decisions.

Decision logic should be plain. Use if. Use pattern matching. The simplest construct that expresses the condition clearly is the right one. When you read the code, the branch should be obvious — "this case handles tax-exempt customers." Not "this delegates to a strategy that... let me follow the dependency chain to find out what it actually does."

The complexity belongs in the model — in having the right rules, the right distinctions, the right cases. Not in how you express them. Decisions are simple. Express them simply.

Model the Data, Simplify the Logic

The cleanest way to make decisions explicit is to model the shapes your data can take. Compare two ways of representing the same business rule.

With imprecise data, the logic has to compensate:

# isTaxExempt: bool, taxRate: float | None
if customer.isTaxExempt:
if customer.taxRate is not None:
# shouldn't happen, but it does
log.warning("tax-exempt customer has a tax rate set")
total = subtotal
elif customer.taxRate is not None:
total = subtotal * (1 + customer.taxRate)
else:
# neither flag set — now what?
raise ValueError(f"customer {customer.id} has no tax configuration")

With precise data, the logic writes itself:

match customer:
TaxExempt -> subtotal
Taxable(rate) -> subtotal * (1 + rate)

The first version has guards for contradictory states that shouldn't exist, a warning for a case that "shouldn't happen" (but does), and a runtime error for a configuration the type system should have prevented. The second version has none of that. The data shape is precise, so the decision logic is too. Legible. Verifiable. Hard to miss a case.

This is why state machine representations map well to business problems. Model the data well, and the logic follows naturally.

Don't Hide the Model Behind Infrastructure

Indirection has legitimate uses. When a component genuinely varies at runtime — pluggable behavior, environment-specific implementations — abstraction earns its place. The problem is reaching for indirection when the structure is fixed at compile time.

A pipeline with five stages that never changes at runtime doesn't need a dynamic plugin registry. It can just be five function calls. A set of discount rules that's determined at startup doesn't need a strategy factory. Write the rules. When you use runtime machinery to express compile-time structure, you've buried the model. Reading the code now means tracing through layers of configuration before you can see what the system actually does.

Make fixed things explicit. The structure is visible in the code, not hidden in the wiring of a framework. Anyone can read it.

The common objection is: "but what if it needs to change later?" The answer is: change the code. You don't need an abstraction layer to support future change — you have source control. And simple, direct code is far easier to change than a dynamic system where the structure is implicit in configuration. Abstraction doesn't make change easier. It makes the current structure harder to read, which makes every future change harder.

The same principle applies to separating business logic from technical concerns. Billing rules shouldn't be tangled with HTTP request handling. Discount calculations shouldn't depend on the database layer. When business logic is mixed with infrastructure, reading the model requires navigating both simultaneously. Keep them separate and each becomes easier to read — and change — independently.

Write for the Team You Have

Software teams aren't static. They're staffed uncertainly — often with junior developers, often with people of mixed skill levels. Many won't understand advanced patterns, much less apply them correctly. Probably even your senior developers don't. Code gets maintained by people who weren't there when it was written, under time pressure, without full context.

Clever code is expensive. Every person who reads it pays a cognitive cost to decode the indirection before they can even start thinking about the business problem. That cost is paid repeatedly, by everyone, for as long as the code exists. Simple code is cheap. The structure is plain, the decisions are visible, and changes can be made with confidence because the model is legible.

This isn't about writing for the least experienced person on the team. It's about recognizing that code is read far more than it's written, in circumstances you can't predict. The investment in clarity compounds over time. The investment in cleverness decays.

AI coding assistants compound this further. They read the codebase to understand context when generating additions or modifications. A model that's legible in the source — explicit decisions, well-typed data, business logic separated from infrastructure — is a model an AI can reason about accurately. Obscure abstractions confuse AI the same way they confuse a developer new to the codebase. The code that's easiest for a human to work with is also the code that works best with AI assistance.

The Implication

If software is a modeling activity, and code is an encoding of the model, then the quality of the code is determined by how faithfully the encoding represents the model and how easily that model can be read back out.

Simple code isn't a style preference or a skill ceiling. It's the most precise encoding — the one where the model is most visible, the decisions are most obvious, and the rules are hardest to misread. Complexity for its own sake doesn't make the model better. It makes the encoding worse.

The simplest code that correctly encodes the model is the best code.