All articlesArchitecture

Screaming Architecture & Colocation: Let Your Project Structure Tell the Story

Learn why you should organize your code by what your app actually does, not by technical roles.

Petar IvanovPetar Ivanov
β€’β€’5 min read
On this page

This post outlines the importance of applying Screaming Architecture and Collocation in both your back-end and front-end projects, and how this helps you and your teammates.


Open most Node.js projects, and you see this:

Text
src/
β”œβ”€β”€ controllers/
β”œβ”€β”€ services/
β”œβ”€β”€ models/
β”œβ”€β”€ middleware/
β”œβ”€β”€ utils/
└── routes/

What does this app do?

You see that it’s an Express app. But that’s all the structure tells you.

Is it an e-commerce platform? A healthcare system? A social network?

You’d have to open files and read code to find out.

That’s the problem.


What Is Screaming Architecture?

Uncle Bob coined the term:

Your project structure shouldscreamwhat the application does, not what framework it uses.

For example, a healthcare app should look like this:

Text
src/
β”œβ”€β”€ patients/
β”œβ”€β”€ appointments/
β”œβ”€β”€ prescriptions/
β”œβ”€β”€ billing/
└── shared/

You open the folder and immediately know: this is a healthcare system.

The domain is front and center.

Express, REST, GraphQL, Prisma, React, those are implementation details, tucked inside each feature.

And this is not just about aesthetics.

When folders map to business capabilities, you get:

  • Discoverability: New developers find code faster.
  • Ownership: Teams own features, not layers.
  • Change isolation: Modifying β€œappointments” doesn’t require touching 5 different layer folders.
  • Deletion: You can remove an entire feature by deleting one folder.
  • Low/High Coupling: Low coupling between features and High coupling for a single feature.

Or we could also look at it in another way to see more clearly how a single feature maps across different technical layers:


The Colocation Principle

In the React world, we must strive to:

Keep things as close as possible to where they're used.

Consider the following hierarchy:

  1. If only one component uses it β†’ put it in that component’s file
  2. If only components in one folder use it β†’ put it in that folder
  3. If multiple folders use it β†’ move it up to the nearest shared parent
  4. If the whole app uses it β†’ put it in shared/ or lib/

This is the same idea as screaming architecture, applied at a granular level.

Don't prematurely extract.

Don't create utils/ and helpers/ folders that become junk drawers.


Combining Both: Feature-First Structure

Here’s what a feature-first Node.js project looks like when you combine screaming architecture with colocation:

Text
src/
β”œβ”€β”€ patients/
β”‚   β”œβ”€β”€ patient.controller.ts
β”‚   β”œβ”€β”€ patient.service.ts
β”‚   β”œβ”€β”€ patient.repository.ts
β”‚   β”œβ”€β”€ patient.types.ts
β”‚   β”œβ”€β”€ patient.validation.ts
β”‚   β”œβ”€β”€ patient.routes.ts
β”‚   └── __tests__/
β”‚       β”œβ”€β”€ patient.service.test.ts
β”‚       └── patient.controller.test.ts
β”œβ”€β”€ appointments/
β”‚   β”œβ”€β”€ appointment.controller.ts
β”‚   β”œβ”€β”€ appointment.service.ts
β”‚   β”œβ”€β”€ appointment.repository.ts
β”‚   β”œβ”€β”€ appointment.types.ts
β”‚   β”œβ”€β”€ appointment.validation.ts
β”‚   β”œβ”€β”€ appointment.routes.ts
β”‚   └── __tests__/
β”‚       └── appointment.service.test.ts
β”œβ”€β”€ shared/
β”‚   β”œβ”€β”€ middleware/
β”‚   β”‚   β”œβ”€β”€ auth.middleware.ts
β”‚   β”‚   └── error-handler.ts
β”‚   β”œβ”€β”€ database/
β”‚   β”‚   └── prisma.ts
β”‚   └── types/
β”‚       └── common.ts
β”œβ”€β”€ app.ts
└── server.ts

Every feature is self-contained.

Tests live next to the code they test.

Shared infrastructure sits in shared/, but only things that are genuinely shared across features.


The Same Principle in React

Text
src/
β”œβ”€β”€ features/
β”‚   β”œβ”€β”€ patients/
β”‚   β”‚   β”œβ”€β”€ PatientList.tsx
β”‚   β”‚   β”œβ”€β”€ PatientDetail.tsx
β”‚   β”‚   β”œβ”€β”€ usePatients.ts
β”‚   β”‚   β”œβ”€β”€ patient.types.ts
β”‚   β”‚   └── patient.api.ts
β”‚   β”œβ”€β”€ appointments/
β”‚   β”‚   β”œβ”€β”€ AppointmentCalendar.tsx
β”‚   β”‚   β”œβ”€β”€ BookAppointment.tsx
β”‚   β”‚   β”œβ”€β”€ useAppointments.ts
β”‚   β”‚   └── appointment.api.ts
β”œβ”€β”€ shared/
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ Button.tsx
β”‚   β”‚   └── Modal.tsx
β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   └── useAuth.ts
β”‚   └── lib/
β”‚       └── api-client.ts
β”œβ”€β”€ App.tsx
└── main.tsx

Same idea.

Features own their components, hooks, types, and API calls.

shared/ contains only genuinely reusable pieces.


When Layers Still Make Sense

Layers aren’t always wrong. They work when:

  • Your app is small (< 10 files). Feature folders are overhead for a simple CRUD API.
  • You’re building a library. Internal structure matters less than the public API.
  • Your team is one person. You already know where everything is.

The pragmatic hybrid: features at the top level, layers within each feature.

Each feature has its own controller, service, repository, but they’re colocated, not scattered.


Enforcing Boundaries

Structure without enforcement is just a suggestion.

Here's how to make it stick:

ESLint (with eslint-plugin-boundaries)

TypeScript
// .eslintrc.js
module.exports = {
  plugins: ['boundaries'],
  settings: {
    'boundaries/elements': [
      { type: 'patients', pattern: 'src/patients/*' },
      { type: 'appointments', pattern: 'src/appointments/*' },
      { type: 'shared', pattern: 'src/shared/*' },
    ],
  },
  rules: {
    'boundaries/element-types': [2, {
      default: 'disallow',
      rules: [
        { from: 'patients', allow: ['shared'] },
        { from: 'appointments', allow: ['shared', 'patients'] },
      ],
    }],
  },
};

TypeScript Project References

TypeScript
// src/shared/tsconfig.json β€” every referenced project needs composite: true
{
  "compilerOptions": {
    "composite": true,
    "rootDir": ".",
    "outDir": "../../dist/shared"
  }
}
TypeScript
// src/patients/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "rootDir": ".",
    "outDir": "../../dist/patients"
  },
  "references": [
    { "path": "../shared" }
  ]
}

Each feature is a TypeScript project.

It can only reference what's listed in references.

The compiler enforces boundaries at build time.


πŸ“Œ Key Takeaways

  1. Your folder structure is documentation. It should tell a new developer what the app does in 5 seconds.
  2. Organize by feature, not by layer. Controllers, services, and models for the same feature belong together.
  3. Colocate aggressively. Tests, types, and utilities live next to the code that uses them. Move things to shared/ only when two or more features need them.
  4. Migrate incrementally. One feature at a time, one PR at a time. No big-bang restructures.
  5. Enforce with tooling. ESLint boundaries, TypeScript project references, or Nx module boundaries. Structure without enforcement decays.

The best architecture is invisible. When a developer opens your project and immediately knows where to find what they need, without reading a README or asking a teammate, your structure is doing its job.

Related articles

Whenever you’re ready, here’s how I can help you:

  1. 1.

    The Conscious React: React architecture, design & clean code β€” 100+ production tips across 6 chapters, updated for React 19, plus 4 companion repos you can clone and run.

  2. 2.

    The Conscious Node: Node.js architecture, design & clean code β€” 157 production tips across 10 chapters, from module boundaries to the transactional outbox and zero-downtime deploys.

  3. 3.

    The JavaScript Architect Bundle: Both books + all React companion repos + CLAUDE.md rulesets + both playbooks. The complete path from developer to architect.

  4. 4.

    Free Resources: Architecture playbooks, cheat-sheets, and the JavaScript Architect Roadmap β€” practical guides for leveling up to senior.

The T-Shaped Dev

Join 30K+ engineers leveling up to architect

One practical tip on JavaScript, React, Node.js, and software architecture every week. No spam, unsubscribe anytime.

Petar Ivanov

Written by

Petar Ivanov

Software engineer, author, and speaker. I help JavaScript developers grow from Mid β†’ Senior β†’ Architect β€” production-grade React, Node.js, and AI systems.