It's your first week at a new job. You've checked out the repository and are scanning the package structure. The codebase is organized by *Service, *Controller, and *Repository. Within each layer, files are grouped by persistence entity: Order*, User*, Product*, and so on.
You've been asked to fix a bug in the pricing logic. Must be in OrderService, right?
You open the file. It's 4,000 lines long. Some methods are hundreds of lines.
You search for pric*. Not found.
You search the entire codebase. The word appears in seven files, scattered across variable names, constants, and magic strings. You open the first result and start reading. This doesn't seem right. You move to the next file.
You can feel the cognitive load accumulating.
Some systems make the correct starting point obvious. I'll call these breadcrumb systems.
Others require exploration. I'll call these mystery meat systems — borrowing the term from "mystery meat navigation," where the destination isn't visible until you hover over it. Unfortunately, mystery meat systems aren't unusual. They're common in codebases organized primarily around framework layers and persistence entities.
Most real systems fall somewhere in between.
Readability vs. Navigability
Designing for readability isn't just about how a file reads. It's also about how easily the system can be navigated. Navigation determines whether understanding starts immediately, or only after a search. A system can be perfectly readable and still force exploration.
Readability answers: "Can I understand this code once I'm in it?"
Navigability answers: "Can I find the right code in the first place?"
Structure determines whether capability can be found by following visible boundaries or by searching and guessing.
Exploration isn't inherently bad; it's how we learn new systems, and it's inevitable in legacy code. The problem is when exploration becomes the default mode of interaction: when every task begins with searching instead of navigating. When that happens, cognitive cost accumulates and people slow down. Navigability reduces forced exploration; it doesn't eliminate learning.
That distinction sounds subtle, but it changes the experience of working in the system. Designing code for human brains means designing how people find things, not just how they read them.
When the starting point isn't obvious, you're already using working memory before you even reach the right file. You're guessing where the logic might live, opening files and reading methods that turn out to be irrelevant. The effort adds up. Finding the code becomes part of the work.
When Structure Makes Navigation Obvious
So what does navigability look like in practice?
A system is navigable when a new developer can answer these questions without asking a teammate:
- Where does this capability live?
- What other parts of the system participate in it?
- What ties those parts together?
If those answers are visible in structure and vocabulary, navigation doesn't depend on tribal knowledge. This means when I'm fixing a pricing bug, I should have a reasonable starting point without asking a teammate or running a full-text search.
If we want navigation instead of guesswork, capability can't live only in someone's head. It has to be visible in the structure of the codebase.
At the structural level, visibility shows up in two places:
- Capability boundaries: packages or modules that reflect what the system does, not just how it runs.
- Shared vocabulary: constants, enums, and other stable identifiers that tie related pieces together across classes.
When both are present, readers can navigate the codebase without guesswork.
When those signals are missing, mystery meat systems emerge.
Mystery Meat Systems
I've noticed three anti-patterns that create mystery meat systems. Every project I've worked on in my career has included at least two of the three.
Anti-Pattern 1: Organization By Framework Layer
Most modern web frameworks encourage a layered structure:
controller/
service/
repository/
This separation is useful. It clarifies technical boundaries and keeps HTTP, application logic, and persistence concerns separate.
But those layers describe how the application runs, not what the application does.
They answer questions like:
- Is this handling a request?
- Is this performing application logic?
- Is this reading from the database?
They don't answer questions like:
- What feature does this represent?
- Where does the pricing logic live?
Framework layers are technical scaffolding: necessary, but not helpful in showing where a feature lives.
This isn't an argument against layering or a specific architectural philosophy. It's about navigability: whether the structure makes the right starting point obvious.
Anti-Pattern 2: Organization By Persistence Entity
In every project I've worked on, the team took layering a step further and organized within each layer by persistence entity:
UserController
UserService
UserRepository
OrderController
OrderService
OrderRepository
That worked for small CRUD systems, but as behavior grew, the business concepts got fragmented across the stack, and the codebase got harder and harder to navigate. Technical layers and persistence entities had become the navigation model, and over time, that model stopped working.
CRUD is a persistence concern, not a feature.
Grouping by entity answers the question:
Which table does this logic touch?
Grouping by feature answers a different question:
What feature does this code implement?
Most real features don't belong to a single table.
Pricing, for example, may span products, discounts, contracts, and tax rules — none of which cleanly belong to a single persistence entity. Refunds may touch orders, payments, customers, and accounting entries.
When everything is centered around a persistence entity, functionality tied to a single feature gets fragmented across controllers, services, and repositories. No single place represents the whole idea.
Centering logic on persistence entities forces exploration. You search and guess instead of navigating to a visible boundary.
In addition, classes named primarily after persistence entities and architectural roles (like OrderService or UserController) tend to accumulate unrelated behavior. Because the name encodes a persistence entity and architectural role rather than a feature, it doesn't constrain what belongs inside. The boundary stops signaling what belongs inside it.
Anti-Pattern 3: Missing Shared Vocabulary
Feature boundaries are visible in language too.
In mystery meat systems, important identifiers aren't encoded consistently. Instead, they appear as scattered hard-coded values embedded directly in conditional statements across multiple files.
When vocabulary isn't shared, related behavior becomes difficult to trace. Searching for functionality becomes a game of "guess and check." You look for one value, then another. You read the surrounding code to figure out if you're in the right place.
Without stable identifiers, you don't have a reliable way to track down specific concepts or functionality. This forces exploration instead of navigation: searching and guessing instead of following visible boundaries.
A codebase without shared vocabulary can't reliably reveal where a feature lives because the language that ties the related pieces together doesn't exist.
The alternative is to make functionality visible, both structurally and linguistically.
Breadcrumb Systems
In contrast, breadcrumb systems give you ways to navigate the system and find functionality without relying on another human. These systems enable navigation by organizing around features and shared vocabulary.
Pattern 1: Packages as Feature Boundaries
Once you organize around features instead of architectural layers or tables, packages become clear mental maps.
A package should answer:
What feature am I in?
You can still keep technical layers:
controller/
service/
repository/
But organize within them by feature.
controller/
pricing/
PricingController
service/
pricing/
PriceCalculator
DiscountApplier
TaxCalculator
repository/
pricing/
DiscountRepository
TaxRuleRepository
PriceListRepository
Now "pricing" is visible across the stack. The feature becomes the breadcrumb you follow through the layers.
If you're trying to fix a pricing bug, you don't scan every controller or every service. You go directly to:
controller.pricingservice.pricingrepository.pricing
In a breadcrumb system, you wouldn't start with OrderService. You'd start with pricing.
That's one way you get the benefits of proximity. Related behavior lives near related behavior.
Search can find text. Structure reveals relationships. The starting point becomes obvious, and that alone changes how the system feels to work in.
When packages reflect features, the system supports navigation instead of requiring exploration.
Pattern 2: Shared Vocabulary
Features aren't only visible in folders. They're also visible in language.
In breadcrumb systems, important identifiers are defined once and referenced consistently. Enums and well-scoped constants create stable identifiers for behavior.
That stability does more than improve readability. It creates structural benefits:
- Traceability: related behavior can be located reliably across files.
- Consistency: the same concept is referenced the same way everywhere.
When a concept has a single name and a clear home, you can navigate to its behavior instead of hunting for it.
Shared vocabulary turns language into infrastructure. It gives the system reliable reference points for its features.
When identifiers aren't stable or specific, you lose the ability to trace functionality.
Where This Goes Wrong
Like any pattern, these can be misapplied.
Structure improves navigation, but only when it reduces exploration instead of adding to it. We don't want more structure; we want clearer boundaries.
Every structural improvement carries risk.
When Packages Become Taxonomy
Packages should support navigation.
Large systems will have lots of packages. The number isn't the problem. Unclear grouping that makes navigation harder instead of easier is.
Warning signs:
- Deep nesting without clear feature boundaries
- Folders organized by abstract categories instead of features
Good structure narrows the search space by giving you a reliable breadcrumb. Poor structure forces you to explore. If structure increases search effort instead of narrowing it, it's working against you.
When Shared Vocabulary Fails
Introducing enums and constants isn't enough. They can be misused, too. Common failure modes include:
- All identifiers are centralized in one location without regard for feature boundaries.
- Constants are so abstract they don't convey any feature meaning (
TRUE,ZERO). - Constants are reused across unrelated functionality.
- Prefixes imply cohesion when they're actually unrelated.
In these cases, language creates another layer of indirection instead of reducing exploration. The system looks structured, but it isn't easier to navigate.
The Common Thread
Across all of these patterns, the failure is the same: instead of improving navigation, the structure increases indirection.
The goal isn't to add more folders or identifiers. The goal is to make features visible without requiring someone to trace execution across or search the entire system.
Good structure narrows the search space and supports navigation. Poor structure expands the search space and pushes readers into guesswork.
Breadcrumb Systems Can Emerge Gradually
Most systems don't start out with breadcrumbs.
On Taz, our system wasn't always organized around feature boundaries. For years, it followed the standard Java/Spring layering pattern — controllers, services, repositories — grouped by persistence entity. That's a common default, and it worked well enough early on.
But as the system grew, navigation became harder. Behavior related to a single feature was scattered across layers and files. Finding the right starting point required searching and guesswork.
About ten years in, we started introducing a new package structure organized around feature boundaries. The legacy packages remained layered by entity, but new features were built using feature boundaries. And as we refactored old code, we moved it into the new structure.
The system didn't flip overnight. For a while, it was part mystery meat, part breadcrumb.
But over time, the navigable surface area grew. Exploration shrank. The default starting point became clearer. Instead of accumulating entropy, the system gradually became easier to understand.
You don't need perfect architecture to improve navigability. Even inside a legacy system, you can begin introducing visible feature boundaries and shared vocabulary. Each new boundary reduces search space for the next person who has to read the code.
Navigability compounds over time. Small structural improvements, repeated consistently, shift how the entire system feels to work in.
Even if you're currently in a mystery meat system, you can start leaving breadcrumbs now.
What About Microservices?
The same principle applies even when the system is distributed.
In distributed systems, cross-service navigation is inherently harder. Some exploration is unavoidable. But inside each boundary, navigability is still possible.
You may not control the entire ecosystem. You may not be able to reorganize every microservice. But you can make the ones you touch internally navigable, with visible feature boundaries and shared vocabulary.
Global coherence may be too lofty a goal, but local coherence will help shrink the search space for the next reader.
Navigation Is The First Step
Organizing around feature boundaries makes it possible to find the right part of the system. But finding the right file doesn't mean the file itself is readable.
Once you're inside a package, the question changes:
- Does this class make its purpose visible?
- Or do you still have to simulate execution to understand it?
Structure determines whether you can find the code. Cohesion determines whether you can understand it. Navigability gets you to the right room; cohesion determines whether the room makes sense once you're inside it.
When finding the right code takes longer, every review, bug fix, and feature change slows down because the starting point is unclear.
If software performance depends on how quickly a team can understand and safely change a system, then navigability becomes part of its performance. The faster the right starting point can be located, the less cognitive overhead a change requires.
Navigation is step one. Cohesion is step two.
That leads directly to the Single Responsibility Principle as a design tool for reducing mental load, which will be my next post.
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 Code for Human Brains
Top comments (1)
Interesting! great article