← Back to principles

Design Principle

Decorator Pattern

Learn the Decorator pattern: attach responsibilities to objects dynamically. A flexible alternative to subclassing for extending functionality at runtime.

Decorator Pattern

Why This Matters

Think of the Decorator pattern like adding toppings to a pizza. You start with a base pizza, then add toppings (cheese, pepperoni, mushrooms) one at a time. Each topping adds something new without changing the base. The Decorator pattern does the same for code—it lets you add behavior to objects dynamically, one feature at a time.

This matters because sometimes you need to add features to objects, but subclassing creates a combinatorial explosion. If you have 3 base classes and 5 features, you'd need 3 × 2^5 = 96 subclasses. The Decorator pattern lets you compose features dynamically, avoiding this explosion.

In interviews, when someone asks "How would you add features to objects without subclassing?", they're testing whether you understand the Decorator pattern. Do you know how to compose decorators? Can you add behavior dynamically? Most engineers can't. They use inheritance and wonder why they have so many classes.

What Engineers Usually Get Wrong

Engineers often think "Decorator pattern is just composition." But Decorator pattern is more specific—it maintains the same interface as the component it decorates, and decorators can be composed. This allows you to stack decorators (e.g., add compression, then encryption, then logging) without the client knowing.

Engineers also don't understand when to use Decorator vs Strategy. Decorator adds behavior to objects (like adding features). Strategy chooses between algorithms (like choosing a sorting algorithm). Use Decorator when you need to add features dynamically. Use Strategy when you need to choose algorithms.

How This Breaks Systems in the Real World

A service was using inheritance to add features. They had classes like CompressedFile, EncryptedFile, CompressedEncryptedFile, LoggedFile, CompressedLoggedFile, etc. The class hierarchy exploded. Adding a new feature required creating many new classes. The fix? Use Decorator pattern. Create decorators for each feature (CompressionDecorator, EncryptionDecorator, LoggingDecorator). Compose them dynamically. Now adding a new feature is just creating one decorator, not many classes.

Another story: A service was using decorators but didn't maintain the same interface. Each decorator had a different interface, making composition impossible. The fix? Ensure decorators implement the same interface as the component they decorate. This allows decorators to be composed.


What is the Decorator Pattern?

Decorator Pattern provides:

  • Dynamic behavior: Add behavior at runtime
  • Composition over inheritance: Extend without subclassing
  • Flexible combination: Combine multiple decorators
  • Single responsibility: Each decorator adds one feature

Use cases:

  • Adding features to objects dynamically
  • Extending functionality without modifying classes
  • Combining multiple features
  • Stream processing (Java I/O streams)

Structure

Component (interface)
  └─ operation()

ConcreteComponent (implements Component)
  └─ operation()

Decorator (implements Component)
  └─ component: Component
  └─ operation() (calls component.operation())

ConcreteDecorator (extends Decorator)
  └─ operation() (adds behavior, calls super.operation())

Examples

Basic Decorator Pattern

1// Component interface
2interface Coffee {
3 getCost(): number;
4 getDescription(): string;
5}
6
7// Concrete component
8class SimpleCoffee implements Coffee {
9 getCost(): number {
10 return 5;
11 }
12
13 getDescription(): string {
14 return "Simple coffee";
15 }
16}
17
18// Decorator
19abstract class CoffeeDecorator implements

HTTP Request Decorator

1// Component
2interface HTTPRequest {
3 send(): Promise<Response>;
4}
5
6// Concrete component
7class BasicHTTPRequest implements HTTPRequest {
8 constructor(private url: string, private options: RequestInit) {}
9
10 async send(): Promise<Response> {
11 return await fetch(this.url, this.options);
12 }
13}
14
15// Decorator

Common Pitfalls

  • Too many decorators: Can become complex. Fix: Keep decorators simple, limit nesting
  • Order matters: Decorator order affects behavior. Fix: Document order, use builder pattern
  • Performance overhead: Multiple layers add overhead. Fix: Consider performance impact
  • Not using interfaces: Tight coupling. Fix: Use interfaces, dependency injection

Interview Questions

Beginner

Q: What is the Decorator pattern and how does it differ from inheritance?

A:

Decorator Pattern attaches additional responsibilities to objects dynamically.

Key characteristics:

  • Dynamic behavior: Add behavior at runtime
  • Composition: Uses composition instead of inheritance
  • Flexible combination: Combine multiple decorators
  • Single responsibility: Each decorator adds one feature

Example:

1let coffee: Coffee = new SimpleCoffee();
2coffee = new MilkDecorator(coffee);
3coffee = new SugarDecorator(coffee);
4// Coffee now has milk and sugar

Difference from inheritance:

  • Inheritance: Static, compile-time, creates new class
  • Decorator: Dynamic, runtime, wraps existing object

Benefits:

  • Flexibility: Add/remove features at runtime
  • No class explosion: Don't need classes for every combination
  • Single responsibility: Each decorator does one thing

Intermediate

Q: Explain how the Decorator pattern works. How do you compose multiple decorators?

A:

Decorator Pattern Structure:

1. Component Interface:

1interface Component {
2 operation(): void;
3}

2. Concrete Component:

1class ConcreteComponent implements Component {
2 operation(): void {
3 console.log("Base operation");
4 }
5}

3. Decorator (Abstract):

1abstract class Decorator implements Component {
2 protected component: Component;
3
4 constructor(component: Component) {
5 this.component = component;
6 }
7
8 operation(): void {
9 this.component.operation();
10 }
11}

4. Concrete Decorators:

1class ConcreteDecoratorA extends Decorator {
2 operation(): void {
3 super.operation();
4 this.addedBehavior();
5 }
6
7 private addedBehavior(): void {
8 console.log("Added behavior A");
9 }
10}

Composing Multiple Decorators:

1let component: Component = new ConcreteComponent();
2component = new ConcreteDecoratorA(component);
3component = new ConcreteDecoratorB(component);
4component = new ConcreteDecoratorC(component);
5
6component.operation();
7// Output:
8// Base operation
9// Added behavior A
10// Added behavior B
11// Added behavior C

Order matters: Decorators are applied in reverse order of wrapping.


Senior

Q: Design a decorator system for a text processing pipeline that supports multiple transformations (encryption, compression, formatting) that can be applied in any order. Handle performance and ensure transformations are reversible.

A:

1// Component interface
2interface TextProcessor {
3 process(text: string): string;
4 reverse(text: string): string; // For reversible transformations
5}
6
7// Concrete component
8class PlainTextProcessor implements TextProcessor {
9 process(text: string): string {
10 return text;
11 }
12
13 reverse(text: string): string {
14 return text;
15 }
16}

Features:

  1. Reversible transformations: Each decorator can reverse
  2. Flexible composition: Add decorators in any order
  3. Performance: Can optimize based on decorator order
  4. Pipeline builder: Fluent interface for building pipelines

  • Decorator pattern: Adds behavior to objects dynamically

  • Composition over inheritance: Extends without subclassing

  • Flexible combination: Combine multiple decorators

  • Single responsibility: Each decorator adds one feature

  • Use cases: Adding features dynamically, stream processing, text processing

  • Best practices: Keep decorators simple, document order, consider performance

  • Adapter Pattern - Both patterns wrap objects. Decorator adds behavior, Adapter converts interface. Understanding both helps choose the right pattern.

  • Proxy Pattern - Both patterns wrap objects. Decorator adds behavior, Proxy controls access. Understanding both helps choose the right pattern.

  • Composite Pattern - Both patterns compose objects. Decorator composes features, Composite composes tree structures.

  • Composition Over Inheritance - Decorator pattern is a prime example of composition over inheritance. Understanding composition helps understand decorator benefits.

  • SOLID Principles - Decorator pattern follows Open/Closed Principle (extend without modify). Understanding SOLID helps understand decorator benefits.

Keep exploring

Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.