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.