Local Reasoning
“The best code is the code you can understand by looking at it.” — Michael Feathers
Understand This First
- Boundary – clear boundaries make local reasoning possible.
- Separation of Concerns – mixed concerns force you to understand multiple domains at once.
Context
At the heuristic level, local reasoning is the ability to understand what a piece of code does by reading only that piece, without tracing through distant files, global state, or implicit side effects. It’s a quality that emerges from applying patterns like Boundary, Separation of Concerns, and KISS well. It’s one of the strongest predictors of whether code is pleasant or painful to maintain.
In agentic coding, local reasoning matters for both humans and models. A context window is finite. If understanding a function requires loading five other files into context, the agent must spend its limited working memory on navigation rather than problem-solving. Code that supports local reasoning is code that agents (and tired humans at 11 PM) can work with effectively.
Problem
How do you write code that can be understood in isolation, so that a reader doesn’t need to reconstruct the entire system in their head before making a change?
Most bugs and most development time live in the gap between what a developer thinks code does and what it actually does. The wider the gap between reading a function and understanding its behavior (because of hidden state, action at a distance, or implicit contracts) the more likely that gap contains a mistake.
Forces
- Global state allows distant parts of the system to affect local behavior in invisible ways.
- Implicit conventions (naming patterns, call order dependencies) create knowledge that exists only in developers’ heads.
- Clever abstractions can hide important details behind layers that look simple but behave unpredictably.
- Performance optimizations often sacrifice locality for speed. Caching, lazy initialization, and shared mutable state all make local reasoning harder.
Solution
Write code so that each function, method, or module tells you what it does without requiring you to read anything else. Several practices support this.
Name things precisely. A function called processData could do anything. A function called validateEmailFormat tells you what it does and what it doesn’t do. Good names reduce the need to read implementations.
Make dependencies explicit. Pass values as parameters rather than reaching into global state. If a function needs a database connection, take it as an argument; don’t import a global singleton. Explicit dependencies are visible at the call site.
Limit side effects. A function that reads input and returns output, changing nothing else, is trivially local. A function that writes to a database, sends an email, and updates a cache requires understanding all three systems to predict its behavior. Isolate side effects at system boundaries.
Keep functions short and focused. Not because of an arbitrary line count, but because a function that does one thing is a function you can understand without scrolling.
When reviewing agent-generated code, check whether you can understand each function without opening another file. If you find yourself jumping between files to trace behavior, ask the agent to refactor for locality: make dependencies explicit and reduce hidden coupling.
How It Plays Out
A developer is debugging a failing test. The test calls a function that reads from a configuration object. The configuration object is populated at startup by a chain of initializers that merge environment variables, file settings, and command-line flags. To understand what value the function sees, the developer must trace through three files and reconstruct the merge order. The function looked simple; the behavior wasn’t local.
Refactored, the function takes its configuration values as parameters. Now the test passes the values directly, and anyone reading the function can see exactly what it depends on. The debugging session that took forty-five minutes would have taken two.
An agent is asked to add a feature to a codebase with heavy use of global state. It introduces a subtle bug because it doesn’t account for a side effect in an unrelated module that mutates a shared variable. The agent’s context window contained the function it was modifying but not the distant module. Code that required global reasoning to modify safely was modified without it.
“This function reads from a global configuration object, which makes it hard to test. Refactor it to accept configuration values as parameters so anyone reading the function can see exactly what it depends on.”
Consequences
Code that supports local reasoning is faster to read, safer to change, and easier for both humans and agents to work with. It reduces onboarding time and debugging time. It makes code reviews more reliable because a reviewer can evaluate a change without understanding the entire system.
The cost is that local reasoning sometimes requires more explicit code. Passing dependencies as parameters instead of using globals adds verbosity. Making contracts explicit through types or documentation takes effort. And some problems (concurrent state, distributed systems, performance-critical paths) resist locality by nature. In those cases, contain the non-local parts and document them clearly so the rest of the system can remain local.
Related Patterns
- Depends on: Boundary — clear boundaries make local reasoning possible.
- Depends on: Separation of Concerns — mixed concerns force you to understand multiple domains at once.
- Enables: KISS — local code tends to be simpler code.
- Enables: Context Window — local code uses less of an agent’s working memory.
- Contrasts with: Make Illegal States Unrepresentable — both reduce errors, but through different mechanisms: locality through readability, illegal states through type constraints.
- Degraded by: Premature Optimization – optimized code is harder to reason about locally.