Software systems exist to solve problems—automating processes, managing data, enforcing logic. But simply “getting the job done” isn’t enough. Systems must also meet a wide range of requirements: performance, security, usability, maintainability, and more. The one force that threatens all of these is complexity.

Complexity makes systems harder to reason about. It hides bugs, invites security holes, and slows down change. The more intricate the rules, the more expertise you need to understand or modify the system safely.

In software architecture, we often define Architecture Decision Records (ADRs)—statements that explain key design decisions. Think of them as the software equivalent of legal paragraphs. And just like laws, the more exceptions and conditional logic you pile onto these statements, the harder they are to apply, enforce, or even comprehend.

Consider the difference between a legal system that says “A flat 20% income tax applies to all incomes” versus one with hundreds of exceptions, exemptions, and special cases. The former can be understood at a glance. The latter requires specialists, loopholes emerge, and outcomes become unpredictable.

Software is no different. When we can make broad, simple architectural statements—and enforce them consistently—we make systems easier to understand, safer to modify, and more resilient. Simplicity isn’t naivety. It’s a design choice, a form of discipline.

This post launches a series on The Art of Simplicity—an exploration of what it is, why it matters, and how we can wield it in our systems.

We start by laying the groundwork: What does simplicity truly mean?

What Is Simplicity? Link to heading

Simplicity is the quality or condition of being easy to understand or do.
— Oxford Languages

Consider how you might describe a simple software system. The explanation is usually short and direct. You can walk through what it does, how its parts fit together, and why it behaves the way it does. The rules are few, and they apply consistently.

Now think about describing a complex system. The explanation gets longer—full of special cases, historical decisions, and “it depends” qualifiers. The behavior varies based on context. Relationships between parts aren’t always obvious. Even small changes require careful thought and testing, because it’s not clear what might break.

This is what happens as systems evolve. Features are added, exceptions pile up, and old assumptions stick around. The more of these a system accumulates, the harder it becomes to reason about its behavior—and the less simple it becomes.

Simplicity Is About Reasoning Link to heading

When we reason about a system, we’re asking questions and working out the answers:

  • “What happens when this button is clicked?”
  • “Which component handles this request?”
  • “Does this rule apply in this situation?”

We answer these questions with statements—concise descriptions of behavior and structure:

  • “This button submits the form and shows a confirmation.”
  • “This component processes API requests from authenticated users.”
  • “This rule applies to invoices older than 30 days.”

The simpler and more consistent these statements are, the simpler the answers become. And the simpler the answers, the easier it is to reason about the system.

The Legal Analogy Link to heading

Consider a legal system. At its core, it’s made up of statements—rules that define what is allowed, what is required, and what happens when those rules are broken.

Take a simple tax rule:

“A flat 20% tax applies to all income.”

It’s clear, easy to understand, and easy to apply. Now compare it to something more typical:

“A 20% tax applies, except for incomes under 10k, students, retirees, parents, residents of certain regions, or individuals with qualifying deductions…”

This statement is more complex. As we continue adding such conditions, the rule becomes harder to interpret. Specialists are required to navigate the details. Loopholes open the door to exploitation. Mistakes become more common, and even basic decisions can be applied inconsistently.

Legal systems are complex, but they serve specific functions. They answer questions like:

  • “Is this action legal?”
  • “Who is responsible in this situation?”
  • “What penalty or payment applies here?”

To answer those questions, the system relies on clear statements (the laws) and on components (like courts or lawyers) that interpret and apply them. In many ways, this maps cleanly to a software system—where components perform specific roles, and rules define how the system behaves.

The complexity of the legal system directly affects how difficult it is to answer its questions. The same is true in software. The more conditions, exceptions, and special cases we introduce, the harder it becomes to describe and reason about the system through clear statements.

Statements in Software Link to heading

Just like legal systems are built on rules, software systems are built on statements too. We call them different things—architecture decisions, business rules, invariants—but they serve the same purpose: to define how the system behaves.

Architecture Decision Records (ADRs), for example, are formalized statements about how and why a system is designed a certain way. They capture decisions like:

“We use eventual consistency between services X and Y.” “Authentication is handled at the API gateway.” “Each tenant has a logically isolated data store.”

These are high-level statements meant to guide understanding and align reasoning across a team.

But statements don’t just live in documentation. In code, we express them most directly through tests. A test defines a setup, an action, and an expected outcome. It describes how the system should behave under a specific condition. In that way, tests are executable statements—each one both a question and an answer.

“Given this setup, when this happens, then the system should do that.”

Test-Driven Development (TDD) makes this even more explicit. It treats software development as a process of writing down expectations before writing the code that fulfills them. It’s reasoning first, implementation second.

In a way, tests are like an FAQ for the system. Each one answers a question: “What should happen in this scenario?” The more comprehensive and clear the tests, the easier it is to understand what the system is supposed to do—and whether it’s still doing it.

Strong vs. Weak Statements Link to heading

Not all statements are equal. Some are simple, clear, and easy to apply. Others are long-winded, full of edge cases, or so conditional that they lose their meaning.

Strong statements tend to be:

  • Short and focused

  • Free of exceptions

  • Easy to verify

  • Broadly applicable

Weak statements often come with qualifications:

  • “Usually this works, unless it’s a legacy record or the feature flag is off.”
  • “This service owns the data—except when it doesn’t.”
  • “We validate input at the edge, unless the request comes from this internal system…”

As complexity grows, the number of weak statements tends to increase. They overlap, contradict each other, or require mental backtracking to fully understand.

Can We Measure Simplicity? Link to heading

We often try:

  • Lines of code? Sometimes helpful—but a short function can still be convoluted.

  • Number of components? Fewer isn’t always better if responsibilities become unclear.

  • Amount of functionality removed? Only meaningful if the system still fulfills its purpose.

A more insightful approach is to look at the statements we can make about the system.

But not just how strong they are—also:

  • How many statements are there? A small set of clear rules is easier to manage than a sprawling list of exceptions.

  • How long are the statements? Simpler systems tend to be describable in shorter, more direct terms.

  • How many concepts are in each statement? The more ideas packed into one sentence, the more mental unpacking is required.

  • How deep is the logic behind a statement? A simple statement might hide complex branching underneath. Cyclomatic complexity increases the cost of understanding and verifying what “should” happen.

For example:

“All requests go through the API Gateway.”
→ Strong, short, and supported by a clear structure.

“Some requests go through the gateway, others bypass it depending on request type, environment, or legacy routing flags.”
→ Longer, conditional, and layered with internal knowledge.

The more consistent the rules, the fewer exceptions you have to remember. The fewer moving parts in each rule, the easier it is to explain, verify, and trust.

Managing Complexity Link to heading

Not all complexity is bad. Some of it is necessary—it reflects the real-world domain the system is meant to model. Business rules, regulatory constraints, performance considerations: these are examples of essential complexity. They belong to the problem itself.

But much of the complexity we encounter isn’t essential. It’s introduced by poor structure, vague naming, leaky abstractions, or inconsistent behavior. This is accidental complexity—complexity added by the solution rather than required by the problem.

A useful guideline:

  • Essential complexity must be understood and expressed.

  • Accidental complexity must be found and removed.

Sometimes we reduce accidental complexity by eliminating exceptions altogether. Other times, we clarify rather than simplify—breaking complex rules into smaller, atomic ones. For example, instead of saying:

“Students, retirees, and parents are taxed differently…”

We can separate it into distinct, verifiable statements:

“The default tax rate is 20%.”
“Students are taxed at 15%.”
“Retirees are taxed at 10%.”

The policy hasn’t changed—but the structure has. The complexity is now expressed through independent, understandable rules. It’s easier to verify, document, and reason about.

The goal is not to eliminate all complexity. It’s to make essential complexity visible—and accidental complexity disappear.

Abstraction: The Sharpest Tool We Have Link to heading

“Everything should be made as simple as possible, but not simpler.”
— Albert Einstein

Simplicity focuses on clarity. It brings the important parts to the surface and pushes the rest into the background. The goal is not to remove complexity, but to organize it in a way that makes the system easier to understand.

Abstraction is one of the main tools we use to do this. A good abstraction hides detail that isn’t relevant in the current context. It exposes a clear interface and makes behavior easier to reason about. When we use a database client, we don’t need to think about sockets or wire protocols. We just write a query. The internal complexity still exists, but it’s no longer in the way.

Some abstractions go too far. When too much is hidden, we lose understanding. The system becomes unpredictable. At the extreme, an abstraction might hide everything and reveal nothing. It becomes impossible to know how or why it behaves a certain way.

The opposite extreme is no abstraction at all. Every internal step, dependency, and decision is exposed. There’s no way to filter signal from noise. Complexity takes over because nothing is out of view.

Both cases make reasoning harder. We need to see what matters, and ignore what doesn’t. A good abstraction helps us do that. It gives us a clear place to look—and fewer reasons to look elsewhere.

Simplicity Is Earned Through Understanding Link to heading

“If you can’t explain something in simple terms, you don’t understand it.”
— Richard Feynman

Programming is a form of explanation. And while it must be precise enough for machines to execute, it is written—first and foremost—for humans to understand. Programming languages exist as a shared middle ground: a format we can reason about, and a structure machines can run.

When our understanding is shallow, the code reflects that. We patch gaps in our reasoning with conditionals, exceptions, and duplicated logic. These aren’t always wrong—but they often point to a missing insight.

Understanding brings structure. It reveals the connections between ideas that once felt separate. In math, we learn four basic operations—addition, subtraction, multiplication, division. Later, we realize subtraction is just addition in reverse, and division is just multiplication by a reciprocal. What looked like four distinct tools turns out to be two ideas seen from different angles.

The same applies to software. What seems complex often turns out to be overlapping expressions of the same underlying concept. But we only see that once we dig deep enough. Eric Evans, author of Domain-Driven Design, called this knowledge crunching—a process of exploring, refining, and eventually distilling domain knowledge until the essential structure becomes clear.

That clarity enables simpler design because it starts aligning with how the domain actually works. And once that happens, simplicity follows. That’s the result of understanding.

Simplicity Is Complexity Handled Well Link to heading

“Clarity trumps cleverness.” — John Maeda

Most real-world systems are not simple. They involve multiple concerns, constraints, and moving parts. But well-designed systems can still feel simple—because the complexity is organized, visible, and in the right place.

Simplicity does not mean avoiding complexity. It means managing it deliberately—shaping it into clear concepts, placing it in the right context, and making it accessible when needed. When complexity has a defined structure, it becomes easier to understand, test, and change.

The real danger lies in implicit complexity—rules and behavior that exist within the system, but are not clearly expressed. They are scattered across conditionals, duplicated in multiple places, or carried as unwritten knowledge among the team. The result is a system that feels unpredictable and fragile. The complexity is there—but no one can say exactly where.

Consider a common example: a discounting system. There may be rules for seasonal promotions, loyalty rewards, referral bonuses, and more. If these rules are embedded directly in scattered if-statements across the codebase, they are difficult to change, verify, or even discuss.

A clearer structure might extract this into an explicit concept—DiscountPolicy. Once named, the idea has a place in the system. It can be tested, documented, and reasoned about on its own terms.

Simplicity in this case is not the absence of rules. It is the presence of clearly defined ones—located where they belong, and expressed in terms the system and its developers can both work with.

Simplicity Is the Ultimate Sophistication Link to heading

“Simplicity is the ultimate sophistication.”
— Leonardo da Vinci

Simplicity, in its most refined form, reflects care, depth, and patience. It is shaped over time by those who have spent long enough with a system—or a problem—to see what truly matters.

At first, everything seems important. The structure follows the urgency of the moment. Ideas are entangled. Rules accumulate. But as understanding grows, what once felt essential begins to fall away. What remains is clearer, more stable, more deliberate.

This process is not quick. It requires attention to detail and comfort with revision. It requires a willingness to remove what no longer serves, and to arrange what remains so it can be understood without explanation.

Simplicity of this kind is complete, but without excess. Everything present serves a purpose. Nothing distracts from the whole.

What appears simple in the end often rests on a long history of complexity—explored, resolved, and quietly put in order.

That’s the Art of Simplicity.