The pattern I saw everywhere
Across multiple companies where I worked, I kept running into the same pattern. Teams knew that organizing by features was important, the intention was there. But in practice, the feature ended up scattered across the entire project: components/auth/, services/auth/, store/auth/, views/auth/. The feature name appeared in every technical folder, but never in a single place.
src/
├── components/
│ ├── auth/ # LoginButton.vue, RegisterForm.vue
│ ├── products/ # ProductCard.vue, ProductList.vue
│ └── orders/ # OrderSummary.vue
├── views/
│ ├── auth/ # LoginView.vue, RegisterView.vue
│ ├── products/ # ProductsView.vue
│ └── orders/ # OrdersView.vue
├── services/
│ ├── auth.service.ts
│ ├── products.service.ts
│ └── orders.service.ts
├── store/
│ ├── auth.store.ts
│ ├── products.store.ts
│ └── orders.store.ts
└── utils/
At first glance it looks organized. But when you need to change something in Auth, you touch 4 folders. When a new dev joins, they have to mentally reconstruct which files belong to which feature. And as the app grows, each technical folder becomes a junk drawer with 30+ files.
The problem isn't a lack of organization. It's that the organization screams "technology" instead of screaming "business."
The analogy that explains it all
Imagine a house blueprint. You don't see a "bricks" section, a "cement" section, and a "windows" section. You see "kitchen," "bedroom," "bathroom." The blueprints scream the purpose of each space, not the materials it's built with.
Screaming Architecture seeks the same for your code. Your top-level folder structure should scream the features of your application, not the technologies you use.
What you gain with this approach
- Instant understanding: you open the project and at a glance you know what the business does.
- Fast onboarding: new devs understand the project structure in minutes, not days.
- Localized changes: modifying a feature doesn't force you to touch 4 folders. Everything is in one place.
- Real scalability: adding a new feature means creating a folder, not inserting files in 6 different places.
- Easier testing: each module is an isolated unit you can test independently.
- Safe refactoring: moving or deleting an entire feature means moving or deleting a folder.
Applying Screaming Architecture
The idea is simple: group by features or business domains. Each folder contains everything needed for that functionality.
src/
├── modules/
│ ├── Auth/
│ │ ├── components/ # LoginButton.vue, RegisterForm.vue
│ │ ├── views/ # LoginView.vue, RegisterView.vue
│ │ ├── routes/ # Routes for this module
│ │ ├── store/ # auth.store.ts
│ │ └── services/ # auth.service.ts
│ ├── Products/
│ │ ├── components/ # ProductCard.vue, ProductList.vue
│ │ ├── views/ # ProductsView.vue, ProductDetailView.vue
│ │ ├── store/ # products.store.ts
│ │ └── services/ # products.service.ts
│ └── Orders/
│ ├── components/
│ ├── views/
│ ├── store/
│ └── services/
├── shared/
│ ├── ui/
│ │ ├── components/ # BaseButton.vue, ModalBase.vue
│ │ └── composables/ # useModal.ts, useDarkMode.ts
│ ├── composables/ # useDebounce.ts, useLocalStorage.ts
│ ├── utils/ # formatDate.ts, validateEmail.ts
│ └── assets/ # Global images, base styles
├── layouts/ # DefaultLayout.vue, AuthLayout.vue
├── router/ # General router
├── app.vue
└── main.ts
Not every module needs the same internal structure. Auth may have store/ and services/, but a Landing module might just be components/ and views/. Each module defines what it needs.
From theory to practice: migrating a feature
Let's take the Auth example from the disaggregated structure and see what the migration looks like:
Before (4 folders):
src/components/auth/LoginButton.vue
src/components/auth/RegisterForm.vue
src/views/auth/LoginView.vue
src/views/auth/RegisterView.vue
src/services/auth.service.ts
src/store/auth.store.ts
After (1 folder):
src/modules/Auth/components/LoginButton.vue
src/modules/Auth/components/RegisterForm.vue
src/modules/Auth/views/LoginView.vue
src/modules/Auth/views/RegisterView.vue
src/modules/Auth/services/auth.service.ts
src/modules/Auth/store/auth.store.ts
The imports change, the code doesn't. If your Auth module needs to expose something to the rest of the app (a guard, a composable, the user state), you can create an index.ts that serves as the module's public API:
// src/modules/Auth/index.ts
export { useAuthStore } from './store/auth.store'
export { useCurrentUser } from './composables/useCurrentUser'
export { authGuard } from './routes/guards'
The rest of the app imports from @/modules/Auth, not from internal folders. If you refactor the module's internal structure, external imports don't break.
Framework coexistence
If you use Nuxt, Next, or any framework with folder conventions (pages/, composables/, components/), the obvious question is: how does this coexist with Screaming Architecture?
Framework conventions handle routing and auto-imports. Screaming Architecture handles business logic.
In Nuxt, for example, pages/ defines routes, but the page can be a thin wrapper that imports the actual module:
<!-- pages/auth/login.vue -->
<template>
<LoginView />
</template>
<script setup lang="ts">
import LoginView from '~/modules/Auth/views/LoginView.vue'
</script>
Nuxt handles routing (/auth/login), and your Auth module handles the logic. pages/ stays thin, just an entry point.
The same applies to composables/. Global composables (auto-imported by Nuxt) go in the framework's conventional folder. Feature-specific composables go inside the module:
composables/ # Auto-imported by Nuxt (global)
├── useTheme.ts
└── useBreakpoint.ts
modules/Auth/composables/ # Auth-specific (manual import)
├── useCurrentUser.ts
└── useAuthRedirect.ts
Cross-cutting concerns: what goes in shared
The most common question when applying this: what happens when two modules need the same thing?
The rule is simple. If something is specific to a feature, it goes in its module. If two or more features use it, it goes in shared/.
shared/ui/ is for generic UI components with no business logic: buttons, modals, inputs. They're the building blocks, not the features.
shared/composables/ is for reusable logic without business state: useDebounce, useLocalStorage, useIntersectionObserver.
shared/utils/ is for pure functions: formatDate, slugify, validateEmail.
What about shared state? If Auth and Orders both need user data, Auth owns the state. Orders imports useCurrentUser from @/modules/Auth. If over time more modules need the same data, you can move that composable to shared/. But don't do it preemptively. Start with the owning module and only move when there's real evidence it's cross-cutting.
When to use it and when not to
This approach works best for medium to large projects with real business logic and multi-dev teams. If your app has 5+ features that grow independently, Screaming Architecture will bring order to your life.
For MVPs, landing pages, or simple CRUDs, it might be overkill. You don't need a module structure for 3 views and a form.
If your project has the potential to grow, adopting it early saves you the painful migration later. And if you already have a large project with a generic structure, you can migrate progressively, one module at a time, starting with the feature that grows the most.
Next time you start a project, think about what the structure will look like when the team grows. If your folders only say components/ and utils/, you know where to start.
I wrote that thinking about human teams. Today the newcomer who opens your repo is increasingly an AI agent, one that reads the structure just like a new dev but without the intuition to fill in the gaps. Taking this same idea all the way, so the repo not only screams its intent but also can't lie about itself, is what Context Architecture is about.