Angular is a robust, component-based framework that encourages modularity, scalability, and maintainability. However, as applications grow, improper architecture can lead to spaghetti code, poor maintainability, and performance issues.
Following Angular architecture best practices ensures that applications remain scalable, testable, and easy to manage, especially in large teams or enterprise projects.
In this comprehensive guide, we will cover:
- Guidelines for creating modular and maintainable Angular applications
- When to create new feature modules vs using shared modules
- How to structure services, components, and modules for scalability
- Examples of practical folder structure and modular design
- Performance considerations and maintainable code patterns
1. Modular Design in Angular
A modular design breaks an application into self-contained, reusable units, each responsible for a specific feature or functionality. Angular supports modular design using NgModules.
Benefits of Modular Design
- Reusability – Modules or components can be reused across multiple features or projects.
- Maintainability – Smaller, self-contained units are easier to maintain and debug.
- Scalability – New features can be added without disrupting existing modules.
- Team Collaboration – Different teams can work on different modules independently.
Example of a Modular Angular App
src/app/
├─ core/ # Core services and singleton providers
│ ├─ core.module.ts
│ └─ services/
├─ shared/ # Shared components, directives, pipes
│ ├─ shared.module.ts
│ └─ components/
├─ features/ # Feature modules
│ ├─ user/ # UserModule
│ ├─ product/ # ProductModule
│ └─ order/ # OrderModule
├─ app-routing.module.ts # Application routing
└─ app.module.ts # Root module
- CoreModule: Singleton services like authentication, logging
- SharedModule: Reusable UI components like buttons, modals
- Feature Modules: Domain-specific functionality
2. Guidelines for Creating Modular and Maintainable Applications
a) Keep Root Module Lean
The AppModule should only include:
- The root component (
AppComponent
) - Root-level services
- Global modules like
BrowserModule
andAppRoutingModule
Avoid cluttering AppModule with feature-specific components.
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule,
CoreModule,
SharedModule
],
bootstrap: [AppComponent]
})
export class AppModule {}
b) Use Feature Modules for Each Domain
Group related functionality in feature modules:
@NgModule({
declarations: [UserListComponent, UserDetailComponent],
imports: [CommonModule, UserRoutingModule]
})
export class UserModule {}
- Keeps related components, services, and routing together
- Makes lazy loading easier
c) Shared Module for Reusable Components
SharedModule contains components, directives, and pipes that are used across multiple modules:
@NgModule({
declarations: [ButtonComponent, HighlightDirective, FormatDatePipe],
imports: [CommonModule],
exports: [ButtonComponent, HighlightDirective, FormatDatePipe]
})
export class SharedModule {}
- Components declared here can be imported into feature modules
- Avoid declaring services here unless stateless
d) Core Module for Singleton Services
CoreModule contains services that should only have a single instance across the application:
@NgModule({
providers: [AuthService, LoggerService]
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error('CoreModule is already loaded. Import only in AppModule.');
}
}
}
- Prevents multiple instances of core services
- Ensures singleton services for authentication, logging, etc.
e) Use Lazy Loading for Feature Modules
Lazy loading improves performance by loading modules only when needed:
const routes: Routes = [
{ path: 'users', loadChildren: () => import('./features/user/user.module').then(m => m.UserModule) }
];
- Only loads UserModule when user navigates to
/users
- Reduces initial bundle size
3. When to Create New Feature Modules vs Using Shared Modules
a) Feature Module
Create a feature module when:
- Functionality is domain-specific
- Components/services are closely related
- You want to enable lazy loading
Example: UserModule containing UserListComponent
, UserDetailComponent
, UserService
.
b) Shared Module
Create a shared module when:
- Functionality is generic and reusable
- Components/services are used in multiple feature modules
- Avoids code duplication
Example: SharedModule containing ButtonComponent
, HighlightDirective
, DatePipe
.
c) Core Module
Create a core module when:
- Services should be singleton
- App-wide providers like authentication, logging, HTTP interceptors are needed
- Only imported in AppModule
4. Structuring Services for Scalability
a) Use Dependency Injection
Angular services provide reusable business logic and can be injected into components or other services:
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor(private http: HttpClient) {}
getUsers() {
return this.http.get('/api/users');
}
}
providedIn: 'root'
ensures singleton service- Keeps business logic separate from UI components
b) Organize Services by Feature
features/user/services/user.service.ts
features/product/services/product.service.ts
- Avoid placing all services in a single folder
- Keeps services closely aligned with features
c) Core Services for Application-Wide Logic
core/services/auth.service.ts
core/services/logger.service.ts
core/services/http-interceptor.service.ts
- Core services are imported only once in AppModule
- Provides singleton pattern for app-wide services
5. Structuring Components for Maintainability
a) Component Folder Structure
Organize components inside each feature module:
features/user/
├─ components/
│ ├─ user-list/
│ │ ├─ user-list.component.ts
│ │ ├─ user-list.component.html
│ │ └─ user-list.component.css
│ └─ user-detail/
│ ├─ user-detail.component.ts
│ └─ user-detail.component.html
- Each component has its own folder
- Contains TypeScript, template, and styles
b) Use Smart vs Presentational Components
- Smart components: Handle data fetching and state management
- Presentational components: Display data and raise events
Example:
// Smart component
export class UserContainerComponent {
users$ = this.userService.getUsers();
}
// Presentational component
export class UserListComponent {
@Input() users: any[];
@Output() select = new EventEmitter<number>();
}
- Separates concerns and improves testability
6. Structuring Modules for Scalability
a) Feature Module Example
@NgModule({
declarations: [
UserListComponent,
UserDetailComponent,
UserContainerComponent
],
imports: [
CommonModule,
SharedModule,
UserRoutingModule
]
})
export class UserModule {}
- Imports
SharedModule
for reusable components - Contains its own routing module
b) Root Module Example
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule,
CoreModule,
SharedModule
],
bootstrap: [AppComponent]
})
export class AppModule {}
- Minimal components
- Imports core, shared, and feature modules as needed
7. Routing Best Practices
- Use feature-specific routing modules with
RouterModule.forChild()
- Keep root routing minimal
- Lazy load feature modules wherever possible
const routes: Routes = [
{ path: 'users', loadChildren: () => import('./features/user/user.module').then(m => m.UserModule) },
{ path: 'products', loadChildren: () => import('./features/product/product.module').then(m => m.ProductModule) }
];
8. Testing and Maintainability
- Write unit tests for services and components
- Feature modules make it easier to test modules independently
- Shared modules can be tested for reusable components
Example:
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
});
it('should fetch users', () => {
expect(service.getUsers()).toBeTruthy();
});
});
9. Performance Considerations
- Lazy load modules to reduce initial bundle size
- Use OnPush change detection for presentational components
- Avoid unnecessary module imports in feature modules
- Keep shared modules lightweight
Leave a Reply