Introduction
The organization of files and folders may seem like a secondary aspect when developing Angular applications, but it's actually an architectural decision that has a profound impact on maintainability, scalability, and understanding of the project. In this article, we'll explore different approaches to structuring Angular projects, from the simplest to the most business domain-oriented.
When we start a project with Angular CLI, we're provided with a default structure. However, as the application grows, this initial structure may prove insufficient. Should we follow the framework conventions or adapt the structure to better reflect our business domain?
The Difference Between Scaffolding and Folder Structure
Before diving deeper, it's important to distinguish between two related but different concepts:
Scaffolding refers to the automatic process of generating code and initial structure using tools like Angular CLI. Commands like ng new
or ng generate component
automatically create files with boilerplate code and register them in the corresponding modules.
Folder structure is the final organization of files and directories in the project, which can be the result of the initial scaffolding, but also of conscious decisions about how to organize the code according to the specific needs of the project.
Scaffolding is a process that occurs at specific moments during development, while folder structure evolves continuously and is more under the control of the development team.
Three Levels of Structure in Angular Projects
We can identify three main approaches to organizing our Angular projects, each with different levels of complexity and benefits:
Level 1: Structure by File Type
This is the most basic structure, generally created by default with Angular CLI:
└── src/
├── app/
│ ├── components/
│ ├── services/
│ ├── models/
│ ├── pipes/
│ ├── directives/
│ └── guards/
├── assets/
├── environments/
└── ...
Advantages:
- Familiar to Angular developers
- Follows framework conventions
- Quick to implement
Disadvantages:
- Does not communicate the business purpose
- Becomes difficult to maintain as it grows
- Related modules are scattered
Ideal use cases:
- Prototypes (POC - Proof of Concept)
- Minimum Viable Products (MVP)
- Short-term projects
- Very small teams (1-2 developers)
This structure is perfect when we need to quickly validate an idea or concept, without worrying too much about long-term scalability.
Level 2: Structure by Features
This approach groups code first by application features:
└── src/
├── app/
│ ├── core/ # Singleton services, global guards, etc.
│ │ ├── services/
│ │ ├── guards/
│ │ └── interceptors/
│ ├── shared/ # Shared components, pipes, directives
│ │ ├── components/
│ │ ├── directives/
│ │ └── pipes/
│ ├── features/ # Application features
│ │ ├── auth/
│ │ │ ├── components/
│ │ │ ├── services/
│ │ │ └── auth.module.ts
│ │ ├── payment/
│ │ │ ├── components/
│ │ │ ├── services/
│ │ │ └── payment.module.ts
│ │ └── employees/
│ │ ├── components/
│ │ ├── services/
│ │ └── employees.module.ts
│ └── app.module.ts
├── assets/
├── environments/
└── ...
Advantages:
- Organizes code by application features
- Uses Angular modules to encapsulate functionalities
- Facilitates parallel work between teams
- Keeps shared components separate
Disadvantages:
- Still maintains some technical orientation instead of domain focus
- It can be difficult to identify boundaries between features
Ideal use cases:
- Medium-sized projects (3-10 developers)
- Applications with expectation of moderate growth
- Teams with staff rotation
This structure is probably the most widely used in medium-sized Angular projects, as it offers a good balance between organization and ease of understanding.
Level 3: Structure with Screaming Architecture
This approach is inspired by the concept of "Screaming Architecture" proposed by Robert C. Martin, where the project structure should "scream" its business purpose:
└── src/
├── app/
│ ├── core/ # Application infrastructure
│ │ ├── infrastructure/ # Technical implementations
│ │ │ ├── http/
│ │ │ ├── storage/
│ │ │ └── auth/
│ │ └── ui/ # Base UI components
│ │ ├── layout/
│ │ └── components/
│ ├── domains/ # Business domains (modules)
│ │ ├── hr-management/ # Domain: HR Management
│ │ │ ├── employee-directory/ # Use case
│ │ │ │ ├── components/
│ │ │ │ ├── services/
│ │ │ │ └── models/
│ │ │ ├── payroll/ # Use case
│ │ │ └── hr-management.module.ts
│ │ ├── sales/ # Domain: Sales
│ │ │ ├── product-catalog/ # Use case
│ │ │ ├── checkout/ # Use case
│ │ │ └── sales.module.ts
│ │ └── customer-support/ # Domain: Customer Support
│ │ ├── ticket-management/ # Use case
│ │ ├── knowledge-base/ # Use case
│ │ └── customer-support.module.ts
│ ├── shared/ # Components shared between domains
│ └── app.module.ts
├── assets/
├── environments/
└── ...
Advantages:
- The structure clearly communicates the business purpose
- Organizes code by domains and use cases
- Facilitates business understanding for new developers
- Each domain can evolve independently
- Promotes separation of concerns
Disadvantages:
- Greater initial complexity
- Requires good knowledge of the business domain
- May be excessive for small applications
Ideal use cases:
- Large and complex projects
- Numerous teams (10+ developers)
- Long-life applications
- Systems with well-defined business domains
What is Screaming Architecture?
The concept of "Screaming Architecture" was proposed by Robert C. Martin (Uncle Bob) in 2011. The central idea is that a project's architecture should clearly communicate its purpose and business domain, not the technology or framework used.
The analogy he offers is clear: when looking at an architect's plans, you immediately identify whether it's a house, an airport, or a shopping mall, without needing to see what materials will be used. Similarly, when opening a software project, we should be able to immediately identify what it does (a podcast application, a ticket sales system, etc.), not just what technology it uses (Angular, React, etc.).
In the context of Angular, this means:
- The framework is just a tool, not the foundation of our architecture
- The structure should reflect domain concepts, not framework conventions
- Critical decisions should be based on business needs, not framework features
Practical Implementation in Angular
Let's see how we can implement these structures in Angular, focusing especially on the Screaming Architecture approach:
1. Using Modules for Domains
Angular has a module system that perfectly fits the concept of domains:
// domains/hr-management/hr-management.module.ts
@NgModule({
declarations: [...],
imports: [
CommonModule,
SharedModule,
RouterModule.forChild([
{ path: 'employees', component: EmployeeListComponent },
{ path: 'payroll', component: PayrollComponent }
])
],
providers: [EmployeeService, PayrollService]
})
export class HrManagementModule { }
2. State by Domain (with NGRX/NGXS)
If you use state management, each domain can have its own state:
└── domains/
└── hr-management/
├── store/
│ ├── actions/
│ ├── reducers/
│ ├── effects/
│ └── selectors/
└── ...
3. Barrel Pattern
Facilitates imports using the barrel pattern:
// domains/hr-management/index.ts
export * from './models';
export * from './services';
export * from './components';
4. Lazy Loading
Take advantage of Angular's lazy loading to load domains only when needed:
// In app-routing.module.ts
const routes: Routes = [
{
path: 'hr',
loadChildren: () => import('./domains/hr-management/hr-management.module')
.then(m => m.HrManagementModule)
},
{
path: 'sales',
loadChildren: () => import('./domains/sales/sales.module')
.then(m => m.SalesModule)
}
];
Transformation Example
To better illustrate the concept, let's see how we could transform a traditional structure into one based on Screaming Architecture:
Before (Type-based approach)
app/
├── components/
│ ├── product-list.component.ts
│ ├── cart.component.ts
│ └── checkout-form.component.ts
├── services/
│ ├── product.service.ts
│ └── cart.service.ts
└── models/
├── product.model.ts
└── cart-item.model.ts
After (Screaming Architecture approach)
app/
└── domains/
├── catalog/
│ ├── components/
│ │ └── product-list.component.ts
│ ├── services/
│ │ └── product.service.ts
│ └── models/
│ └── product.model.ts
└── shopping/
├── components/
│ ├── cart.component.ts
│ └── checkout-form.component.ts
├── services/
│ └── cart.service.ts
└── models/
└── cart-item.model.ts
Practical Considerations
When to use each approach?
There is no universal "correct" structure. The choice depends on several factors:
For small projects (1-3 developers):
- Level 1 is ideal for POCs and MVPs where the priority is to quickly validate concepts
- Level 2 is suitable if moderate growth is expected
For medium projects (4-10 developers):
- Level 2 is usually the most balanced
- Consider Level 3 if the domain is complex and well-defined
- Implement lazy loading for modules/features
- Evaluate using NgRx/NGXS with state separated by domain
For large projects (10+ developers):
- Level 3 with Screaming Architecture offers the best long-term benefits
- Consider dividing into multiple applications or microfrontends
- Use shared libraries for common code between domains
Gradual Migration
It's not necessary to do a complete refactoring immediately. You can adopt a gradual approach:
- Identify the main domains of your application
- Start reorganizing one domain at a time
- Use the barrel pattern to minimize changes in imports
- Implement lazy loading to reduce performance impact during migration
To Summarize
At the end of the day, what makes a folder structure truly "good" isn't about following trendy patterns or rigid rules—it's about what works for your team in the real world. The best structure is one that makes your developers smile when they navigate the codebase, not curse under their breath.
Think of your folder structure as a living, breathing part of your application. It should grow and evolve alongside your project, adapting to new challenges and insights your team gains along the way. Some of the most effective structures I've seen came from teams who weren't afraid to say, "This isn't working for us anymore," and make thoughtful changes.