Skip to main content

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

DirectoryRoleExamples
core/Singleton services and guards that exist only once app-wideAuthService, ApiInterceptor
shared/UI components and utilities reused across multiple featuresButtonComponent, DatePipe
features/Independent modules per business functionDashboardModule, 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

AspectBenefit
TestabilityDumb components need no service mocks, making tests straightforward
ReusabilityDumb components can be reused across any page
Separation of concernsLogic and display are decoupled, making code easier to read
PerformanceEasy 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.

LayerResponsibilityImplementation Examples
PresentationRender UI and receive user inputUserListComponent, LoginPageComponent
ApplicationOrchestrate use cases, manage state, implement FacadesUserFacade, AuthFacade
DomainBusiness rules, domain objects, interfacesUser, Order, IUserRepository
InfrastructureCommunicate with external systems, handle persistenceUserApiService, 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 TypeDescriptionManagement Approach
Local stateUsed only within a component (form input values, modal open/close)signal(), BehaviorSubject
Shared state (within a feature)Shared across multiple components in the same featureFeature-level service + Signals
Global stateShared app-wide (logged-in user, theme, etc.)NgRx Store / NgRx SignalStore
Server stateRemote data fetched from an APINgRx 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

AspectDetails
BenefitsTeams can deploy independently; technology stack (version) independence
ConsiderationsCareful version management of shared dependencies (e.g. @angular/core) is critical
Best suited forLarge 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 use feature-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.