Architecture Patterns for Large-Scale Development
The primary reason Angular is suited for large-scale development is that the framework itself promotes strict architectural patterns. This document covers enterprise-level Angular architecture patterns.
1. Feature-based Directory Structure
The recommended approach for large-scale Angular applications is to split directories by feature (business function).
src/
├── app/
│ ├── core/ # Singleton services, guards, interceptors
│ │ ├── guards/
│ │ │ └── auth.guard.ts
│ │ ├── interceptors/
│ │ │ └── auth.interceptor.ts
│ │ └── services/
│ │ └── auth.service.ts
│ ├── shared/ # Shared components, directives, pipes
│ │ ├── components/
│ │ │ ├── button/
│ │ │ └── modal/
│ │ ├── directives/
│ │ └── pipes/
│ ├── features/ # Feature modules
│ │ ├── dashboard/
│ │ │ ├── components/ # Presentational components
│ │ │ ├── pages/ # Smart components (pages)
│ │ │ ├── services/ # Feature-specific services
│ │ │ ├── models/ # Type definitions and interfaces
│ │ │ └── index.ts # Barrel export
│ │ └── user/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── services/
│ │ ├── models/
│ │ └── index.ts
│ └── app.routes.ts # Route definitions
Role of Each Directory
| Directory | Role | Examples |
|---|---|---|
core/ | Singleton services and guards that exist only once app-wide | AuthService, ApiInterceptor |
shared/ | UI components and utilities reused across multiple features | ButtonComponent, DatePipe |
features/ | Independent modules per business function | DashboardModule, UserModule |
Comparison with Next.js
In Next.js, the app/ directory equals the routing structure. Angular lets you decouple routing from directory structure, enabling business logic organization that is independent of page transitions.
2. Smart/Dumb Component Pattern
This design pattern classifies components as either "logic-holding" or "display-only."
Smart (Container) Components
Responsible for data fetching and state management. They depend on services and pass data down to child components via Inputs.
// features/user/pages/user-list.component.ts
@Component({
selector: 'app-user-list-page',
template: `
<app-user-list
[users]="users()"
[loading]="loading()"
(userSelected)="onUserSelected($event)"
/>
`,
})
export class UserListPageComponent {
private userFacade = inject(UserFacade);
users = this.userFacade.users; // Signal
loading = this.userFacade.isLoading; // Signal
onUserSelected(userId: string) {
this.userFacade.selectUser(userId);
}
}
Dumb (Presentational) Components
Pure display components that operate only via @Input / @Output. They have no dependency on services.
// features/user/components/user-list.component.ts
@Component({
selector: 'app-user-list',
template: `
@if (loading()) {
<app-spinner />
} @else {
<ul>
@for (user of users(); track user.id) {
<li (click)="userSelected.emit(user.id)">{{ user.name }}</li>
}
</ul>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserListComponent {
users = input.required<User[]>();
loading = input<boolean>(false);
userSelected = output<string>();
}
Benefits of Separation
| Aspect | Benefit |
|---|---|
| Testability | Dumb components need no service mocks, making tests straightforward |
| Reusability | Dumb components can be reused across any page |
| Separation of concerns | Logic and display are decoupled, making code easier to read |
| Performance | Easy to apply the OnPush strategy, minimizing change detection |
3. Facade Pattern
The Facade pattern introduces an intermediate layer between components and complex logic (Store, API, business rules). It is especially effective when leveraging Angular's powerful DI system.
// features/user/services/user.facade.ts
@Injectable({ providedIn: 'root' })
export class UserFacade {
private store = inject(Store);
private apiService = inject(UserApiService);
// Signals exposed to components
users = toSignal(this.store.select(selectAllUsers), { initialValue: [] });
isLoading = toSignal(this.store.select(selectUsersLoading), { initialValue: false });
selectedUser = toSignal(this.store.select(selectSelectedUser));
loadUsers() {
this.store.dispatch(UserActions.loadUsers());
}
selectUser(userId: string) {
this.store.dispatch(UserActions.selectUser({ userId }));
}
updateUser(user: User) {
this.store.dispatch(UserActions.updateUser({ user }));
}
}
Components depend only on the Facade and don't need to know the implementation details of the Store. If you later migrate the Store to a Signal-based approach, no component changes are required.
4. Layered Architecture
An Angular application can be designed in a 4-layer structure, similar to server-side Clean Architecture.
| Layer | Responsibility | Implementation Examples |
|---|---|---|
| Presentation | Render UI and receive user input | UserListComponent, LoginPageComponent |
| Application | Orchestrate use cases, manage state, implement Facades | UserFacade, AuthFacade |
| Domain | Business rules, domain objects, interfaces | User, Order, IUserRepository |
| Infrastructure | Communicate with external systems, handle persistence | UserApiService, LocalStorageService |
Dependency Direction Rules
- Dependencies flow inward only (Presentation → Domain direction)
- The Domain layer does not depend on frameworks like Angular or HTTP
- The Infrastructure layer implements Domain interfaces but is depended upon by the Domain
5. Barrel Exports (index.ts)
Place an index.ts in each feature directory to centrally manage its public API.
// features/user/index.ts
export { UserListPageComponent } from './pages/user-list.component';
export { UserFacade } from './services/user.facade';
export type { User, UserRole } from './models/user.model';
// Internal implementation (e.g. UserApiService) is NOT exported
The importing side no longer needs to know the internal paths.
// Before: need to know the internal path
import { UserFacade } from '../features/user/services/user.facade';
import type { User } from '../features/user/models/user.model';
// After: reference only the feature's public API
import { UserFacade } from '../features/user';
import type { User } from '../features/user';
Combined with path aliases in tsconfig.json, imports become even more concise.
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@features/user": ["src/app/features/user/index.ts"],
"@features/dashboard": ["src/app/features/dashboard/index.ts"],
"@shared/*": ["src/app/shared/*"]
}
}
}
6. State Management Design Guidelines
Classify an Angular application's state into four categories and choose the appropriate management strategy for each.
| State Type | Description | Management Approach |
|---|---|---|
| Local state | Used only within a component (form input values, modal open/close) | signal(), BehaviorSubject |
| Shared state (within a feature) | Shared across multiple components in the same feature | Feature-level service + Signals |
| Global state | Shared app-wide (logged-in user, theme, etc.) | NgRx Store / NgRx SignalStore |
| Server state | Remote data fetched from an API | NgRx Effects + Cache, or RxJS |
// Local state (within a component)
export class CounterComponent {
count = signal(0);
increment() { this.count.update(v => v + 1); }
}
// Feature state (shared via service)
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
readonly items$ = this.items.asReadonly();
readonly total = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
}
// Global state (NgRx SignalStore)
export const UserStore = signalStore(
{ providedIn: 'root' },
withState({ currentUser: null as User | null, isLoggedIn: false }),
withMethods((store, authService = inject(AuthService)) => ({
async login(credentials: Credentials) {
const user = await authService.login(credentials);
patchState(store, { currentUser: user, isLoggedIn: true });
},
}))
);
Comparison with React
React offers many choices — useState, Context, Redux, TanStack Query — and teams often end up with inconsistent structures. Angular's unified DI and service mechanism makes it easier to align on state management strategies from the start.
7. Micro Frontends
Using Module Federation (Webpack 5), you can split an Angular application into multiple independently deployable micro frontends.
ng add @angular-architects/module-federation
Example Setup
shell-app/ # Host (shell) application
webpack.config.js
mfe-dashboard/ # Remote app (dashboard team)
webpack.config.js
mfe-orders/ # Remote app (order management team)
webpack.config.js
// shell-app/src/app/app.routes.ts
export const routes: Routes = [
{
path: 'dashboard',
loadChildren: () =>
loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4201/remoteEntry.js',
exposedModule: './DashboardModule',
}).then(m => m.DashboardModule),
},
];
Benefits and Considerations
| Aspect | Details |
|---|---|
| Benefits | Teams can deploy independently; technology stack (version) independence |
| Considerations | Careful version management of shared dependencies (e.g. @angular/core) is critical |
| Best suited for | Large organizations where multiple teams develop and deploy independently |
8. Coding Conventions and Consistency
Angular Style Guide
Following the Angular Official Style Guide ensures code consistency across teams. Key rules:
- Naming conventions: Components use
feature-name.component.ts, services usefeature-name.service.ts - Single Responsibility Principle: One class per file, aim for under 400 lines
- File order: Decorators order (
@Input→@Output→ lifecycle hooks → public methods → private methods)
ESLint Configuration
ng add @angular-eslint/schematics
// .eslintrc.json (example Angular rules)
{
"rules": {
"@angular-eslint/component-selector": [
"error",
{ "type": "element", "prefix": "app", "style": "kebab-case" }
],
"@angular-eslint/directive-selector": [
"error",
{ "type": "attribute", "prefix": "app", "style": "camelCase" }
],
"@angular-eslint/no-empty-lifecycle-method": "error",
"@angular-eslint/use-lifecycle-interface": "error"
}
}
Integration with Prettier
npm install --save-dev prettier eslint-config-prettier
// .prettierrc
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
}
The Advantage of Angular's Opinionated Approach
Because Angular is opinionated, developers joining a team spend less time wondering "how should I write this?" In the React/Next.js ecosystem, teams must independently decide on state management, styling, and routing solutions for each project. With Angular, the baseline is established from day one.