Mark Lowel Montealto — Full Stack Developer & DevOps Engineer

Mark Lowel
Montealto

Full Stack Developer & DevOps Engineer

Blog

March 2025

·

2 min read

Scalable Angular Architecture: Standalone Components & Lazy Loading

Architect large Angular apps with standalone components, feature-based folder structure, and route-level lazy loading for faster bundles.

Introduction

  • The problem with NgModule-heavy apps: circular imports, barrel files gone wrong, hard-to-split bundles.
  • Standalone components (stable since Angular 15) remove the NgModule requirement — what that means for architecture.
  • Goal: a feature-based folder structure where every feature can be independently lazy-loaded, tested, and replaced.

Why Go Module-Free with Standalone Components

  • @Component({ standalone: true }) — the component declares its own imports directly.
  • No more declarations, exports, and entryComponents in NgModules.
  • Faster bootstrapping: bootstrapApplication(AppComponent, appConfig) replaces AppModule.
  • IDE and tree-shaking benefits: unused components are clearly orphaned.
// Before (NgModule world)
@NgModule({ declarations: [HeroListComponent], imports: [CommonModule] })
export class HeroModule {}
 
// After (standalone)
@Component({
  selector: 'app-hero-list',
  standalone: true,
  imports: [CommonModule, RouterModule],
  templateUrl: './hero-list.component.html',
})
export class HeroListComponent {}

Feature-Based Folder Structure

  • Recommended structure: src/app/features/<feature-name>/ containing components, services, routes, and types local to that feature.
  • Shared code: src/app/shared/ for cross-feature UI components and pipes; src/app/core/ for singletons (auth, HTTP interceptors, error handling).
  • Keep feature services providedIn: 'root' or provide them at route level for lazy-loaded scope.
src/app/
  core/             ← singletons (auth, logger)
  shared/           ← reusable UI components
  features/
    dashboard/
      dashboard.routes.ts
      dashboard.component.ts
      widgets/
    users/
      users.routes.ts
      user-list.component.ts
      user-detail.component.ts
      user.service.ts

Route-Level Lazy Loading Without NgModules

  • Use loadComponent for individual lazy-loaded pages and loadChildren for a route config file.
  • Each feature exports a Routes array — no NgModule required.
// app.routes.ts
export const APP_ROUTES: Routes = [
  { path: 'dashboard', loadComponent: () =>
      import('./features/dashboard/dashboard.component').then(m => m.DashboardComponent) },
  { path: 'users', loadChildren: () =>
      import('./features/users/users.routes').then(m => m.USERS_ROUTES) },
];
  • Result: Angular generates separate JS chunks per route automatically.
  • Verify chunk sizes with ng build --stats-json + webpack-bundle-analyzer.

Providing Services at Route Level (Scoped DI)

  • Services provided at a route's providers array are created per lazy-loaded route tree and destroyed when the user navigates away.
  • Use case: feature-specific state that should not bleed between features.
// users.routes.ts
export const USERS_ROUTES: Routes = [
  {
    path: '',
    providers: [UserService, UserStore],
    children: [
      { path: '', component: UserListComponent },
      { path: ':id', component: UserDetailComponent },
    ],
  },
];

Shared UI Library Strategy

  • Extract truly shared components to a local library using ng generate library or a monorepo (Nx).
  • Barrel export pattern: shared/index.ts re-exports all public components and directives.
  • Avoid over-abstracting: only extract components used in 3+ features.

Dependency Injection Best Practices at Scale

  • Prefer inject() function over constructor injection in standalone components for better tree-shakability.
  • Use InjectionToken for configuration objects instead of @Injectable services.
  • HttpClient is tree-shakable when provided with provideHttpClient(withInterceptors([...])).

Testing Standalone Components

  • TestBed.configureTestingModule({ imports: [MyStandaloneComponent] }) — no declarations.
  • Override imports at test level to swap real HTTP for provideHttpClientTesting().
  • Faster unit tests: standalone components with no global module overhead.

Performance Checklist

  • ChangeDetectionStrategy.OnPush on every component.
  • Signals for local state (see: Angular Signals post).
  • @defer blocks for non-critical UI (Angular 17+): defers rendering until interaction or viewport visibility.
  • trackBy / @for track in lists to avoid full DOM re-renders.
  • Preloading strategy: PreloadAllModules or custom QuicklinkStrategy.

Conclusion

  • Standalone components + feature-based routing is Angular's modern baseline — no new projects should use NgModules.
  • Lazy load at the route level from day one; retrofitting later is painful.
  • The folder structure mirrors the router: if it's a route, it's a feature folder.
  • Next steps: add Nx to scale to a monorepo, or explore @ngrx/store for cross-feature shared state.