← Back to Tutorials Chapter 18

Design Patterns in Angular

Design patterns provide reusable solutions to common programming challenges. Angular's architecture naturally implements several classic patterns, and understanding them will help you write cleaner, more maintainable code.

Singleton Pattern

The singleton pattern ensures a class has only one instance. Angular's dependency injection system makes this the default behavior for services.

// A singleton service shared across the entire app
@Injectable({ providedIn: 'root' })
export class AppStateService {
  private state = signal<AppState>({ theme: 'light', sidebar: true });

  getState() { return this.state.asReadonly(); }
  updateState(partial: Partial<AppState>) {
    this.state.update(s => ({ ...s, ...partial }));
  }
}

// Both components share the same instance
@Component({ ... })
export class SidebarComponent {
  constructor(private state: AppStateService) {}
}

@Component({ ... })
export class ThemeToggleComponent {
  constructor(private state: AppStateService) {}
}

When a service is provided at the root level, Angular creates exactly one instance and injects it into every consumer.

Observer Pattern

RxJS is built on the observer pattern. A Subject (observable) maintains a list of subscribers (observers) and notifies them of state changes.

@Injectable({ providedIn: 'root' })
export class NotificationService {
  private notifications = new Subject<Notification>();
  notifications$ = this.notifications.asObservable();

  notify(message: string, type: 'success' | 'error' | 'info') {
    this.notifications.next({ message, type, timestamp: Date.now() });
  }
}

// Component A emits
this.notificationService.notify('Saved!', 'success');

// Component B listens
this.notificationService.notifications$.subscribe(n => {
  this.showToast(n.message, n.type);
});

Factory Pattern

The factory pattern creates objects without specifying the exact class. In Angular, use useFactory in providers or dynamic component creation for this purpose.

// Dynamic component factory
@Directive({ selector: '[adHost]' })
export class AdDirective {
  constructor(public viewContainerRef: ViewContainerRef) {}
}

export class AdService {
  getAds() {
    return [
      { component: HeroProfileComponent, data: { name: 'Angular' } },
      { component: HeroJobComponent, data: { title: 'Framework' } }
    ];
  }
}

// Load components dynamically
loadComponent(ad: AdItem) {
  const componentRef = this.adHost.viewContainerRef
    .createComponent(ad.component);
  componentRef.instance.data = ad.data;
}

Strategy Pattern

The strategy pattern defines a family of algorithms and makes them interchangeable. Use dependency injection to select strategies at runtime.

// Strategy interface
export interface ValidationStrategy {
  validate(value: any): { valid: boolean; error?: string };
}

// Concrete strategies
export class EmailStrategy implements ValidationStrategy {
  validate(value: string) {
    const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
    return { valid, error: valid ? undefined : 'Invalid email' };
  }
}

export class RequiredStrategy implements ValidationStrategy {
  validate(value: any) {
    return { valid: !!value, error: value ? undefined : 'Required' };
  }
}

// Use via DI
@Injectable()
export class ValidatorService {
  constructor(private strategies: ValidationStrategy[]) {}

  validate(value: any, type: string) {
    const strategy = this.strategies.find(s => s.constructor.name === type);
    return strategy?.validate(value);
  }
}

Dependency Injection Patterns

Angular's DI system supports a hierarchy of injectors. You can control service visibility at different levels:

@Component({
  selector: 'app-user-list',
  providers: [UserService] // New instance for this component tree
})
export class UserListComponent { }

Facade Pattern

A facade provides a simplified interface to a complex subsystem. In Angular, service facades wrap multiple services to expose a clean API.

@Injectable({ providedIn: 'root' })
export class DashboardFacade {
  constructor(
    private userService: UserService,
    private analyticsService: AnalyticsService,
    private notificationService: NotificationService
  ) {}

  // Simplified API
  loadDashboardData(): Observable<DashboardData> {
    return forkJoin({
      users: this.userService.getUsers(),
      stats: this.analyticsService.getStats()
    }).pipe(
      tap(() => this.notificationService.notify('Dashboard loaded', 'info'))
    );
  }
}
Use facades to reduce coupling between your components and the underlying service layer.
Angular design patterns
Practice Task: Implement a logger service using the strategy pattern. Create two strategies: ConsoleLogger and FileLogger. Use Angular's DI to inject the correct strategy based on the environment (development vs production).