Skip to content
Angular ng patterns 4 min read

Design Patterns Overview

Design patterns are reusable solutions to recurring problems in software design. Angular, with its component tree, dependency injection container, and reactive primitives, naturally encourages a handful of patterns that keep applications testable, decoupled, and easy to scale. This page surveys the patterns most worth knowing — what each one solves, when to reach for it, and how it maps onto modern Angular (standalone components, signals, and inject()).

Why patterns matter in Angular

Angular is opinionated about structure: it ships with dependency injection, change detection, and a reactive layer built on RxJS and signals. Patterns let you compose those primitives deliberately instead of accidentally. A well-chosen pattern reduces coupling between the things that render and the things that fetch, transform, or coordinate data — which is precisely where large Angular apps tend to rot.

The patterns below fall into three loose buckets: component structure (how UI is organized), service design (how state and side effects are managed), and behavior abstraction (how you swap or adapt implementations).

The pattern catalog at a glance

PatternProblem it solvesAngular vehicle
Smart & Dumb componentsMixing data access with presentationContainer vs. presentational components, input() / output()
Singleton serviceSharing one stateful instance app-wide@Injectable({ providedIn: 'root' })
Observable data serviceReactive, push-based shared stateBehaviorSubject / signals exposed read-only
FacadeHiding a complex subsystem behind a small APIA single injectable that orchestrates services
AdapterReshaping external data to your domain modelA mapper service or function
StrategySelecting an algorithm at runtimeInjection tokens + interchangeable implementations

Component structure patterns

The smart and dumb component split is the most foundational Angular pattern. Smart (container) components inject services, hold state, and decide what to show. Dumb (presentational) components receive data through inputs and emit events through outputs — they know nothing about where data comes from. This keeps presentational components trivially reusable and testable.

import { Component, input, output } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <article class="card">
      <h3>{{ name() }}</h3>
      @if (active()) {
        <span class="badge">active</span>
      }
      <button (click)="select.emit(id())">Select</button>
    </article>
  `,
})
export class UserCardComponent {
  id = input.required<string>();
  name = input.required<string>();
  active = input(false);
  select = output<string>();
}

This dumb component has zero dependencies — a parent container decides how to wire it up.

Service design patterns

State that outlives a single component belongs in a service. The singleton service pattern uses providedIn: 'root' so Angular’s injector creates exactly one instance, shared everywhere it is injected. Layer the observable data service pattern on top to expose state reactively while keeping writes private.

import { Injectable, signal, computed } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class CartService {
  private items = signal<string[]>([]);

  // Read-only views for consumers.
  readonly count = computed(() => this.items().length);
  readonly contents = this.items.asReadonly();

  add(sku: string): void {
    this.items.update((list) => [...list, sku]);
  }
}
const cart = inject(CartService);
cart.add('SKU-42');
console.log('Items in cart:', cart.count());

Output:

Items in cart: 1

Consumers can read count and contents but cannot mutate the underlying signal — mutation flows only through add(). That encapsulation is the whole point of the observable data service pattern.

Tip: Exposing signal.asReadonly() (or Subject.asObservable() for RxJS) is the cleanest way to enforce one-way data flow. Never hand out the writable source.

Behavior abstraction patterns

When complexity grows, the facade pattern gives feature components a single, intention-revealing API that hides several collaborating services behind it. The adapter pattern translates messy external shapes into clean domain models, and the strategy pattern lets you swap algorithms (sorting, pricing, validation) at runtime via injection tokens.

import { InjectionToken, inject } from '@angular/core';

export interface PricingStrategy {
  total(amounts: number[]): number;
}

export const PRICING_STRATEGY = new InjectionToken<PricingStrategy>('PricingStrategy');

export const standardPricing: PricingStrategy = {
  total: (amounts) => amounts.reduce((sum, n) => sum + n, 0),
};

// Swap the implementation in a route or test by re-providing the token.
class Checkout {
  private strategy = inject(PRICING_STRATEGY);
  pay(line: number[]) {
    return this.strategy.total(line);
  }
}

Because the strategy is resolved through DI, you can provide a discount-aware implementation in one feature and a tax-aware one in another without touching Checkout.

Choosing the right pattern

Patterns are tools, not goals. Reach for them when you feel a specific pain:

SymptomLikely fix
Components fetch data and render itSmart / dumb split
State copied across componentsObservable data service
A component injects five services to do one taskFacade
Third-party API shapes leak into templatesAdapter
Long if/switch choosing behaviorStrategy

Best Practices

  • Start simple — introduce a pattern when duplication or coupling actually appears, not preemptively.
  • Keep presentational components dependency-free so they stay reusable and easy to test.
  • Expose state as read-only signals or observables; route all writes through explicit methods.
  • Prefer inject() and functional providers/guards over constructor boilerplate in modern Angular.
  • Use injection tokens for strategy and adapter implementations so they are swappable in tests.
  • Let a facade hide coordination, not just delegate one-to-one — otherwise it adds indirection without value.
  • Document the intent of each pattern in code so future contributors understand the boundary it enforces.
Last updated June 14, 2026
Was this helpful?