Ir al contenido

Design Patterns

Esta página aún no está disponible en tu idioma.

Design patterns are tried-and-tested solutions for common design issues in software development. Rather than offering complete solutions, they serve as best practices addressing typical software development challenges.

In Angular, these patterns facilitate cleaner, more organized code that improves buildability, scalability, and maintainability.


Design patterns decompose applications into small, reusable components, facilitating feature additions without disrupting existing functionality.

Organized code structure ensures clarity. Using patterns like Singleton means updating logic in one location automatically cascades changes across dependent components.

Separation of concerns enabled by design patterns simplifies mocking and testing procedures for individual components.

Patterns encourage writing reusable code. Factory and Singleton patterns consolidate similar tasks, eliminating redundant logic.


The Singleton pattern ensures that a class only has one instance and allows that instance to be shared globally.

Angular services typically leverage this through root-level provision using providedIn: 'root'. This guarantees only one instance exists throughout the application lifecycle.

@Injectable({
providedIn: 'root'
})
export class MyService {
constructor() { }
}

Real-World Example: Authentication Service

Section titled “Real-World Example: Authentication Service”
@Injectable({
providedIn: 'root'
})
export class AuthService {
private currentUser: User | null = null;
login(email: string, password: string): Observable<User> {
return this.http.post<User>('/api/login', { email, password })
.pipe(
tap(user => this.currentUser = user)
);
}
logout(): void {
this.currentUser = null;
}
getCurrentUser(): User | null {
return this.currentUser;
}
isAuthenticated(): boolean {
return this.currentUser !== null;
}
}

✅ Single instance across application
✅ Centralized state management
✅ Guaranteed consistency
✅ Easy to inject and test


The Facade Pattern provides a simplified interface to interact with complex subsystems. Rather than components directly communicating with multiple services (causing duplication), they interact with a facade service containing centralized business logic.

@Injectable({
providedIn: 'root',
})
export class CartFacade {
constructor(
private cartService: CartService,
private orderService: OrderService
) {}
addToCart(product: Product): void {
this.cartService.addItem(product);
}
removeFromCart(product: Product): void {
this.cartService.removeItem(product);
}
checkout(): void {
const cart = this.cartService.getCart();
this.orderService.processOrder(cart);
}
getCartItems(): Product[] {
return this.cartService.getCart();
}
getTotalPrice(): number {
return this.cartService.getTotalPrice();
}
}
@Component({
selector: 'app-shopping-cart',
template: `
<h2>Shopping Cart</h2>
@for (item of cartItems(); track item.id) {
<div>
<p>{{ item.name }} - ${{ item.price }}</p>
<button (click)="removeItem(item)">Remove</button>
</div>
}
<h3>Total: ${{ totalPrice() }}</h3>
<button (click)="checkout()">Checkout</button>
`
})
export class ShoppingCartComponent implements OnInit {
cartItems = signal<Product[]>([]);
totalPrice = signal<number>(0);
constructor(private cartFacade: CartFacade) {}
ngOnInit(): void {
this.cartItems.set(this.cartFacade.getCartItems());
this.totalPrice.set(this.cartFacade.getTotalPrice());
}
removeItem(product: Product): void {
this.cartFacade.removeFromCart(product);
this.cartItems.set(this.cartFacade.getCartItems());
this.totalPrice.set(this.cartFacade.getTotalPrice());
}
checkout(): void {
this.cartFacade.checkout();
}
}

✅ Simplifies complex operations
✅ Encapsulates business logic
✅ Reduces component complexity
✅ Single point of change for related functionality


The Factory Pattern creates objects without specifying exact classes, handling instantiation details. This enables dynamic object creation when runtime requirements aren’t predetermined.

Notification Interface:

export interface Notification {
send(message: string): void;
}

Concrete Implementations:

@Injectable({
providedIn: 'root',
})
export class EmailNotification implements Notification {
send(message: string): void {
console.log(`Email: ${message}`);
// Send via email service
}
}
@Injectable({
providedIn: 'root',
})
export class SmsNotification implements Notification {
send(message: string): void {
console.log(`SMS: ${message}`);
// Send via SMS service
}
}
@Injectable({
providedIn: 'root',
})
export class PushNotification implements Notification {
send(message: string): void {
console.log(`Push: ${message}`);
// Send via push notification service
}
}

Factory Service:

@Injectable({
providedIn: 'root',
})
export class NotificationFactory {
constructor(
private emailNotification: EmailNotification,
private smsNotification: SmsNotification,
private pushNotification: PushNotification
) {}
createNotification(type: 'email' | 'sms' | 'push'): Notification {
switch (type) {
case 'email':
return this.emailNotification;
case 'sms':
return this.smsNotification;
case 'push':
return this.pushNotification;
default:
throw new Error(`Unknown notification type: ${type}`);
}
}
}

Component Usage:

@Component({
selector: 'app-notification',
template: `
<h1>Send Notification</h1>
@for (type of notificationTypes; track type) {
<button (click)="send(type)">
Send {{ type | titlecase }}
</button>
}
`,
})
export class NotificationComponent {
notificationTypes: Array<'email' | 'sms' | 'push'> = ['email', 'sms', 'push'];
constructor(private notificationFactory: NotificationFactory) {}
send(type: 'email' | 'sms' | 'push'): void {
const notification: Notification =
this.notificationFactory.createNotification(type);
notification.send(`This is a ${type} notification!`);
}
}

✅ Decouples creation from usage
✅ Easy to add new notification types
✅ Centralized object creation logic
✅ Simplifies component code


Strategy Pattern manages different algorithms or behaviors, allowing dynamic switching based on context. It’s especially useful for handling different payment methods, sorting algorithms, or authentication strategies.

Payment Strategy Interface:

export interface PaymentStrategy {
processPayment(amount: number): Promise<PaymentResult>;
validate(): boolean;
}

Concrete Strategies:

@Injectable({
providedIn: 'root',
})
export class PayPalPayment implements PaymentStrategy {
validate(): boolean {
return !!localStorage.getItem('paypal_token');
}
processPayment(amount: number): Promise<PaymentResult> {
return Promise.resolve({
success: true,
transactionId: 'PAYPAL_' + Date.now(),
amount,
method: 'PayPal'
});
}
}
@Injectable({
providedIn: 'root',
})
export class CreditCardPayment implements PaymentStrategy {
validate(): boolean {
return !!localStorage.getItem('card_token');
}
processPayment(amount: number): Promise<PaymentResult> {
return Promise.resolve({
success: true,
transactionId: 'CARD_' + Date.now(),
amount,
method: 'Credit Card'
});
}
}
@Injectable({
providedIn: 'root',
})
export class CryptoPayment implements PaymentStrategy {
validate(): boolean {
return !!localStorage.getItem('crypto_wallet');
}
processPayment(amount: number): Promise<PaymentResult> {
return Promise.resolve({
success: true,
transactionId: 'CRYPTO_' + Date.now(),
amount,
method: 'Cryptocurrency'
});
}
}

Context Service:

@Injectable({
providedIn: 'root',
})
export class PaymentService {
private currentStrategy: PaymentStrategy;
constructor(
private payPalPayment: PayPalPayment,
private creditCardPayment: CreditCardPayment,
private cryptoPayment: CryptoPayment
) {
this.currentStrategy = this.creditCardPayment; // Default
}
setPaymentStrategy(strategy: PaymentStrategy): void {
if (!strategy.validate()) {
throw new Error('Payment strategy is not valid');
}
this.currentStrategy = strategy;
}
processPayment(amount: number): Promise<PaymentResult> {
return this.currentStrategy.processPayment(amount);
}
getAvailableStrategies(): PaymentStrategy[] {
const strategies = [
this.payPalPayment,
this.creditCardPayment,
this.cryptoPayment
];
return strategies.filter(s => s.validate());
}
}

Component Usage:

@Component({
selector: 'app-checkout',
template: `
<h2>Checkout</h2>
<select [(ngModel)]="selectedMethod()">
@for (method of availableMethods(); track method.constructor.name) {
<option [value]="method.constructor.name">
{{ method.constructor.name }}
</option>
}
</select>
<button (click)="pay()">Pay ${{ amount }}</button>
`
})
export class CheckoutComponent implements OnInit {
amount = 99.99;
availableMethods = signal<PaymentStrategy[]>([]);
selectedMethod = signal('CreditCardPayment');
constructor(private paymentService: PaymentService) {}
ngOnInit(): void {
this.availableMethods.set(this.paymentService.getAvailableStrategies());
}
async pay(): Promise<void> {
try {
const result = await this.paymentService.processPayment(this.amount);
console.log('Payment successful:', result);
} catch (error) {
console.error('Payment failed:', error);
}
}
}

✅ Easy to switch between algorithms at runtime
✅ Eliminates conditional logic (if/else chains)
✅ Follows Open/Closed Principle
✅ Simplifies testing each strategy independently



The Dependency Injection pattern is fundamental to Angular. Instead of classes creating their own dependencies, they receive them from an external source, promoting loose coupling and testability.

Service Example:

@Injectable({
providedIn: 'root'
})
export class ProductService {
constructor(private http: HttpClient) {}
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products');
}
getProductById(id: string): Observable<Product> {
return this.http.get<Product>(`/api/products/${id}`);
}
}

Component Usage:

@Component({
selector: 'app-products',
template: `
<div *ngFor="let product of products$ | async">
{{ product.name }} - ${{ product.price }}
</div>
`
})
export class ProductsComponent implements OnInit {
products$: Observable<Product[]>;
constructor(private productService: ProductService) {}
ngOnInit(): void {
this.products$ = this.productService.getProducts();
}
}

✅ Loose coupling between components and services
✅ Easy to test (mock dependencies)
✅ Single responsibility per service
✅ Service reuse across components
✅ Centralized service configuration


The Observer Pattern enables reactive programming where multiple objects automatically receive updates when a source object changes. Angular uses RxJS Observables to implement this pattern.

Service with Observable State:

@Injectable({
providedIn: 'root'
})
export class CartService {
private cartItems = new BehaviorSubject<Product[]>([]);
public cartItems$ = this.cartItems.asObservable();
private cartCount = new BehaviorSubject<number>(0);
public cartCount$ = this.cartCount.asObservable();
addToCart(product: Product): void {
const current = this.cartItems.value;
const updated = [...current, product];
this.cartItems.next(updated);
this.cartCount.next(updated.length);
}
removeFromCart(productId: string): void {
const current = this.cartItems.value;
const updated = current.filter(p => p.id !== productId);
this.cartItems.next(updated);
this.cartCount.next(updated.length);
}
getCartItems(): Observable<Product[]> {
return this.cartItems$;
}
}

Component Observing Changes:

@Component({
selector: 'app-cart',
template: `
<h2>Shopping Cart ({{ cartCount() }} items)</h2>
@for (item of cartItems(); track item.id) {
<div>
<p>{{ item.name }} - ${{ item.price }}</p>
<button (click)="remove(item.id)">Remove</button>
</div>
}
`
})
export class CartComponent implements OnInit {
cartItems = signal<Product[]>([]);
cartCount = signal<number>(0);
constructor(private cartService: CartService) {}
ngOnInit(): void {
this.cartService.getCartItems().subscribe(
items => this.cartItems.set(items)
);
this.cartService.cartCount$.subscribe(
count => this.cartCount.set(count)
);
}
remove(productId: string): void {
this.cartService.removeFromCart(productId);
}
}
// Subject — Manual control, multicast
private subject = new Subject<Data>();
// BehaviorSubject — Emits last value to new subscribers
private behaviorSubject = new BehaviorSubject<Data>(initialValue);
// ReplaySubject — Emits n previous values
private replaySubject = new ReplaySubject<Data>(bufferSize);
// AsyncSubject — Emits only the last value on complete
private asyncSubject = new AsyncSubject<Data>();

✅ Automatic UI updates when data changes
✅ Reactive, declarative code
✅ Easy to handle asynchronous operations
✅ Built-in unsubscription handling with async pipe
✅ Multiple subscribers without duplication


The Decorator Pattern attaches metadata or additional behavior to classes without modifying the class itself. Angular heavily relies on decorators (@Component, @Injectable, @Directive, @Pipe) to provide metadata and functionality.

Class Decorators:

@Component({
selector: 'app-user-profile',
template: `
<div class="profile">
<h1>{{ user.name }}</h1>
<p>{{ user.email }}</p>
</div>
`,
styles: [`
.profile {
padding: 20px;
border: 1px solid #ccc;
}
`]
})
export class UserProfileComponent {
user = input<User>(null!);
userUpdated = output<User>();
}

Property Decorators:

import { Component, input, output, viewChild, contentChild } from '@angular/core';
export class UserComponent {
firstName = input<string>('');
nameChanged = output<string>();
template = viewChild<TemplateRef<any>>('template');
card = contentChild(CardComponent);
}

Method Decorators:

@Injectable({
providedIn: 'root'
})
export class ApiService {
@HostListener('window:resize', ['$event']) onResize(event: any) {
console.log('Window resized:', event);
}
}

Custom Decorator Example:

export function LogExecution(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`${propertyKey} returned:`, result);
return result;
};
return descriptor;
}
export class UserService {
@LogExecution
getUserById(id: string): User {
return { id, name: 'John Doe', email: 'john@example.com' };
}
}
APIPurposeAngular 18+
@ComponentMarks a class as Angular component with metadata✓
@InjectableMarks a class as injectable service✓
@DirectiveMarks a class as Angular directive✓
@PipeMarks a class as Angular pipe✓
input()Declares input property (replaces @Input)✓ Modern
output()Declares output event (replaces @Output)✓ Modern
viewChild()Queries template (replaces @ViewChild)✓ Modern
viewChildren()Queries templates (replaces @ViewChildren)✓ Modern
contentChild()Queries content (replaces @ContentChild)✓ Modern
contentChildren()Queries contents (replaces @ContentChildren)✓ Modern
@HostBindingBinds to host element property✓
@HostListenerListens to host element events✓

✅ Adds behavior without modifying original class
✅ Separates concerns (presentation, logic, metadata)
✅ Enables Angular framework features
✅ Makes code declarative and readable
✅ Supports custom cross-cutting concerns


  1. Don’t Over-Engineer — Use patterns when they solve real problems, not for theoretical perfection
  2. Keep It Simple — Choose the simplest pattern that solves your use case
  3. Combine Patterns — Patterns work well together (Facade + Singleton, DI + Observer)
  4. Document Your Patterns — Make it clear why a pattern was chosen
  5. Test Early — Patterns make code more testable; leverage this advantage
  6. Use TypeScript Strictly — Interfaces and types help enforce pattern contracts
  7. Leverage Angular Features — Use @Injectable, @Component, Observables as intended

ScenarioPatternExample
Manage global stateSingletonAuthentication service
Create complex objectsFactoryNotification system
Simplify complex subsystemsFacadeShopping cart manager
Switch algorithms at runtimeStrategyPayment methods
Receive updates automaticallyObserverCart count updates
Decouple dependenciesDIService injection
Add metadata/behaviorDecorator@Component, @Input