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

RuleDescription
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 isolationFeatures should not import from other features. Shared logic goes in shared/ or core/.
Lazy loadingEvery feature folder has a *.routes.ts — lazy-loaded from app.routes.ts.

Smart vs Dumb Components

Smart (Container)Dumb (Presentational)
Knows aboutServices, store, routerOnly @Input / @Output
Fetches dataYesNever
Has logicBusiness logicDisplay logic only
ReusableNo (feature-specific)Yes
TestingNeeds mocksEasy — just pass inputs
LocationFeature rootFeature 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/ or core/
  • 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 OnPush on ALL presentational components