Screaming Architecture
Folders should tell you what the app does.
The top-level folder structure is the architecture diagram. If you can't tell what the system is for from ls, the structure is failing.
Opinion
Almost every codebase I've walked into has the same opening tell. The top-level folders are controllers/, models/, views/, utils/, services/: a tree that screams “Rails app” or “Spring app” or “Next.js app” and is silent on what the business actually does. Robert C. Martin's 2011 essay calls this exact mistake out.1“Screaming Architecture”, blog.cleancoder.com, 30 Sep 2011. Restated as Chapter 21 of Clean Architecture (Pearson, 2017). The blueprint analogy: a house plan screams “house”; a banking system should scream “banking”. A house blueprint screams “house”, not “wood-framed structure”; a banking system should scream “banking”, not “Spring MVC”.
The cost of getting this wrong is the half-day every new joiner spends searching across controllers/, services/, repositories/, and models/ to assemble a picture of one feature. Jimmy Bogard's Vertical Slice Architecture frames the same point as a tooling argument: features that are cohesive in the requirements doc should not be scattered across the file tree.2A Philosophy of Software Design (2018, 2nd ed. 2021), Ch. 4 “Modules Should Be Deep”. The strongest steelman: folder hierarchies are a brittle proxy for the metric that actually matters — interface depth. The reply lives below. Co-locate by use case and the search becomes a glance.
The Next.js App Router pushes in the right direction here. Routes get folders; the framework files (page.tsx, route.ts, layout.tsx) live underneath the route name, not the other way around. The framework gets the small letters and the domain gets the big ones. The same shape is what A2 Three-Tier Hoisting protects from the inside: domain folders earn the architecture, generics earn a separate drawer.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
The top-level folders should name what the application does, not which framework runs it. If your `ls` reads `controllers/ models/ services/`, the architecture is failing. Name the domain — `orders/`, `billing/`, `identity/` — and let the framework files live inside. /tenet/screaming-architecture/A1
AI eyes only
Rule: top-level folders name what the application does, not the framework.
Reject: top-level controllers/, services/, models/, routes/, views/, utils/. Reject: tutorial-shape MVC layouts.
Generate: top-level folders by domain (e.g. billing/, bookings/, auth/). Framework files live one tier inside the domain folder.
Diagnostic: a contributor reads the top-level ls. If they cannot guess what the application does, the architecture is not screaming.
Why?
- A new joiner reads
lsand knows what the system does in five seconds. Onboarding shrinks to the time it takes to read the directory listing, not the time it takes to assemble a feature in your head from four scattered folders. - The framework becomes a detail. When the routes, the data layer, or the rendering strategy changes, the domain folders stay; only the framework files inside them swap. The application outlives the runtime.
- Feature work touches one folder. Code review reads one folder. The PR diff stops looking like a hairball spread across
controllers/,services/,repositories/, andmodels/. - Domain folders give the linter something to enforce. With domain at the top, an import from
billing/intoorders/is a tractable boundary check. Without it, every import looks the same and the graph is opaque. - Pairs with F1 Single Responsibility: SRP at function scale and Screaming Architecture at folder scale are the same idea climbed up the granularity ladder. One promise per unit, all the way up.
- Surfaces a Bounded Context. A folder called
orders/is a commitment to one meaning of “Order” inside it. When two meanings show up, the folder splits, and the model integrity stays intact — A6 Bounded Contexts handles the rest. - Coding agents reach for framework folders by default because the training data is loud with tutorials. Domain folders at the top short-circuit that habit and give the agent the same map the human reviewer is using.
Origins
The principle is older than its name. Ivar Jacobson's Object-Oriented Software Engineering (1992) put use cases at the centre of system design two decades before Martin gave the file-tree corollary a slogan.4Object-Oriented Software Engineering: A Use Case Driven Approach (Addison-Wesley, 1992). The conceptual ancestor: design organised around the actors and the things they do. Predates Martin's slogan by two decades. The use-case-driven approach (the entire system organised around the actors and the things they do) is the conceptual ancestor of every modern “feature folder” convention. Martin's contribution was the rhetorical move: a system whose folders do not scream the use cases is a system that has lost touch with what it is for.
Robert C. Martin's “Screaming Architecture” (2011) named the principle and gave it the building-blueprint analogy that does most of the teaching work.1“Screaming Architecture”, blog.cleancoder.com, 30 Sep 2011. Restated as Chapter 21 of Clean Architecture (Pearson, 2017). The blueprint analogy: a house plan screams “house”; a banking system should scream “banking”. The piece is short, blunt, and load-bearing: a banking system should scream “banking”; a healthcare system should scream “healthcare”; a system that screams “Rails” or “Spring” has buried its purpose under its delivery mechanism. Restated as Chapter 21 of Clean Architecture (2017), it became the spine of the layered-architecture argument: the framework is a detail, the use cases are the application.5Clean Architecture (Pearson, 2017), Ch. 21 “Screaming Architecture”. The framework is a detail; the use cases are the application. The folder tree is the architecture diagram.
Jimmy Bogard's Vertical Slice Architecture (2018) is the modern restatement with a stronger emphasis on tooling.3“Vertical Slice Architecture” (2018) plus the 2018 NDC talk. Frames feature folders as a reaction to layered MVC: each slice owns its handler, validator, command, and read model. Slices are organised around the request that drives them (the use case) and each slice owns its handler, validator, command, and read model. The slice is the unit of change; the framework folders are emergent. Milan Jovanović's walkthrough is the most cited modern primer on the same idea.6“Vertical Slice Architecture” (2023) and the companion piece “Screaming Architecture” (2023). The most cited modern primers on feature-organised .NET, with worked examples that map directly to A1.
Eric Evans's Domain-Driven Design (2003) supplies the reason a folder name is worth defending: a domain folder is the visible edge of a bounded context, and the ubiquitous language of that context lives inside it.7Domain-Driven Design (Addison-Wesley, 2003), Part IV “Strategic Design”. A domain folder is the visible edge of a Bounded Context. When two contexts share a folder, the model integrity is gone before the first import line. When two contexts share a folder (orders/ doing both checkout and fulfilment under one ambiguous “Order” type) the system has lost its model integrity, and the file tree is the first place to read the loss. See A6 Bounded Contexts for the cross-context coding rule that catches this from the inside.
Quotes
Architectures should shout the intent of the system. A good architecture screams its use cases.
Frameworks are tools to be used, not architectures to be conformed to. If your architecture is based on frameworks, then it cannot be based on your use cases.
Minimise coupling between slices, and maximise coupling within a slice. Each slice can decide for itself how to best fulfil the request.
The most important technique for managing software complexity is to design systems so that developers only need to face a small fraction of the overall complexity at any one time.
Evidence
Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01“Screaming Architecture”SupportsThe blog post that named the principle. A house blueprint screams “house”; a banking system should scream “banking”. Use cases first, framework second.
- 02The book-length restatement. The framework is a detail; the use cases are the application. The folder tree is the architecture diagram.
- 03“Vertical Slice Architecture”SupportsThe .NET-community restatement. Slices organised around the request — handler, validator, command, read model all co-located. Reaction to layered MVC.
- 04“Vertical Slice Architecture”SupportsModern primer with worked examples. Frames slices as the unit of change and the framework folders as emergent rather than imposed.
- 05“Screaming Architecture”SupportsCompanion piece focused on the top-level folder argument. Pairs with the VSA piece to give a complete picture of feature-organised architecture in modern .NET.
Sixteen sources across supports, qualifiers and opposers. The supporters cluster on the use-case-first thesis: Martin's blog post and Clean Architecture chapter, Bogard's Vertical Slice frame, Jovanović's modern primers. The qualifiers beneath the canon carry the harder reading: folders that name a domain are only useful if the domain itself is well-bounded. The opposers further down carry the steelman the reply has to address: a folder name is a brittle proxy for what the code actually does.
Examples
// Before: top-level says 'Next.js app'. Nothing names the domain. ls is silent.import { getTenetById } from "@/lib/services/tenet-service";import { buildPageMetadata } from "@/lib/controllers/page-controller";import { Pillar } from "@/lib/models/pillar-model";export default function Page() { const tenet = getTenetById("a1"); return <TenetCard tenet={tenet} />;}
// After: top-level says what the system is for. ls is the architecture diagram.import { getTenetById } from "@/lib/pillar-canon/get-tenet-by-id";import { buildTenetPageMetadata } from "@/lib/pillar-canon/build-tenet-page-metadata";import { Pillar } from "@/lib/pillar-canon/pillar.types";export default function Page() { const tenet = getTenetById("a1"); return <TenetCard tenet={tenet} />;}
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| import/no-restricted-paths | eslint-plugin-import | any import that crosses a domain boundary — orders reaching into billing, billing reaching into identity. The folder name becomes the lint rule. |
| no-restricted-imports | ESLint core | deep relative paths (../../) that punch through folder boundaries — the import shape that quietly defeats Screaming Architecture by routing around the public surface. |
| forbidden / required dependency rules | dependency-cruiser | the same domain-boundary contract as a standalone CI step — useful when ESLint is not the surface, e.g., monorepo boundary checks across packages. |
| import/no-cycle | eslint-plugin-import | import cycles between domain folders — the surest sign two folders should be one, or that a missing extraction is hiding under both. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
plugins: { import: importPlugin },
rules: {
'import/no-restricted-paths': ['error', {
zones: [
{
target: './src/orders',
from: ['./src/billing', './src/identity', './src/scheduling'],
message: 'cross-domain import — go through the published interface, not the internals.',
},
{
target: './src/billing',
from: ['./src/orders', './src/identity', './src/scheduling'],
message: 'cross-domain import — go through the published interface, not the internals.',
},
],
}],
'no-restricted-imports': ['error', {
patterns: [
{
group: ['../../*'],
message: 'use domain-relative imports; reaching across folders is the smell A1 is here to catch.',
},
],
}],
}
});AI rules
.cursor/rules/a1-screaming-architecture.mdc---
description: Prickles A1 — Screaming Architecture
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---
## Prickles A1 — Screaming Architecture
The top-level folder structure is the architecture diagram. Read your `ls` aloud — it should name what the system does, not which framework it runs on.
Top-level folders name domains (orders, billing, identity, scheduling), not framework artefacts (controllers, models, views, services). Framework files live one level down, inside the domain folder they serve.
Cross-domain imports are linted, not encouraged. Two domains share a contract by going through a published interface, never by reaching into each other's internals.
Refuse to add a folder that names a framework concept at the top level. If you would add `controllers/` or `models/`, name the domain instead and put those files inside it.Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.
Counter-argument
The strongest steelman is John Ousterhout's in A Philosophy of Software Design.2A Philosophy of Software Design (2018, 2nd ed. 2021), Ch. 4 “Modules Should Be Deep”. The strongest steelman: folder hierarchies are a brittle proxy for the metric that actually matters — interface depth. The reply lives below. A folder name is a string the compiler ignores. Two files in the same folder may be loosely coupled; two files in opposite corners of the tree may share state through a singleton. Module depth (how much capability the interface hides) is the metric that actually predicts whether a system is hard to work with, not the spelling of the directory it lives in. The implication for Screaming Architecture is sharp: folders that scream “orders” can still produce a tangled order subsystem if the interface boundaries inside them are shallow.
Counter-argument retort
Ousterhout is right that a folder name is not a guarantee. The reply is that the folder name is a commitment, not a proof, and a commitment expressed in the file tree is one a linter can enforce. Once orders/ exists, an import from billing/orders/ back into orders/ is a build failure, not a vibe. The proxy stops being brittle the moment the import direction is enforced (A2 Three-Tier Hoisting handles the upward-only flow; A6 Bounded Contexts handles the cross-context one).
The depth concern is real but lives one level down from this tenet. Screaming Architecture names which folders sit at the top; module depth governs how those folders organise their internals. Both rules ship; neither replaces the other. Ousterhout's framing is what keeps orders/ from becoming a dumping ground.2A Philosophy of Software Design (2018, 2nd ed. 2021), Ch. 4 “Modules Should Be Deep”. The strongest steelman: folder hierarchies are a brittle proxy for the metric that actually matters — interface depth. The reply lives below. When the team starts arguing about whether two files are “part of orders,” the question to ask is the depth one: does the order module's public surface hide a meaningful capability, or has it leaked into a hundred caller-visible files?
The novelty challenge (that Vertical Slice Architecture and feature-folders already ship this rule under different names) is fair, and the rule borrows from both. Bogard coined “Vertical Slice” for the .NET community in 2018, framing it as a reaction to layered MVC.3“Vertical Slice Architecture” (2018) plus the 2018 NDC talk. Frames feature folders as a reaction to layered MVC: each slice owns its handler, validator, command, and read model. What Screaming Architecture adds, by name, is the top level commitment. A repo can have feature folders four directories deep and still scream “Spring” from the root ls. The point of this rule is the first directory level, not the slicing strategy underneath it.
The genuine residue is when the system genuinely is “a Rails app” or “a Next.js site” in the sense that there is no domain (a CRUD admin tool, a framework demo, a code-coverage reporter). In those cases the framework is the product, and Screaming Architecture sensibly screams the framework. Most production codebases are not those cases; they are domain systems running inside framework folders.
Notes
- [1]Robert C. Martin — “Screaming Architecture”, blog.cleancoder.com, 30 Sep 2011. Restated as Chapter 21 of Clean Architecture (Pearson, 2017). The blueprint analogy: a house plan screams “house”; a banking system should scream “banking”.
- [2]John Ousterhout — A Philosophy of Software Design (2018, 2nd ed. 2021), Ch. 4 “Modules Should Be Deep”. The strongest steelman: folder hierarchies are a brittle proxy for the metric that actually matters — interface depth. The reply lives below.
- [3]Jimmy Bogard — “Vertical Slice Architecture” (2018) plus the 2018 NDC talk. Frames feature folders as a reaction to layered MVC: each slice owns its handler, validator, command, and read model.