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.
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.
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);
});
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;
}
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);
}
}
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 { }
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'))
);
}
}
ConsoleLogger and FileLogger. Use Angular's DI to inject the correct strategy based on the environment (development vs production).