Information Foraging: How Developers Navigate Systems
Developers rarely read code from top to bottom. They navigate it. When debugging or extending a system, developers follow cues that suggest where relevant behavior might live, such as:
- Directory structure and package names
- Class names
- Method names
- Enums and constant values
Each cue helps answer a constant question during exploration: "Where should I look next?"
This behavior resembles patterns studied in human-computer interaction research. Information Foraging Theory describes how people search for information by following cues that signal where valuable information is likely to be found.
When the cues are strong, developers can quickly eliminate large portions of the system and focus only on relevant areas. When they are weak, developers must explore much more of the system before they can understand the behavior.
Software systems differ dramatically in how much of the codebase developers must explore to understand behavior.
Cognitive surface area describes how much of a system developers must mentally explore to understand or change behavior.
Imagine debugging a refund bug: customers who should receive refunds are not getting them.
You notice a refunds package in the service layer. Inside it is a class named RefundPolicy. You open it. The class contains a single public method and a few helpers, all focused on refund eligibility. Within a few seconds you know you're looking in the right place. Most of the system has already been ruled out. You only had to explore a small portion of the codebase.
Now imagine the same bug in a system with weaker cues.
Packages reveal only architectural layers (controller, service, repository) but not functionality. No classes contain "refund" in their names. A full-text search returns seventeen results scattered across CustomerService, EmailService, OrderService, PaymentService, and ScheduledJobs. Each class is large, and refund-related logic appears in several different places. Instead of quickly eliminating irrelevant areas, you must open and read multiple files just to determine whether they are related to the bug. Much more of the system must be explored before you know you're finally in the right place.
In the first system, the structure provides clear breadcrumbs that guide exploration. In the second, the cues are weak and the developer must wander through the codebase. Breadcrumb systems create strong cues that guide exploration. In mystery meat systems, developers are forced to wander. (I explored this distinction in more detail in Mystery Meat vs. Breadcrumb Systems.)
Search Space: Why Cognitive Surface Area Matters
When debugging or extending code, developers search the system for where behavior lives or should live. For example, when tracking down a bug:
- Is it in the front end or the back end?
- Which package is it in?
- Which class is it in?
- Which method is it in?
- What state triggered the behavior?
Each question defines part of the search space a developer must explore. The more places a bug could plausibly live, the harder it becomes to narrow the investigation. Good structure narrows it by helping developers rule out large portions of the system quickly.
Large systems aren't necessarily difficult to understand. Systems with large search spaces are. Some systems remain navigable even as they grow, while others become confusing surprisingly quickly.
The difference often isn't size, but cognitive surface area.
When a system has a small cognitive surface area, the path from a question ("Why is this refund failing?") to the responsible code is short. When surface area is large, developers must search much more of the system before they can find the relevant behavior.
Question: "Why did this refund fail?"
Small Surface Area
──────────────────
service/
refund/
RefundPolicy
Large Surface Area
──────────────────
service/
CustomerService
EmailService
OrderService
PaymentService
ScheduledJobs
I've reviewed files only a hundred lines long that were surprisingly difficult to follow because understanding them required exploring several unrelated concerns. But I've also reviewed files twice that size that were much easier to understand because the structure was predictable and the naming was clear.
The same contrast appears at the system level. I've worked in legacy codebases that were decades old and relatively easy to navigate, and in younger codebases that were much harder. The difference wasn't age or scale. It was how much of the system I had to mentally explore to get my work done.
Four Sources of Cognitive Surface Area
When developers investigate behavior in a system, they must find answers to several questions.
- Where should I look?
- What concepts do I need to understand?
- What patterns does this system follow?
- What other components influence this behavior?
These questions correspond to four different ways systems expand the amount of code developers must explore.
| Developer Question | Surface Area |
|---|---|
| Where should I look? | Navigation surface area |
| What concepts do I need to understand? | Concept surface area |
| What patterns does this system follow? | Structural surface area |
| What other components influence this behavior? | Dependency surface area |
These four dimensions describe common sources of cognitive surface area within the code itself.
Navigation surface area concerns how many places behavior might live. Structural surface area concerns whether developers can predict where behavior should live.
The dimensions are not entirely independent. Many problems increase several types of surface area at once. The goal of the framework is not perfect categorization, but to highlight the different ways systems become harder to understand.
Each surface area expands for different reasons:
- Navigation surface area grows when behavior could plausibly live in many places.
- Concept surface area grows when multiple ideas must be understood at once.
- Structural surface area grows when the system's organization is unpredictable.
- Dependency surface area grows when many components influence the behavior.
When teams say a codebase is "hard to understand," it often means one or more of these surface areas has grown too large.
Some systems naturally have large search spaces because the domain itself is complex. Good architecture can't eliminate that complexity, but it can prevent unnecessary cognitive surface area from accumulating.
Navigation Surface Area
Navigation surface area relates to the question: Where should I look?
It describes how many places a developer might need to search to locate the behavior they are investigating.
When navigation surface area is large, developers must explore many parts of the system before finding the relevant code. When it is small, much of the system can be ruled out immediately.
Layer-Based Package Dumping Grounds
When packages are organized solely by architectural layer (controller, service, repository), package names provide little guidance about where behavior lives. A developer can't rule out files by package and must search across many classes to locate the relevant functionality.
service/
CustomerService
EmailService
OrderService
PaymentService
ScheduledJobs
A developer trying to locate refund logic must open multiple classes to determine where it lives. The navigation surface area is large because many files remain plausible candidates.
Compare that to functionality-oriented packaging:
service/
refund/
RefundOrchestrator
RefundPolicy
RefundProcessor
Now the search space is much smaller. Developers can navigate directly to the part of the system that matches the functionality they are investigating.
Entity God Classes
Some systems organize controller, service, and repository classes around persistence entities. Over time, these classes accumulate many unrelated behaviors associated with that table.
For example:
class CustomerService {
calculateLoyaltyDiscount(...)
processRefund(...)
sendPromotionalEmail(...)
}
Because many unrelated behaviors accumulate in the same class, developers must search through large files to locate the behavior they need.
Compare that with classes organized around specific behaviors:
LoyaltyDiscountCalculator
RefundPolicy
PromotionalEmailSender
Now the class name itself helps narrow the search space.
Microservices
In distributed systems, navigation begins at the microservice level. Before debugging can begin, a developer must determine which service owns the behavior. Each additional plausible service expands the navigation surface area. If service boundaries are unclear, developers may need to explore several services before locating the relevant behavior.
When service boundaries align clearly with business capabilities, the search space is small. When they don't, the navigation surface area grows.
Concept Surface Area
Concept surface area relates to the question: What concepts must I understand?
It describes how many concepts a developer must hold in mind simultaneously while reading code.
Mixed Responsibilities
Classes with multiple responsibilities force developers to switch between conceptual frames while reading the code.
The problem is not that a piece of code performs several steps. Many methods legitimately coordinate a workflow. For example, a method that executes a refund process may validate the request, calculate the refund amount, call a payment gateway, and record the result. Those steps all belong to the same conceptual frame: executing the refund workflow.
Mixed responsibilities appear when a boundary begins to absorb several different kinds of reasoning that do not belong to a single conceptual frame.
For example, a single class or method might contain:
- business rules and policy decisions
- financial calculations
- persistence logic
- external API integration details
- notification behavior
- analytics or auditing decisions
Even if each individual piece of logic is understandable, the reader must constantly switch between different kinds of thinking while reading the code. Instead of reasoning about one idea at a time, the developer must juggle several unrelated concerns simultaneously.
When responsibilities are separated into clearer conceptual units, each piece of code can usually be understood within a single mental frame. This reduces the number of ideas developers must keep in working memory at once and makes the system easier to reason about. (This is how the Single Responsibility Principle protects working memory.)
Unclear Concept Boundaries
Concept surface area also increases when a single concept is scattered across multiple components instead of having a clear home.
For example, refund eligibility rules might appear in several places:
// OrderService
if (order.isExpired()) {
throw new RefundNotAllowedException();
}
// PaymentService
if (order.getAmount().compareTo(MAX_REFUND_AMOUNT) > 0) {
throw new RefundNotAllowedException();
}
// CustomerService
if (customer.isFlaggedForFraud()) {
throw new RefundNotAllowedException();
}
A developer trying to understand refund rules must inspect multiple classes and mentally assemble the full policy from scattered pieces.
Compare that with a centralized design:
RefundPolicy
Now the concept has a clear boundary. Developers know exactly where to look to understand the rules.
Structural Surface Area
Structural surface area relates to the question: What patterns does this system follow?
It describes how predictable the organization of the system is.
In predictable systems, developers can make strong assumptions about where certain types of logic belong. In unpredictable systems, those assumptions break down, forcing developers to inspect more of the codebase to confirm where behavior lives.
Structural surface area grows when developers can't rely on consistent structural rules about where certain kinds of logic belong.
Inconsistent Structural Patterns
Structural surface area increases when similar problems are solved using different structural patterns.
For example, validation might sometimes appear in a *Validator class, but other times be embedded directly inside a *Service class.
// Pattern A
CustomerValidator
// Pattern B
CustomerService.validateCustomer(...)
Now developers cannot rely on a single structural rule to locate validation logic. They must remember that validation may appear in multiple structural forms.
In systems with consistent patterns, developers quickly learn where different kinds of logic belong.
Inconsistent Naming Patterns
Predictable naming patterns reinforce structural expectations, because they act as structural cues developers rely on to predict where logic belongs.
Consider the following class names:
CustomerService
CustomerManager
CustomerHelper
CustomerUtils
CustomerProcessor
The roles of these classes are unclear, making it difficult to infer what logic belongs in each one.
Compare that with names that constrain responsibility:
CustomerCreditLimitValidator
CustomerRefundEligibilityPolicy
CustomerLoyaltyPointsCalculator
These names create structural expectations about what logic belongs in each class. For example, CustomerCreditLimitValidator clearly suggests that only credit limit validation belongs there.
Clear naming reduces structural surface area because developers can predict where logic should exist.
Structural Drift
Structural surface area can also grow gradually as teams introduce exceptions to previously clear patterns.
Imagine a system with a clear place for refund eligibility rules:
RefundEligibilityPolicy
Later, a special case appears in unrelated code:
if (order.getCustomerType() == VIP) {
return true; // vips always get refunds
}
Then another rule is implemented separately:
VipRefundPolicy
Now refund eligibility logic exists in multiple structural forms:
- a central policy class
- inline conditional logic
- a special-case policy class
Developers can no longer infer where new rules should live because the structural pattern has broken down.
Dependency Surface Area
Dependency surface area relates to the question: What other components influence this behavior?
It describes how many other parts of the system must be understood in order to reason about the code being examined.
When behavior depends on many other components, developers must explore those dependencies before they can confidently understand or modify the code.
Distributed Workflow Chains
Dependency surface area increases when behavior propagates through multi-step workflows across microservices.
A developer might change this code in an order service:
order.setRiskCategory(calculateRiskCategory(order));
That looks local. But the risk category may later be read by a saga orchestrator, passed to a payment service, and then used by a fraud service to decide whether the transaction should be rejected.
The dependency is not visible where the value is first written. To understand the impact of the change, the developer must trace how that data is consumed across several downstream steps.
The more behavior depends on these distributed workflow chains, the larger the dependency surface area becomes.
Implicit Side Effects
Dependency surface area also increases when code triggers behavior that is not visible where the call occurs.
For example:
orderRepository.save(order);
This appears to simply persist the order. But it might also trigger:
- inventory updates
- fraud checks
- event publication
- email notifications
If these side effects occur through framework hooks, triggers, or event listeners, they may not be visible at the call site.
Understanding the behavior requires knowledge of interactions that occur elsewhere in the system.
Event Chains
Event-driven systems can expand dependency surface area. This trade-off is often intentional (event architectures provide flexibility and decoupling), but they also make system behavior harder to reason about.
Publishing an event:
eventBus.publish(new OrderPlacedEvent(order));
might trigger several consumers:
InventoryServiceEmailServiceBillingServiceAnalyticsService
To understand the full consequences of placing an order, developers must track how the event propagates through the system.
Long event chains make it harder to predict the downstream effects of a change.
These dimensions describe four different ways systems increase the amount of code developers must explore to understand behavior.
Cognitive surface area grows when developers must explore more of the system to understand behavior.
Mechanical Size vs Cognitive Surface Area
We usually measure system size mechanically: lines of code, binary size, runtime performance. Those metrics say very little about how difficult a system is to understand. Two systems can contain the same number of lines of code but impose very different cognitive demands. What matters is how much of the system developers must explore to understand behavior.
Human working memory is limited. When developers must load too many unrelated concepts at once, reasoning slows and mistakes become more likely.
A large system can still have a small cognitive surface area if developers can quickly navigate to the code that contains the relevant concepts. When boundaries are clear and structure is predictable, developers only need to load the concepts relevant to the task at hand into their working memory.
A large system with a small cognitive surface area typically has:
- Clear boundaries between packages, classes, and methods
- Predictable structure
- Strong navigation cues
A large system with a large cognitive surface area tends to have:
- Scattered logic
- Inconsistent patterns and unpredictable structure
- Hidden dependencies
Traditional code complexity metrics focus on control flow inside individual functions — how difficult the code is to simulate mentally. Cognitive surface area focuses on how much of the system developers must explore to understand behavior — how difficult the system is to navigate and reason about.
How Architecture Shrinks Cognitive Surface Area
Many well-known design principles in software engineering reduce one or more dimensions of cognitive surface area:
| Practice | Primary Surface Area Reduced |
|---|---|
| Domain-oriented package organization | Navigation surface area |
| Clear, descriptive naming | Navigation surface area |
| Single Responsibility Principle | Concept surface area |
| Small, focused methods | Concept surface area |
| Consistent architectural patterns | Structural surface area |
Predictable naming conventions (*Validator, *Calculator) |
Structural surface area |
| Loose coupling | Dependency surface area |
| Dependency injection | Dependency surface area |
Individually, these practices can feel like stylistic preferences or architectural taste. Collectively, they shape how easy a system is to explore and reason about. This is why large codebases can still feel easy to navigate, while small systems can feel overwhelming when their structure is unclear.
Good architecture helps minimize how much of the system developers must explore to locate, understand, and reason about behavior.
Why Cognitive Surface Area Slows Teams Down
Cognitive surface area directly affects engineering velocity because it determines how much of the system developers must explore before they can safely modify behavior.
Large surface area means developers must explore more of the system before they can safely modify behavior. This slows debugging, pull-request review, onboarding, refactoring, and incident response.
Because a large portion of engineering time is spent understanding existing code rather than writing new code, these exploration costs dominate day-to-day development work. These delays compound across the team: onboarding takes longer, pull requests require more context to review, and incidents take longer to diagnose. When developers must search more of the system, load more concepts into working memory, navigate inconsistent patterns, or reason about hidden dependencies, progress slows. (I explored how readability constrains performance in Readability is a Performance Constraint.)
Reducing cognitive surface area shortens the path between seeing a problem and understanding the code responsible for it.
AI Is Expanding Cognitive Surface Area Faster
Traditionally this growth happened slowly. AI changes that dynamic by dramatically reducing the cost of producing code. Systems can now grow faster than teams can control the cognitive surface area they create.
AI-generated features introduce new classes, modules, and services rapidly, often based on local context rather than system-wide architectural patterns unless those patterns are explicitly reinforced. If the surrounding code already contains structural problems, like inconsistent patterns, overloaded abstractions, or unclear boundaries, AI will often reproduce them.
Without guardrails, AI can unintentionally increase:
- navigation surface area (more places code might live)
- concept surface area (more responsibilities mixed together)
- structural surface area (more inconsistent patterns)
- dependency surface area (more hidden interactions)
The result is systems that grow quickly but become progressively harder to understand.
AI makes it easier to produce code, but it does not automatically make systems easier to navigate. (I previously explored how AI makes readability more important, not less.)
Diagnosing Cognitive Surface Area
When a codebase feels difficult to understand, the difficulty usually traces back to one or more of these questions:
Navigation surface area:
- Do I know where the relevant behavior lives?
- Are there many plausible places to check?
Concept surface area:
- How many different ideas are mixed together in this code?
- Do I have to hold multiple responsibilities in mind at once?
Structural surface area:
- Does the system follow predictable patterns?
- Can I guess where similar logic should exist?
Dependency surface area:
- What other components influence this behavior?
- Are those dependencies visible from the code in front of me?
When several of these questions are difficult to answer, developers must explore more of the system to understand what it is doing.
Managing Cognitive Surface Area
The cheapest way to manage cognitive surface area is to prevent it from growing in the first place. But most teams inherit systems that already contain large cognitive surface area. In those environments, improvement usually happens gradually.
One approach we used on Taz was to clean as we went. We added a new base package to our monolith where all new code went. Legacy code remained isolated in the old base package. As old features were extended or refactored, they were moved into the new base package. Over time, the amount of legacy code shrunk.
Managing cognitive surface area often involves small improvements applied consistently:
Reduce Navigation Surface Area
- Organize packages around functionality, not just architectural layers
- Break up packages that contain unrelated functionality
- Use clear, descriptive package and class names
Reduce Concept Surface Area
- Refactor multi-responsibility classes
- Apply the Single Responsibility Principle to new classes
- Use names that are fences, keeping related functionality inside and unrelated functionality outside
- Break large methods up using helper methods
Reduce Structural Surface Area
- Maintain consistent architectural patterns
- Use predictable naming conventions
- Correct structural drift when introducing new code
Reduce Dependency Surface Area
- Limit hidden side effects
- Prefer explicit dependencies
- Limit dependencies across unrelated modules
AI can help with many of these tasks. Teams can encode rules into agent instructions or automated code reviews. AI can assist with refactoring, renaming, and decomposing large classes. Used carefully, AI can reduce cognitive surface area instead of expanding it.
The Goal: Keep the System Navigable
Software systems inevitably grow. But they don't have to become unnavigable as they grow. The goal of good architecture is not only to control complexity inside individual components, but also to keep the cognitive surface area of the system manageable over time.
The real difficulty of a system isn't its raw complexity. It's how much of the system developers must explore before they understand it.
Reducing cognitive surface area helps you find the code. It limits how much of the system you need to search and understand just to locate behavior.
But finding the code is not the same as feeling confidence that it's safe to change. You can reach the right place in the system and still hesitate, still wondering what you might be missing.
Navigation tells you where to go. It doesn't tell you whether you understand enough to make a safe change — and that's the gap I'll explore next in my next article, Stoppability in Code Design.
AI was my editor, but these ideas are my own.
This article is part of a broader series exploring how code structure, navigability, and cohesion align with cognitive limits.
If you're interested in the deeper dive, the full series is here:
Designing Systems for Human Brains
Top comments (0)