The Evolution of Angular Modules
Angular's module system has undergone significant transformation. In the early days of Angular (versions 2 through 13), NgModules served as the fundamental building block of every application. Every component, service, and directive needed to belong to a module, and the root AppModule was the entry point that bootstrapped the entire application.
With Angular 14, the framework introduced standalone components, fundamentally changing how developers structure their applications. Standalone components eliminate the need for NgModules by allowing components to declare their dependencies directly.
The Angular team's current guidance is clear: for new applications, start with standalone components as your default approach. However, understanding when to use NgModules versus standalone components is a critical skill for modern Angular development. Our web development approach emphasizes choosing the right architectural patterns for each project's unique requirements.
Standalone Components vs NgModules
When to Use Standalone Components
Standalone components offer several compelling advantages that make them the preferred choice for most new development. First, they reduce boilerplate by eliminating the need to create and maintain separate module files for every feature. Second, they improve the framework's ability to tree-shake unused code, potentially reducing bundle sizes. Third, they make component dependencies explicit and self-contained.
The standalone approach works exceptionally well for single features, reusable UI components, and applications following a domain-driven structure where each domain is relatively self-contained. When a feature requires multiple components, services, and directives that work together, a standalone component can still serve as the entry point while importing other standalone building blocks as needed.
When NgModules Still Make Sense
Despite the rise of standalone components, NgModules continue to serve important purposes. Lazy-loaded feature modules remain a powerful pattern for improving initial load time. For applications with complex organizational needs, NgModules can provide clearer boundaries between features, helping communicate ownership and reduce the risk of unintended coupling.
1@Component({2 standalone: true,3 selector: 'app-user-card',4 templateUrl: './user-card.html',5 imports: [CommonModule, MatCardModule],6})7export class UserCardComponent {}1@NgModule({2 declarations: [UserCardComponent],3 imports: [CommonModule, MatCardModule],4 exports: [UserCardComponent],5})6export class UserCardModule {}Organizing by Feature and Domain
The most important organizational principle for Angular applications is structuring by feature rather than by file type. Instead of grouping all components together, all services together, and all models together, organize your code around business features or domains. This approach brings related code together and makes it easier to understand the complete picture of what a feature does.
A well-structured feature directory contains everything that feature needs: components, services, models, routes, and utilities. When you need to modify or debug a feature, you know exactly where to find everything related to it. The Angular CLI supports this approach through its schematics, which can generate feature modules with associated components, services, and routing configured appropriately. Following these web development best practices helps teams maintain scalable codebases as applications grow.
Example Feature Structure
src/
app/
features/
products/
components/
product-list/
product-detail/
services/
product.service.ts
models/
product.model.ts
users/
cart/
This structure keeps everything related to products in one place. When you need to modify how products are displayed, you navigate to the products feature. When you need to understand how product data is fetched, you find the service in the same location. For applications with shared code that multiple features need, create a separate shared directory with focused libraries rather than accumulating unrelated utilities.
Lazy Loading for Performance
Lazy loading is essential for maintaining good application performance as your codebase grows. Without lazy loading, users download your entire application upfront, even if they only use a small portion of it during their session. Lazy loading allows you to split your application into chunks that load only when needed.
Route-Based Lazy Loading
const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./features/products/products.routes')
.then(m => m.ProductsRoutes)
},
{
path: 'cart',
loadChildren: () => import('./features/cart/cart.routes')
.then(m => m.CartRoutes)
}
];
Preloading Strategies
Angular's router supports preloading, which loads lazy chunks in the background after the initial page renders, making subsequent navigation instant for preloaded routes. The default preloading strategy is reasonable for most applications, but you may want to customize it for specific use cases. Be cautious about eager imports that defeat lazy loading--audit your imports regularly to ensure that features you intend to lazy load remain separate.
Angular Signals for State Management
Angular Signals represent a paradigm shift in how Angular handles reactivity. Introduced in Angular 16, Signals provide a more intuitive and performant way to manage state compared to traditional RxJS Observables.
Signals work by creating reactive values that automatically update any code that depends on them. When a signal's value changes, Angular efficiently updates only the parts of the DOM that actually use that value, avoiding unnecessary change detection cycles.
Using Signals in Components
export class ProductListComponent {
products = signal<Product[]>([]);
filter = signal<string>('');
filteredProducts = computed(() =>
this.products().filter(p =>
p.name.toLowerCase().includes(this.filter().toLowerCase())
)
);
}
For global or shared state, Signals work well when placed in services that are injected where needed. Angular provides interoperability between Signals and RxJS--you can convert Observables to Signals using toSignal and vice versa using toObservable. This interoperability allows you to adopt Signals incrementally while maintaining existing RxJS-based state management. When building AI-powered applications, Signals provide an efficient way to manage the reactive state required for real-time AI interactions and data streams.
Fine-Grained Reactivity
Updates only the DOM elements that actually use a changed value
Simpler Mental Model
More intuitive than RxJS Observables for local state
Better Performance
Avoids unnecessary change detection cycles
RxJS Interop
Convert between Signals and Observables seamlessly
Library Boundaries and Shared Code
As applications grow, organizing shared code effectively is crucial for maintaining developer productivity and code quality. Create clear boundaries that prevent unintended coupling while making reusable functionality easily accessible.
Shared Library Organization
A well-organized shared structure includes distinct categories:
- UI Library - Reusable components used across features
- Data-Access Library - API clients and services
- Utilities Library - Helpers, pipes, validators
Avoiding Common Pitfalls
- God Library Anti-pattern: Avoid accumulating too many unrelated utilities in one library--prefer many small, focused libraries
- Circular Dependencies: Use Nx dependency graph visualization to identify and resolve circular dependencies
- Tight Coupling: Encourage features to depend only on public APIs, not internal implementation details
For applications using Nx or similar monorepo tools, shared libraries become even more powerful. Nx enforces library boundaries through its build system, preventing features from depending on internal implementation details of other features. These architectural patterns align with our web development services that emphasize maintainable, scalable application architecture.
Modern Tooling and Quality Assurance
Modern Angular development benefits from a rich ecosystem of tooling:
Testing
- Jest: Faster execution and simpler configuration than Karma and Jasmine
- Cypress: Excellent end-to-end testing with debugging tools
Code Quality
- ESLint & Prettier: Consistent style and issue detection
- Husky: Git hooks for quality gates at commit time
Component Development
- Storybook: Build and test UI components in isolation from application context
Nx Monorepo Benefits
- Enforces library boundaries through build system
- Dependency graph visualization
- Computation caching for faster builds across large codebases
Storybook serves dual purposes: it provides interactive examples for developers using components and serves as living documentation that stays current as components evolve.
Feature-First Organization
Structure by business domain, not file type
Default to Standalone
Use standalone components for new development
Lazy Load Routes
Split application into chunks that load on demand
Leverage Signals
Use Angular Signals for local state management
Clear Library Boundaries
Organize shared code into focused libraries
Automated Quality Gates
Use ESLint, Prettier, and testing tools
Frequently Asked Questions
Sources
- Angular.dev Style Guide - Official Angular style conventions and module best practices
- Angular.dev - Lazy Loading - Route-based code splitting documentation
- Angular.dev - Signals - Modern reactive state management documentation
- DEV Community - Modern Best Practices for Angular Applications - Community guide on architectural patterns
- Nx Documentation - Monorepo tooling for Angular applications