Docs
/
Angular
Chapter 17
17 — Architecture Patterns for Enterprise Angular
Folder Structure (Scalable)
src/app/
├── core/ # Singleton services, guards, interceptors
│ ├── auth/
│ │ ├── auth.service.ts
│ │ ├── auth.guard.ts
│ │ └── auth.interceptor.ts
│ ├── http/
│ │ ├── api.service.ts
│ │ └── error.interceptor.ts
│ └── layout/
│ ├── header.component.ts
│ ├── sidebar.component.ts
│ └── layout.component.ts
│
├── shared/ # Reusable, stateless components/pipes/directives
│ ├── components/
│ │ ├── button/
│ │ ├── modal/
│ │ ├── data-table/
│ │ └── form-error/
│ ├── directives/
│ ├── pipes/
│ └── models/ # Shared interfaces/types
│ └── api-response.model.ts
│
├── features/ # Feature modules (lazy-loaded)
│ ├── dashboard/
│ │ ├── dashboard.component.ts
│ │ ├── dashboard.component.html
│ │ ├── dashboard.routes.ts
│ │ ├── services/
│ │ │ └── dashboard.service.ts
│ │ ├── store/
│ │ │ └── dashboard.store.ts
│ │ └── components/
│ │ ├── stats-card.component.ts
│ │ └── activity-feed.component.ts
│ ├── users/
│ │ ├── user-list.component.ts
│ │ ├── user-detail.component.ts
│ │ ├── user-form.component.ts
│ │ ├── users.routes.ts
│ │ ├── services/
│ │ │ └── user.service.ts
│ │ ├── store/
│ │ │ └── user.store.ts
│ │ └── models/
│ │ └── user.model.ts
│ └── admin/
│
├── app.component.ts
├── app.config.ts
└── app.routes.ts
Rules
| Rule | Description |
|---|---|
core/ | Instantiated once. Services with providedIn: 'root'. Guards, interceptors, layout. |
shared/ | Stateless, reusable. Imported by features. No feature-specific logic. |
features/ | Self-contained. Each feature has its own routes, services, store, models. |
| Feature isolation | Features should not import from other features. Shared logic goes in shared/ or core/. |
| Lazy loading | Every feature folder has a *.routes.ts — lazy-loaded from app.routes.ts. |
Smart vs Dumb Components
| Smart (Container) | Dumb (Presentational) | |
|---|---|---|
| Knows about | Services, store, router | Only @Input / @Output |
| Fetches data | Yes | Never |
| Has logic | Business logic | Display logic only |
| Reusable | No (feature-specific) | Yes |
| Testing | Needs mocks | Easy — just pass inputs |
| Location | Feature root | Feature components/ or shared/ |
// SMART — fetches data, dispatches actions
@Component({
template: `
@if (store.loading()) { <app-spinner /> }
<app-user-table
[users]="store.filteredUsers()"
(edit)="onEdit($event)"
(delete)="onDelete($event)"
/>
`,
})
export class UserListComponent {
store = inject(UserStore);
private router = inject(Router);
onEdit(user: User) { this.router.navigate(['/users', user.id, 'edit']); }
onDelete(user: User) { this.store.deleteUser(user.id); }
}
// DUMB — receives data, emits events
@Component({
selector: 'app-user-table',
template: `
<table>
@for (user of users; track user.id) {
<tr>
<td>{{ user.name }}</td>
<td><button (click)="edit.emit(user)">Edit</button></td>
</tr>
}
</table>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserTableComponent {
@Input() users: User[] = [];
@Output() edit = new EventEmitter<User>();
@Output() delete = new EventEmitter<User>();
}
Nx Monorepo
For large teams with multiple Angular apps and shared libraries.
npx create-nx-workspace my-org --preset=angular-monorepo
cd my-org
# Generate apps
nx g @nx/angular:app admin-portal
nx g @nx/angular:app customer-portal
# Generate shared libraries
nx g @nx/angular:lib shared/ui
nx g @nx/angular:lib shared/data-access
nx g @nx/angular:lib shared/utils
nx g @nx/angular:lib features/auth
my-org/
├── apps/
│ ├── admin-portal/ # Admin Angular app
│ └── customer-portal/ # Customer Angular app
├── libs/
│ ├── shared/
│ │ ├── ui/ # Shared components (button, modal, table)
│ │ ├── data-access/ # Shared services, API client
│ │ └── utils/ # Pure utility functions
│ └── features/
│ ├── auth/ # Auth feature (used by both apps)
│ └── users/ # User management feature
├── nx.json
└── tsconfig.base.json
Dependency Rules (Enforce via Nx)
// nx.json or .eslintrc.json
{
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{ "sourceTag": "type:app", "onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:data-access", "type:util"] },
{ "sourceTag": "type:feature", "onlyDependOnLibsWithTags": ["type:ui", "type:data-access", "type:util"] },
{ "sourceTag": "type:ui", "onlyDependOnLibsWithTags": ["type:util"] },
{ "sourceTag": "type:util", "onlyDependOnLibsWithTags": [] }
]
}
]
}
Domain-Driven Design (DDD) with Angular
features/
├── orders/ # Bounded Context: Orders
│ ├── domain/
│ │ ├── order.model.ts # Domain entities
│ │ └── order.repository.ts # Abstract interface
│ ├── application/
│ │ └── create-order.use-case.ts # Business logic
│ ├── infrastructure/
│ │ └── order-api.service.ts # HTTP implementation
│ └── presentation/
│ ├── order-list.component.ts
│ └── order-form.component.ts
// Domain layer — no Angular dependencies
export interface Order {
id: string;
items: OrderItem[];
total: number;
status: OrderStatus;
}
export abstract class OrderRepository {
abstract getAll(): Observable<Order[]>;
abstract getById(id: string): Observable<Order>;
abstract create(data: CreateOrderDto): Observable<Order>;
}
// Infrastructure layer — Angular HTTP
@Injectable({ providedIn: 'root' })
export class OrderApiService extends OrderRepository {
private http = inject(HttpClient);
getAll() { return this.http.get<Order[]>('/api/orders'); }
getById(id: string) { return this.http.get<Order>(`/api/orders/${id}`); }
create(data: CreateOrderDto) { return this.http.post<Order>('/api/orders', data); }
}
// Provide
providers: [
{ provide: OrderRepository, useClass: OrderApiService },
]
Facade Pattern
A facade simplifies the API for consumers — hides store/service complexity.
@Injectable({ providedIn: 'root' })
export class UserFacade {
private store = inject(UserStore);
private router = inject(Router);
// Expose read-only state
readonly users = this.store.filteredUsers;
readonly loading = this.store.loading;
readonly selectedUser = this.store.selectedUser;
// Expose actions
loadUsers() { this.store.loadUsers(); }
selectUser(id: string) { this.store.selectUser(id); }
deleteUser(id: string) { this.store.deleteUser(id); }
navigateToEdit(id: string) {
this.router.navigate(['/users', id, 'edit']);
}
}
// Component — thin and clean
@Component({ ... })
export class UserListComponent {
facade = inject(UserFacade);
ngOnInit() { this.facade.loadUsers(); }
}
Key Takeaways
- Structure:
core/(singletons) +shared/(reusable) +features/(lazy-loaded, isolated) - Features should never import from other features — use
shared/orcore/ - Use smart/dumb component separation — containers fetch data, presentational components display it
- For large orgs: use Nx monorepo with enforced module boundaries
- Facade pattern simplifies component interaction with stores/services
- Every feature should be lazy-loaded with its own
*.routes.ts - Use
OnPushon ALL presentational components