| name | screaming-architecture |
| description | Domain-first folder structure reflecting business capabilities. Trigger: When structuring projects or refactoring toward use-case-driven boundaries. |
| license | Apache 2.0 |
| metadata | {"version":"1.0","type":"domain"} |
Screaming Architecture
The top-level structure of a project should scream its domain, not its framework. Folder names reflect business capabilities and use cases โ not technical layers like controllers, models, or services.
Coined by Robert C. Martin: "Your architecture should tell readers about the system, not about the frameworks you used."
When to Use
- Starting a new project or service
- Refactoring a codebase grown around framework conventions
- Multiple developers struggle to locate where a business concept lives
- The folder structure reads as "Express app" or "MVC project" instead of the actual domain
Don't use for:
- Very small scripts or utilities with no meaningful domain
- Projects where the entire domain is one concept (no benefit to partitioning)
Critical Patterns
โ
REQUIRED: Domain-First Root Structure
Top-level directories name business capabilities, not technical layers.
# โ WRONG: structure screams "Express MVC"
src/
โโโ controllers/
โ โโโ orderController.ts
โ โโโ userController.ts
โ โโโ paymentController.ts
โโโ models/
โ โโโ order.ts
โ โโโ user.ts
โโโ services/
โโโ orderService.ts
# โ
CORRECT: structure screams "E-commerce"
src/
โโโ orders/
โ โโโ order.entity.ts
โ โโโ order.service.ts
โ โโโ order.routes.ts
โ โโโ order.test.ts
โโโ users/
โ โโโ user.entity.ts
โ โโโ user.service.ts
โโโ payments/
โ โโโ payment.service.ts
โโโ shared/ # cross-cutting infrastructure only
โโโ database.ts
โ
REQUIRED: Feature Self-Containment
Each domain module owns all the code it needs: models, services, routes, and tests together.
# โ
CORRECT: self-contained feature module
src/orders/
โโโ order.entity.ts # domain model
โโโ order.repository.ts # persistence interface
โโโ order.service.ts # application logic
โโโ order.routes.ts # HTTP handlers (framework at the edge)
โโโ order.dto.ts # input/output shapes
โโโ order.test.ts # tests co-located
# โ WRONG: tests and logic scattered across technical layers
src/
โโโ controllers/orderController.ts
โโโ services/orderService.ts
โโโ models/order.ts
โโโ tests/orderController.test.ts # far from the code it tests
โ
REQUIRED: Frontend Feature Structure (React / Component-Based)
Same principle, different naming convention. Frontend projects use React idioms โ no .entity, .service, .route suffixes.
# โ WRONG: organized by file type (screams "React project", not the domain)
src/
โโโ components/
โ โโโ OrderList.tsx
โ โโโ UserProfile.tsx
โ โโโ ProductCard.tsx
โโโ hooks/
โ โโโ useOrders.ts
โ โโโ useUser.ts
โโโ pages/
โโโ OrdersPage.tsx
# โ
CORRECT: organized by domain (screams "e-commerce")
src/
โโโ features/
โ โโโ orders/
โ โ โโโ index.ts โ public API โ only export what other features need
โ โ โโโ OrderList.tsx โ components: PascalCase, no suffix
โ โ โโโ OrderCard.tsx
โ โ โโโ useOrders.ts โ business logic: hooks with "use" prefix
โ โ โโโ orderStore.ts โ state: store/slice per feature
โ โ โโโ orderApi.ts โ data fetching: Api suffix
โ โ โโโ order.types.ts โ types co-located with the feature
โ โโโ users/
โ โ โโโ index.ts
โ โ โโโ UserProfile.tsx
โ โ โโโ useUser.ts
โ โโโ products/
โโโ shared/
โ โโโ ui/ โ generic UI atoms (Button, Input, Modal)
โ โโโ hooks/ โ cross-cutting hooks (useDebounce, useLocalStorage)
โโโ app/ โ framework at the edge: routing + providers
โโโ Router.tsx
โโโ providers.tsx
Rules:
- Cross-feature imports must go through the feature's
index.ts โ never import internals directly
shared/ui/ is for generic design system components, not domain-specific ones
app/ contains routing and providers only โ framework-specific, at the edge
โ
REQUIRED: Framework at the Edge
Framework-specific code lives at the outermost layer. The domain core has no framework imports.
export class Order {
confirm(): void {
if (this._items.length === 0) throw new Error('Cannot confirm empty order');
this._status = 'confirmed';
}
}
import { Router } from 'express';
import { OrderService } from './order.service';
export function orderRouter(service: OrderService): Router {
const router = Router();
router.post('/:id/confirm', async (req, res) => {
await service.confirm(req.params.id);
res.json({ status: 'confirmed' });
});
return router;
}
import { Injectable } ;
()
{ ... }
โ
REQUIRED: Shared Kernel for True Cross-Cutting Concerns
Infrastructure shared across all features belongs in a dedicated shared/ or infrastructure/ directory โ not scattered across features, and never as a dumping ground.
src/
โโโ orders/ # feature module
โโโ users/ # feature module
โโโ payments/ # feature module
โโโ shared/ # only truly shared infrastructure
โโโ database/
โโโ logger/
โโโ config/
โโโ errors/ # base error types used everywhere
โ NEVER: Technical Layering at Root
Organizing by technical role at the root creates a structure that says nothing about what the system does.
# โ WRONG โ these are all technical roles, not business concepts
src/
โโโ controllers/ # What business problem? Unknown.
โโโ middlewares/ # What domain concept? None.
โโโ models/ # Which models? All of them mixed together.
โโโ repositories/
โโโ utils/ # Catch-all โ the danger zone
โ NEVER: Shared Utils Catch-All
A utils/ or helpers/ folder that grows without domain context becomes a second dump for everything that didn't fit elsewhere.
export function formatDate(d: Date) { ... }
export function hashPassword(p: string) { ... }
export function calculateOrderTotal(items: Item[]) { ... }
Decision Tree
New project?
โ Start with domain-first: one folder per business capability
Existing project with technical layers at root?
โ Refactor incrementally: move one feature at a time
โ Don't big-bang rewrite: create new feature folders alongside old layers
Large domain with 20+ features?
โ Group by subdomain: orders/, catalog/, identity/, fulfillment/
โ Each subdomain is its own mini screaming architecture
Monorepo?
โ packages/orders/, packages/catalog/, packages/identity/
โ Each package is self-contained; cross-package deps are explicit
Unclear whether two concepts belong in the same module?
โ Ask: do they change together for the same business reason?
โ YES โ same module
โ NO โ separate modules
Something doesn't fit anywhere?
โ Try harder to find the right domain home before adding to shared/
โ Shared/ is for infrastructure, not domain concepts
Example
Before: a Node.js API organized by technical layer.
src/
โโโ controllers/
โ โโโ catalogController.ts
โ โโโ cartController.ts
โ โโโ orderController.ts
โโโ models/
โ โโโ product.ts
โ โโโ cart.ts
โ โโโ order.ts
โโโ services/
โ โโโ catalogService.ts
โ โโโ cartService.ts
โ โโโ orderService.ts
โโโ routes/
โโโ index.ts
After: same system, screaming "e-commerce".
src/
โโโ catalog/
โ โโโ product.entity.ts
โ โโโ product.service.ts
โ โโโ product.routes.ts
โ โโโ product.test.ts
โโโ cart/
โ โโโ cart.aggregate.ts
โ โโโ cart.service.ts
โ โโโ cart.routes.ts
โ โโโ cart.test.ts
โโโ orders/
โ โโโ order.aggregate.ts
โ โโโ order.repository.ts
โ โโโ order.service.ts
โ โโโ order.routes.ts
โ โโโ order.test.ts
โโโ shared/
โโโ database/
โโโ errors/
A new developer opens the project and immediately understands: "This is an e-commerce system with a catalog, cart, and order management."
Edge Cases
Framework conventions conflict with screaming architecture: NestJS defaults to module-per-feature which aligns well. Express and Fastify have no convention โ apply screaming architecture explicitly. Next.js app/ dir routes by URL path โ keep domain logic in src/domain/ or src/features/ separate from the routing layer.
Shared code that's actually domain logic: If two features share a concept (e.g., Money used by both orders/ and payments/), it belongs in a shared domain kernel โ not utils/. Name it shared/domain/ or kernel/.
Growing beyond 15-20 feature folders: Group by subdomain. An e-commerce system becomes catalog/, fulfillment/, identity/, payments/ โ each containing their own feature sub-folders.
Resources