The Directory is the Diagram
Why Your File Tree Should Scream Your Intent
Part of the Codifying Your Architecture series
- The Directory is the Diagram
You open a new codebase or even your own project from long time ago. You look at the file tree in your editor. What do you see?
In most cases, you’ll see a sea of directories that describe the code’s technical role, not its business purpose. I’ve encountered this in almost every ecosystem; it’s the default starting point for many frameworks, but it’s a convention that fails to scale as a project grows. For example:
- In a Java Spring application, you might see packages like
com.example.web,com.example.service, andcom.example.repository. - In a Ruby on Rails or .NET MVC app, you’ll find top-level directories named
controllers,models, andviews. - In a modern React frontend, you’ll likely find
components,hooks, andapi.
In every case, the structure tells you about the code’s technical role. It’s a controller. It’s a service. Or a UI component.
But what is the business capability? You can’t tell. Managing ticket payments? Calculating user reputation? It’s all invisible, buried under a generic structure that the framework likely forced on you from day one.
Imposed vs. Encouraged
The degree of enforcement varies. Some frameworks, like Next.js with its file-based routing, technically impose a certain structure. In other ecosystems, like Spring Boot, it’s less of a technical requirement and more a powerful common practice driven by documentation and tutorials. In either case, the result is the same: the business domain gets lost behind technical layers.This marks the start of a new series: Codifying Your Architecture.
The mission is to close the gap between the whiteboard and the IDE. I want to stop treating architecture as a set of lofty, abstract principles and start treating it as something tangible that you can actually see and enforce in your code. If my architecture doesn’t manifest as a physical constraint within my codebase, is it truly my architecture, or just a shared hallucination?
We’ve known the solution for decades. People like Robert Martin have been shouting about “Screaming Architecture” since 2011, urging us to close the “model-code gap” and write “architecture-haunted code.” Yet, the central truth remains ignored: architecture that isn’t visible in the code is just a fantasy. In this post, I’ll explore how to make your architecture manifest through its structure, how to enforce its boundaries directly in your codebase, and how this tangible approach closes the gap between whiteboard dreams and codebase reality.
Ilities are Goals, Dependencies are Mechanisms
In my previous series, From Patterns to Practice, I discussed architecture in terms of “ilities” (Quality Attributes like Scalability, Modularity, Reliability, and Maintainability). These “ilities” are the goals; they describe why I’m building it a certain way.
But a goal like “Scalability” doesn’t just live in a Jira ticket or a PowerPoint slide. It lives in the code. If Module A is tightly coupled to Module B in a way that prevents independent deployment, you have inadvertently fused them into a single Architectural Quantum. By fusing them into a single quantum, you’ve just killed your scalability goal.
This brings me to the core reconciliation: The “Ilities” are the Goal. The “Dependencies” are the Mechanism.
We cannot Achieve Modularity (the Goal) without Encapsulation and controlled Dependencies (the Mechanisms). If we care about Maintainability, we must ensure that a change in one part of the system doesn’t unexpectedly break another. The only way to guarantee that is by meticulously distinguishing between Good Coupling and Bad Coupling.
Strip away the lofty principles, and you’re left with one reality: architecture is about defining and enforcing dependencies and boundaries. Your dependency graph is the physical reality of your architectural choices, acting as the guardrails that ensure the “ilities” you promised on the whiteboard actually survive in the IDE.
The Many Faces of Architecture
Software Architecture is a vast discipline encompassing infrastructure, data consistency, security protocols, and even team organization. However, when we focus on architecture within the codebase, dependencies are the primary way your design choices manifest. The compiler doesn’t understand abstract scalability goals; it only understands which modules are allowed to interact with each other.Building the Structure
To build a structure that reveals its intent, you stop organizing by technical layers and start organizing by business capability. I’ll use Java for this example to demonstrate how standard language features can enforce architectural intent, but the concepts apply to any language.
Before (Package by Layer):
1src/main/java/com/citypulse/
2├── controllers/
3│ ├── TicketController.java
4│ ├── PaymentController.java
5│ └── UserController.java
6├── services/
7│ ├── TicketService.java
8│ ├── PaymentService.java
9│ └── UserService.java
10└── repositories/
11 ├── TicketRepository.java
12 ├── PaymentRepository.java
13 └── UserRepository.java
After (Package by Component / Screaming Architecture):
1src/main/java/com/citypulse/
2├── ticketing/
3│ ├── TicketService.java <-- PUBLIC: The only entry point
4│ ├── web/
5│ │ └── TicketController.java <-- Internal: Adapter (driving)
6│ ├── domain/
7│ │ ├── Ticket.java <-- Internal: Domain Logic
8│ │ └── TicketRepository.java <-- Internal: Interface (port)
9│ └── persistence/
10│ └── JpaTicketRepository.java <-- Internal: Adapter (driven)
11├── payments/
12│ ├── PaymentService.java
13│ ├── web/
14│ ├── domain/
15│ └── persistence/
16└── users/
17 ├── UserService.java
18 ├── web/
19 ├── domain/
20 └── persistence/
In this structure, the ticketing package acts as a distinct module.
The 'Shared' Gravity Well
Experienced developers know that as soon as you start creating components, someone will ask: “Where does the DateHelper go?”
The temptation is to create a common/ or shared/ directory. Be extremely careful. shared is where modularity goes to die. Every time you add a class to a shared module, you create a “gravity well” that couples every other component together. If two modules need the same three helper functions, I often prefer Duplication over Coupling. Copy the code. Keep the boundaries clean.
Is This Over-Engineered?
You might look at the nested directories (web, domain, persistence) for a simple feature and think: “This is too much.” For a CRUD app, you are right. But we are designing for evolvability.
By explicitly separating web (HTTP concerns) from domain (Business Logic), we ensure that a change to our API framework doesn’t ripple into our business rules. We accept a little extra directory nesting today to prevent a “Big Ball of Mud” tomorrow.
Is This Just Renaming Directories?
At this point, a natural question arises, perhaps with a mocking tone: “So, architecture is just about directory names? TicketingController, TicketingRepository, and TicketingModel instead of Controller, Repository, and Model?”
You are absolutely right to ask that. If we just move files around and change nothing else, we haven’t architected anything. We have just rearranged the furniture. The directory structure is the physical manifestation of your architecture, designed for human cognition. But the real architecture lives in the logical constraints that structure enables you to enforce.
One is for your team to understand the domain; the other is for your build tool to protect it.
Architect's Log: The 'Where Does This Go?' Friction
I have fought this battle in a dozen codebases. The hardest part isn’t the file move; it’s the muscle memory. Developers love “Buckets of Types” (like a controllers/ directory) because they don’t have to think about the domain to file a new class. It’s lazy, and it’s the exact reason why six months into a project, nobody knows where anything actually lives.
When you switch to Package by Component, you force a difficult question at the moment of creation: “What business capability does this code serve?” This friction is a feature, not a bug. It forces architectural thinking into the daily coding loop.
Here’s the deeper reality of what this enables:
1. It’s About Encapsulation
In a traditional “Package by Layer” approach (e.g., src/controllers, src/services, src/repositories), almost everything has to be public or exported. This means a PaymentService can casually bypass the TicketingService’s business logic and directly grab data from TicketRepository if TicketRepository is also public. That’s architectural rot, leading to a “distributed monolith” in microservices contexts or an entangled mess in a monolith.
In a “Package by Component” or “Screaming Architecture” approach, you group TicketController, TicketService, and TicketRepository within a Ticketing/ directory. Now, TicketRepository can be made package-private or internal. The rest of the application can only interact with the well-defined public interface of the Ticketing component (e.g., exposed by TicketService).
This is the practical implementation of Strong Module Isolation where we physically prevent other parts of the app from coupling to internal details by enforcing encapsulation directly in the code.
Crucially, in Java, within each component’s root package (com.citypulse.ticketing), you would make only the TicketService.java class public.
Other classes like Ticket, TicketRepository (interface), TicketController, and JpaTicketRepository can be made package-private (default visibility). This means no other package outside com.citypulse.ticketing can accidentally import the Ticket domain object or call JpaTicketRepository directly. They must go through the defined public API (TicketService).
Advanced: Java Modules (JPMS)
For even stricter enforcement, you can use the Java Platform Module System (JPMS). By creating amodule-info.java for each component, you can explicitly export only the packages you want to be public. This enforces boundaries at the compiler level, though it comes with increased build configuration complexity.2. It’s About Dependency Direction
Architecture is defined by what depends on what.
If your directory structure looks like src/ticketing and src/payments, you can finally draw a line in the sand. Ticketing can use Payments. Payments cannot use Ticketing. Simple. Tools can enforce this, and the “knot” never forms in the first place.
3. The “Extraction” Test
This serves as a practical stress test for the strength of my architectural boundaries.
Scenario: Your Ticketing domain has grown too large or complex for its current home (whether that’s a Monolith or an existing Microservice) and needs to be split into its own independently deployable unit.
- Layered/Framework Architecture: You have to hunt through
controllers/,services/,models/, andutils/, untangling thousands of cross-references. This “surgery” is so risky and expensive that teams often decide to just live with the bloat, leading to the “Big Ball of Mud.” - Screaming Architecture (Package by Component): You drag the
Ticketing/directory out of the project. You fix a handful of compile errors where the component’s well-defined public interface was called. You are done.
The structure facilitates this future evolution because the architectural boundary is already clear and enforced.
Architect's Log: The 10% Solution
Let’s be realistic: dragging a directory out of a project is only the first 10% of extracting a microservice. You are only solving for Connascence of Name (imports).
The real pain lives in the other 90%: Connascence of Data (shared database tables) and Connascence of Timing (synchronous call chains). A clean directory structure isn’t a magic wand that creates microservices; it is simply the prerequisite that makes tackling the harder 90% survivable.
The 'Microservices' Trap
It is common to think, “We use Microservices, so we don’t need this; our boundaries are the network calls.”
Be careful. Service boundaries are rarely perfect on day one. A “User Service” often grows to encompass “Auth,” “Profiles,” and “Preferences.” If the internal code of that service isn’t modular, you can’t split it later when it inevitably becomes a bottleneck. You end up with a “Distributed Monolith”: services that are too big to maintain but too entangled to split.
Enforcing the Boundaries
If the directory structure is the map, then automated tools are the border patrol. Without them, your boundaries are just suggestions. People will cross them. They’ll smuggle in a “quick” import from another module just to meet a deadline. Eventually, your “sovereign” components become a single, messy territory again.
You need guards at the gate. You need a system that checks every single “crossing” (import) and turns back anyone without the proper clearance. That’s what automation provides: a border that actually exists.
I will cover Architectural Fitness Functions in depth later in this series, but for now, we can implement a basic check to ensure our “Blueprint” is respected.
Tools like ArchUnit for Java allow you to write unit tests that assert architectural rules. For instance, you can assert that ticketing and payments remain decoupled:
1@AnalyzeClasses(packages = "com.citypulse")
2public class ArchitectureTest {
3
4 @ArchTest
5 static final ArchRule componentIsolation =
6 slices().matching("com.citypulse.(*)..")
7 .should().beFreeOfCycles();
8
9 @ArchTest
10 static final ArchRule webShouldNotAccessPersistence =
11 noClasses().that().resideInAPackage("..web..")
12 .should().dependOnClassesThat().resideInAPackage("..persistence..");
13}
This simple test acts as a ratchet. If a developer on the Payments team tries to import a class from Ticketing to save time, the build breaks. The architecture is no longer just a diagram on a wiki; it is a compilation error.
The Cost of Upfront Design
This approach is not a silver bullet. It requires more upfront thought to identify the core business components. I once spent a week over-engineering boundaries for a “Notifications” module that turned out to be three lines of code; but when the domain finally stabilized, having those (initially wrong) boundaries made the cleanup trivial.The Blueprint is Just the Beginning
Architecture starts with the shape of your code. By grouping files by business purpose, you turn the directory structure into a tangible first line of defense against chaos. This clarity isn’t about being neat, it’s about paying the architectural cost upfront to avoid the bankruptcy of a “Big Ball of Mud” later.
But directories alone are just a blueprint, not a fortress. In our next post, “The Formal Contract,” we’ll lock down these boundaries using the compiler itself, turning good intentions into enforceable guarantees.
The code examples for this series are available on GitHub: vijayanant/codifying-architecture-examples



