Clean Code Is About Boundaries: Everything Else Is Just a Side Effect

For years, developers have debated the minutiae of clean code. Should methods be five lines or fifteen? What makes a good variable name…

For years, developers have debated the minutiae of clean code. Should methods be five lines or fifteen? What makes a good variable name? How long should a class be? We’ve created linters, formatting tools, and extensive style guides all aimed at enforcing these rules.

But we’ve been asking the wrong questions.

Clean code isn’t about counting lines or perfecting names. These visible artifacts are merely symptoms of a deeper discipline. At its core, clean code is about one fundamental skill: knowing where to draw boundaries.

Once you understand this, everything else falls into place. You don’t need to memorize a hundred rules. You need to master one decision.

Every Decision Is a Boundary Decision

When you write code, every choice you make is answering a single question: what belongs together, and what must be separated?

Should this logic live in this method or that one? Boundary decision.

Should this data structure be passed here or transformed there? Boundary decision.

Should these two classes know about each other? Boundary decision.

Should this configuration live with the code or outside it? Boundary decision.

The problem is that most developers don’t consciously recognize this. They’re making boundary decisions constantly but without a framework for evaluating them. So they fall back on rules of thumb that treat symptoms rather than root causes.

“Keep methods short” is trying to create boundaries without understanding why.

“Use meaningful names” is trying to mark boundaries without knowing what they protect.

“Follow Single Responsibility Principle” is trying to enforce boundaries without recognizing the costs.

The Spectrum of Proximity

Imagine a room full of people. When everyone sits close together, personal space becomes ambiguous. Someone’s elbow encroaches on another’s space. Conversations bleed together. Individual boundaries become fuzzy and contested.

Now imagine the same people arranged in distant groups. The separation is obvious. Each group has clear space. No one questions where one group ends and another begins.

Your code works exactly the same way.

When concepts sit close together in your codebase, they naturally want to share space. A helper method reaches into another class’s data. A utility function knows too much about the caller’s context. A component starts depending on implementation details it shouldn’t see.

This isn’t developers being sloppy. This is the natural consequence of proximity. Things placed near each other will find ways to connect, to share, to depend. It’s entropy in action.

The art of clean code is understanding this force and working with it intentionally. You place things close when you want them to share freely. You separate them when you need isolation. The distance between code constructs isn’t arbitrary — it’s the primary tool you have for controlling relationships.

Boundary Placement as the Root Decision

Boundary placement is not one decision among many. It’s the root decision from which all other patterns emerge.

Consider the Single Responsibility Principle. Developers often struggle with it because it seems subjective. What is a “responsibility”? How do you know if something has one or many?

But frame it as a boundary question and it becomes clear: you’re drawing a boundary around a reason to change.

Imagine a class that handles user registration. It validates input, saves to the database, and sends a welcome email. That’s three different reasons to change:

  • Validation rules might evolve (new password requirements)
  • Database schema might change (new user fields, different storage)
  • Email template or delivery mechanism might change (new provider, different format)

Each represents a different stakeholder, a different timeline, a different type of change. When business wants new validation rules, they shouldn’t have to risk breaking email delivery. When you switch email providers, you shouldn’t have to touch database code.

The boundary question is: do these things change independently or together? If independently, they need separation. The boundary isolates each reason to change so that addressing one doesn’t force you to understand or risk breaking the others.

SRP isn’t about making classes small. It’s about ensuring that when change happens — and it always does — it’s contained within a boundary rather than rippling across multiple concerns.

Or take the Open-Closed Principle. You can memorize “open for extension, closed for modification,” but what does that mean in practice?

It means: draw boundaries around the parts that vary.

Say you have payment processing code. Today you support credit cards. Tomorrow you need PayPal. Next month, cryptocurrency. The week after, buy-now-pay-later services.

Without boundaries, each new payment method forces you to modify the existing payment processing logic. You add if-else branches, new parameters, special case handling. The core payment flow becomes increasingly fragile as it accumulates knowledge about every payment type.

With a boundary — a PaymentMethod interface — you create a stable contract. The payment processor depends only on that contract: “give me something that can process a payment.” It doesn’t know about credit cards or PayPal or anything else. That knowledge is trapped inside each implementation, behind the boundary.

Now when you add cryptocurrency support, you create a new class implementing PaymentMethod. The payment processor never changes. It was closed to modification but open to extension through the boundary.

The boundary protects the core logic from the variation. What changes (payment methods) stays on one side. What stays stable (the processing flow) stays on the other.

Every design pattern is a boundary pattern. Factory? A boundary separating object creation from object use. Strategy? A boundary isolating algorithmic variation. Observer? A boundary decoupling state changes from state reactions.

When you see patterns as boundary solutions, you stop memorizing recipes and start understanding the forces at play. You can invent your own patterns because you understand what boundaries accomplish.

How Boundaries Create Reasoning Ability

The human brain can hold roughly seven pieces of information in working memory. This isn’t a guideline — it’s a biological constraint.

When you read code, you’re loading a mental model. Every variable, every method call, every class relationship consumes slots in your working memory. Exceed the limit and you start forgetting things. You lose track of what you were reasoning about.

This is where boundaries save you.

A well-placed boundary is a forgetting mechanism. It lets you forget what’s on the other side. You can reason about a function without loading the entire codebase into your head because boundaries tell you what you can safely ignore.

Think about reading a method that calls another method. If there’s no clear boundary, you need to understand:

  • What the called method does
  • What state it depends on
  • What it might modify
  • What errors it might throw
  • How it might behave differently in different contexts

That’s five additional things to track, and you’ve only looked at one method call.

Now put a boundary there. Maybe it’s an interface with a clear contract. Maybe it’s a pure function with no side effects. Suddenly you need to track far less:

  • What the contract promises
  • What you’re passing in

The implementation details are trapped behind the boundary. You can reason about your code without understanding theirs. You’ve compressed a complex reality into a simple abstraction.

Poor boundaries destroy reasoning ability. When everything can affect everything else, when data flows freely across all constructs, when knowledge is globally shared, you can’t reason locally anymore. You have to understand the entire system to safely modify one line.

Good boundaries create islands of comprehensibility. You can understand this component without understanding that one. You can modify this layer without analyzing three others.

The difference between a codebase that makes sense and one that doesn’t often comes down to whether the boundaries support local reasoning or demand global understanding.

What Crosses Boundaries

Understanding what can and cannot cross boundaries is where the real skill lives. There are four primary things to consider: data, behavior, knowledge, and change.

Data crossing boundaries is the most visible. When you pass a parameter to a function, you’re moving data across a boundary. The question is: what form does it take?

Primitive data crosses easily. An integer is an integer. But complex data structures carry implicit knowledge. Pass an entire object and you’ve given the receiver access to everything that object knows and can do. You’ve widened the boundary opening.

This is why Data Transfer Objects exist. They’re not bureaucracy — they’re boundary control. You deliberately choose what data crosses and in what shape, rather than letting implementation details leak through.

Behavior crossing boundaries happens through delegation. When one component calls another, behavior flows across the boundary. The critical question is: does the caller need to understand how the behavior works, or just what it accomplishes?

Interfaces and abstract contracts are tools for controlling this. They let behavior cross while keeping implementation trapped on one side.

Knowledge crossing boundaries is the subtlest and often the most damaging when uncontrolled. Knowledge is what one part of your system understands about another part.

When a component knows that another component uses a specific database, that knowledge has crossed a boundary it shouldn’t. When a UI component knows the exact shape of your API response, knowledge leaked across an architectural layer.

The Law of Demeter — “don’t talk to strangers” — is really about knowledge boundaries. It says: only know about your immediate neighbors, not about the entire graph of relationships.

Change crossing boundaries is the ultimate test. When you modify code on one side of a boundary, does it force changes on the other side?

If yes, your boundary is weak or misplaced. A good boundary isolates change. You can refactor, optimize, or completely rewrite one side without touching the other, because the boundary contract remains stable.

The Cost of Boundaries

Every boundary you create introduces coordination overhead. Two pieces of code separated by a boundary must communicate through that boundary’s interface. They must serialize and deserialize data. They must handle errors across the boundary. They must maintain the boundary contract.

In a monolithic codebase, this might mean extra function calls and parameter passing. In a distributed system, it might mean network calls and serialization costs. The boundary determines the performance characteristics.

Boundaries also create indirection. Following the flow of execution through code with many boundaries means jumping through multiple layers of abstraction. This makes debugging harder. It makes performance optimization more complex. It makes the codebase harder for newcomers to navigate.

And boundaries create duplication. Not always code duplication, but conceptual duplication. The same data might need to exist in different forms on either side of a boundary. The same validation might happen at multiple layers. This is often necessary and correct, but it’s still a cost.

This is why boundary placement is hard. You’re not just separating concerns — you’re making tradeoffs.

Too few boundaries and you get the big ball of mud. Everything affects everything. Reasoning becomes impossible. Change propagates uncontrollably. The codebase becomes unmaintainable.

Too many boundaries and you get needless complexity. Simple operations require orchestrating multiple components. Performance suffers from excessive indirection. Developers spend more time managing boundaries than solving problems.

The skill is finding the right level of granularity. Where do you need the isolation badly enough to pay the coordination cost?

The Discipline of Boundary Thinking

So how do you develop this discipline?

Start by making boundary decisions explicit. When you’re about to create a new class, write a new method, or add a new module, ask yourself: what boundary am I creating? What will be inside it? What will be outside? What will cross it and what won’t?

Don’t make these decisions unconsciously. Don’t fall back on “well, that’s how we always structure things.” Understand the forces at play and make an intentional choice.

Identify what changes together. This is your primary signal for what belongs on the same side of a boundary. If two pieces of logic always change for the same reasons, at the same time, by the same people, they probably belong together. A boundary between them creates coordination cost without isolation benefit.

But if they change independently, for different reasons, at different times, they need separation. The boundary pays for itself by preventing coupled change.

Recognize the spectrum of distance. Boundaries aren’t binary. Code in the same method is closest. Same class is nearby. Same package is further. Different module is distant. Different service is very far. Microservices are at the extreme end.

Each level of distance has different costs and benefits. Choose the right distance for the relationship you need.

Pay attention to what wants to cross. When you find yourself constantly passing complex data structures across a boundary, or when changes ripple across boundaries frequently, your boundary is probably in the wrong place.

The code is telling you where the natural seams are. Listen to it.

Accept the costs. Some coordination overhead is the price of maintainability. Some indirection is necessary for flexibility. Some duplication is required for isolation. Don’t optimize these costs away if they’re serving a purpose.

But also don’t accept them blindly. Every boundary should earn its keep.

Beyond the Rules

This is why clean code can’t be reduced to a checklist. You can’t create a rule that says “methods should be X lines long” because the right length depends on what boundary you’re trying to create.

You can’t mandate “always use dependency injection” because whether you need that boundary depends on whether the dependency varies independently.

You can’t enforce “no class longer than Y lines” because sometimes concepts are genuinely coupled and should live together, and sometimes they need clear separation despite being individually small.

The rules are trying to approximate good boundary decisions, but they’re imperfect proxies. They work some of the time. They mislead you the rest of the time.

Once you understand boundaries, you don’t need the rules anymore. You make each decision based on the actual forces in your system:

  • What changes together?
  • What needs to be understood together?
  • What needs to vary independently?
  • What’s the cost of coordination here?
  • What’s the cost of coupling here?

The answers depend on your domain, your team, your constraints. They’re contextual. They require judgment.

This is what makes senior developers valuable. Not that they’ve memorized more patterns, but that they’ve developed better judgment about boundaries.

The Path Forward

If you want to write cleaner code, stop focusing on the surface-level rules. Stop arguing about brace placement and line length and naming conventions — these are solved problems with adequate tooling.

Instead, develop your boundary intuition.

Look at code you find readable and ask: where are the boundaries? What’s crossing them? Why does this feel easy to understand?

Look at code you find confusing and ask: where are the boundaries unclear? What’s leaking across them? Why does this feel hard to reason about?

When you write new code, think explicitly about the boundaries you’re creating. Write them down if it helps. Sketch diagrams showing what’s inside, what’s outside, what crosses.

When you review code, evaluate the boundaries. Are they in the right places? Do they control the right things? Do they pay for themselves?

This shift in thinking — from rules to boundaries — is transformative. It turns clean code from a list of dos and don’ts into a coherent discipline. You stop following recipes and start solving problems.

You stop wondering if your method is too long and start asking if it crosses a boundary it shouldn’t.

You stop debating class names and start asking what knowledge this class should hide.

You stop mechanically applying patterns and start understanding what boundaries you need to create.

Clean code isn’t about the code at all. It’s about the boundaries that make the code comprehensible, maintainable, and evolvable.

Everything else is just a side effect.

Read more