Screaming Architecture
The core idea
Look at the top-level directory structure of a project. What does it scream?
A good architecture screams the use cases of the application — Health Care System, Accounting System, Inventory Management. It does not scream Rails, Spring, Hibernate, Django, or Next.js. The framework is scaffolding you happened to use; it is not the point of the system.
Uncle Bob's analogy: blueprints for a house scream "house" — you see the foyer, the kitchen by the dining room, the bedrooms. Blueprints for a library scream "library." Your code should be as legible. A new developer should be able to glance at the structure and understand what the system does before learning what it was built with.
"Software architectures are structures that support the use cases of the system." — Ivar Jacobson, Object Oriented Software Engineering
The principle is fractal. Uncle Bob framed it at the top-level directory, but the same test applies at every zoom level. A name, a function, a class, a table, an endpoint — each is a tiny architecture, and each should scream its intent, not its mechanism. Zoom all the way in and the rule is identical: reveal what this does and why it exists; hide what it was built with. See "Screaming at every scale" below.
The test: what does it scream?
Open the repo root and read the top-level names. Ask: framework or purpose?
| Screams the framework (bad) | Screams the purpose (good) |
|---|---|
controllers/ models/ views/ |
ordering/ billing/ shipping/ |
app/ config/ db/ lib/ |
patients/ appointments/ claims/ |
pages/ api/ components/ |
catalog/ checkout/ inventory/ |
services/ repositories/ dtos/ |
underwriting/ policies/ payouts/ |
If someone can name your web framework faster than they can name your business, the architecture is screaming the wrong thing.
This isn't about banning controllers/. It's about the top level. Frameworks and delivery
mechanics belong in a lower, replaceable layer — not as the primary organizing principle.
The web (and every framework) is a delivery mechanism
The web is an I/O device — a detail. So is the database, the message queue, the CLI, the mobile app. These are ways to deliver the use cases to a user; they are not the use cases.
A system organized around its use cases lets you defer the big technical decisions:
- Which database? Decide later — the domain doesn't care if it's Postgres or a flat file.
- Which web framework? Decide later — or swap it — without touching business rules.
- REST, GraphQL, gRPC, CLI? A delivery choice bolted onto the same core.
If those decisions are baked into the center of your system, you've coupled your business to tools that will outlive their welcome. Push them to the edge.
Frameworks are tools, not ways of life. When a framework's docs say "structure your whole app like this," ask two questions:
- How can I preserve the use-case emphasis of my architecture?
- How can I keep the framework from taking over?
Use the framework; do not marry it. Keep your domain able to run when the framework is gone.
Screaming at every scale
The directory listing is just the widest zoom. Take the same test — intent, or mechanism? — all the way down. At each level, the failure mode is the same: the code names the how (the data structure, the generic operation, the framework) instead of the what (the business concept, the use case, the intent).
Names
A name should scream what a thing is for in the business, not its type or its plumbing.
| Screams the mechanism (bad) | Screams the intent (good) |
|---|---|
data, info, obj, temp, val |
pendingInvoice, expiredSession |
list, arr, map1, flag |
overdueAccounts, isEligibleForRefund |
handleData(), process(), doIt() |
settleClaim(), reserveInventory() |
UserManager, DataHelper, Utils |
LoyaltyTier, ShippingQuote |
If the name would fit unchanged in any app in any industry, it's screaming "generic CRUD," not
your product. (For the full discipline, see naming-guidelines.)
Functions
A function's signature should read like a sentence about the domain. applyLateFee(account)
tells you the use case; update(account, field, value) tells you it touches a database.
Name for the business operation, not the mechanical one. A function that screams its intent
needs no comment explaining what it does — only, occasionally, why.
Classes and types
A type is a claim about the domain. Money, EmailAddress, PolicyNumber scream intent and
carry their own rules; String, int, Map<String,Object> scream "I didn't model this yet."
Make illegal states unrepresentable so the type itself screams the invariant — a
VerifiedEmail that can't be constructed from an unverified string says more than any doc.
(See ddd for value objects and functional-design-architecture for type-driven modeling.)
Business model
The domain model should read like the business talks. Its entities, events, and operations
should be words a domain expert recognizes — Subscription, renewed, cancelPolicy — not
technical stand-ins (Record, statusChanged, setFlag). If you can't map a model element to
a sentence a stakeholder would say, it's screaming the implementation, not the domain. This is
the Ubiquitous Language: the same words in the conversation, the model, and the code.
Database schema
A schema screams intent when its tables and columns name business facts, not a framework's conventions or a bag of generic slots.
| Screams the mechanism (bad) | Screams the intent (good) |
|---|---|
data table with a JSON payload column |
orders, line_items, shipments |
type, status as opaque ints |
subscription_state, refund_reason |
field1, meta, extra |
billed_at, canceled_at, net_cents |
| everything nullable "just in case" | NOT NULL / constraints that state rules |
Constraints and column names are documentation the database enforces. A schema that lets any row hold anything screams "we deferred every decision"; one whose constraints encode the rules screams what the business actually permits.
API and event contracts
The contract is the loudest scream to the outside world — treat it as domain-first, not a thin skin over your tables or a CRUD reflex.
- Resources and events named for business concepts and actions:
POST /policies/{id}/renew,PaymentSettled,ClaimApproved— notPOST /update,RowChanged,genericEvent. - Fields that a consumer understands without reading your source:
estimatedDeliveryDate, notts2. Enumerated states with meaning, not magic numbers. - Don't leak the mechanism: no ORM entity shapes, framework wrappers, or table column names bleeding into the public contract. The contract should survive a rewrite of the internals.
The same litmus everywhere: could a newcomer read this — the name, the signature, the table, the endpoint — and tell you what the business does, before knowing what built it? If not, it's screaming the wrong thing.
Testability is the proof
The reliable check that your architecture actually screams the right thing:
You can unit-test all your use cases without the framework, without a database, and without a web server running.
If your business rules can only be exercised through an HTTP request that hits a live database, they are entangled with the delivery mechanism. If you can instantiate a use case, hand it plain input, and assert on plain output — in a fast test, no I/O — the domain is properly independent.
This is the litmus test. It's also why the discipline pays off: independence from frameworks buys you fast tests, swappable infrastructure, and a domain a human can read.
How to apply it
When starting a project:
- Name top-level modules after the business (the use cases / capabilities), not the layers.
- Put the framework at the boundary — a thin adapter that calls into the domain, never the other way around. Dependencies point inward, toward the use cases.
- Don't let
main/ the framework entry point contain business rules. It should wire things up and get out of the way.
When reviewing an existing project:
- Read the top-level directory listing aloud. Does it name the product or the toolchain?
- Grep the core domain for framework imports (
import rails,from django,@nestjs/...,next/...). Framework symbols deep in business logic = leakage. Push them to adapters. - Try to write one use-case test with no database and no server. If you can't, that's the first thing to fix.
When a framework fights you:
- Keep the framework's objects (controllers, request handlers, ORM entities) at the edge. Translate them into your own domain types at the boundary; don't let them flow inward.
- A
ponytail-sized version of this: even one folder named for a capability, with the business rule callable without booting the server, beats a textbook layer cake.
What this is NOT
- Not "never use frameworks" — use them, at the boundary, deliberately.
- Not "no
controllers/folder ever" — it's about what the top level organizes around. - Not premature abstraction — you don't need ports/adapters everywhere on day one. The minimum that earns the win: name modules for the business, and keep use cases testable without I/O.
One-line summary
At every scale — the system, the module, the name, the function, the class, the schema, the API — it should scream what it does, not what it was built with. The proof at the top is that you can test the use cases without the framework, database, or web; the proof lower down is that a newcomer can read one name or contract and tell you the business behind it.
Based on Robert C. Martin (Uncle Bob), "Screaming Architecture" (2011), taken from the
top-level directory down to every identifier and contract.
See also: clean-code, naming-guidelines, ddd, functional-design-architecture.