The Decorator Pattern allows behavior to be added to individual objects dynamically, either statically or at run-time, without affecting the behavior of other objects from the same class. This provides a flexible alternative to subclassing for extending functionality.
Difficulty: ⭐⭐ Medium
Category: Structural Pattern
Interview Frequency: ⭐⭐⭐⭐ Very Common
Imagine you have a base object and want to add features dynamically:
- Without Decorator: Create 5 subclasses for 5 features, or 25 subclasses for combinations
- With Decorator: Wrap the object with feature decorators
Example: Coffee shop with add-ons
Coffee
↓
Coffee + Milk → Coffee + Milk + Sugar → Coffee + Milk + Sugar + Whipped Cream
Think of gifts and wrapping:
- Core: A gift (book, watch, toy)
- Decorators: Wrapping paper, ribbon, gift tag
- You can combine any decorators without changing the core gift
- Each wrapper adds functionality (protection, decoration) without modifying the gift itself
✅ Use Decorator Pattern when:
- You want to add responsibilities to objects dynamically
- Subclassing would create too many classes
- You need to combine multiple features
- You want to add features without modifying existing code
- Features are independent and can be combined
❌ Don't use when:
- Features are core to the object (use inheritance)
- Order of decorators matters critically
- Simple scenarios (just add a property)
- Performance is critical (wrapping adds overhead)
See decorator.ts for complete implementations.
- Component: Defines interface for objects that can be decorated
- ConcreteComponent: The base object
- Decorator: Abstract decorator implementing Component
- ConcreteDecorators: Add specific behaviors
// Component interface
interface Component {
operation(): string;
}
// Concrete Component
class ConcreteComponent implements Component {
operation(): string {
return "ConcreteComponent";
}
}
// Abstract Decorator
abstract class Decorator implements Component {
constructor(protected component: Component) {}
operation(): string {
return this.component.operation();
}
}
// Concrete Decorators
class ConcreteDecoratorA extends Decorator {
operation(): string {
return `ConcreteDecoratorA(${super.operation()})`;
}
}
class ConcreteDecoratorB extends Decorator {
operation(): string {
return `ConcreteDecoratorB(${super.operation()})`;
}
}
// Usage
let component: Component = new ConcreteComponent();
component = new ConcreteDecoratorA(component);
component = new ConcreteDecoratorB(component);
console.log(component.operation());
// Output: ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))✅ Flexibility: Add/remove features at runtime
✅ Single Responsibility: Each decorator has one job
✅ Composition over Inheritance: Avoid explosion of subclasses
✅ Open/Closed Principle: Open for extension, closed for modification
✅ Combination: Mix and match decorators in any order
❌ Order of decorators can matter
❌ Hard to remove decorators from chain
❌ Performance overhead (wrapping)
❌ Complex to debug (many layers)
❌ Can lead to "decorator soup"
Answer: Decorator is better when:
- You want to combine multiple features dynamically
- You have many possible combinations (2^n subclasses needed)
- Features are optional or vary at runtime
Example:
// Inheritance (Problem)
class Coffee {}
class CoffeeWithMilk extends Coffee {}
class CoffeeWithMilkAndSugar extends Coffee {}
class CoffeeWithMilkAndSugarAndWhippedCream extends Coffee {}
// ... exponential growth!
// Decorator (Solution)
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new WhippedCreamDecorator(coffee);Answer:
- Decorator: Adds behavior (wraps object) at runtime
- Strategy: Swaps algorithm for context
// Decorator
class LoggingDecorator {
constructor(private service: Service) {}
execute() {
console.log("Starting...");
return this.service.execute();
}
}
// Strategy
class Context {
constructor(private strategy: Strategy) {}
execute() {
return this.strategy.execute();
}
}Decorator: "Add extra behavior"
Strategy: "Choose which algorithm"
Answer: Order can matter:
// Different order, different results
const coffee1 = new MilkDecorator(new SugarDecorator(baseCoffee));
const coffee2 = new SugarDecorator(new MilkDecorator(baseCoffee));
// Results differ if price calculation depends on base
coffee1.getPrice(); // Milk(Sugar(base))
coffee2.getPrice(); // Sugar(Milk(base))Solution: Make decorators order-independent or document the expected order.
Answer: Yes! Common combinations:
// Decorator + Factory
class DecoratorFactory {
static createCoffee(options: string[]): Coffee {
let coffee: Coffee = new SimpleCoffee();
options.forEach((opt) => {
coffee = this.getDecorator(opt, coffee);
});
return coffee;
}
}
// Decorator + Strategy
class PaymentService {
private strategy: PaymentStrategy;
private decorators: Decorator[];
execute() {
let result = this.strategy.pay();
this.decorators.forEach((d) => (result = d.decorate(result)));
return result;
}
}Answer: Several strategies:
- Limit chain depth:
addDecorator(decorator: Decorator): void {
if (this.decoratorCount >= 3) {
throw new Error("Max decorators reached");
}
// ...
}- Composite decorators:
// Combine multiple into one
class PremiumDecorator extends Decorator {
constructor(component: Component) {
super(new HealthBenefitsDecorator(new PriorityDecorator(component)));
}
}- Builder pattern:
new CoffeeBuilder().withMilk().withSugar().withWhippedCream().build();Answer:
- TypeScript @decorator: Syntax feature (metadata, class/method modification)
- Decorator Pattern: Design pattern (wrapping objects)
They're different things!
// TypeScript decorator (syntax)
@Logging
@Auth
class MyService {
@Memoize
expensiveMethod() {}
}
// Pattern decorator (runtime wrapping)
const service = new MyService();
const logged = new LoggingDecorator(service);
const secured = new AuthDecorator(logged);- I/O Streams: BufferedInputStream, DataInputStream
- GUI Components: Borders, scrollbars, decorations
- Middleware: Express.js middleware stack
- Caching: Cache decorators for expensive operations
- Logging: Add logging to any service
- Authentication: Add auth checks to endpoints
- Compression: Add compression to data streams
- Validation: Add validation to input data
- Formatting: Add formatting to output
- Monitoring: Add metrics/telemetry
class User {}
class UserWithLogging extends User {}
class UserWithLoggingAndCaching extends User {}
class UserWithLoggingAndCachingAndAuth extends User {}
// ... nightmare!let user: UserService = new UserRepository();
user = new LoggingDecorator(user);
user = new CachingDecorator(user);
user = new AuthDecorator(user);
// Clean and composable!class ServiceWrapper implements Service {
constructor(private inner: Service) {}
method(): Result {
return this.inner.method();
}
}if (enableLogging) {
service = new LoggingDecorator(service);
}
if (enableCaching) {
service = new CachingDecorator(service);
}const config = loadConfig();
config.decorators.forEach((dec) => {
service = decoratorFactory.create(dec, service);
});test("decorator should wrap component", () => {
const component = new MockComponent();
const decorated = new TestDecorator(component);
decorated.operation();
expect(component.operation).toHaveBeenCalled();
});
test("decorator should add behavior", () => {
const component = new SimpleComponent();
const decorated = new LoggingDecorator(component);
const result = decorated.operation();
expect(result).toContain("logged");
});- Strategy: Swap algorithms; Decorator adds behavior
- Proxy: Control access; Decorator adds features
- Adapter: Change interface; Decorator enhances
- Chain of Responsibility: Sequential processing
- Builder: Construct complex objects
The Decorator Pattern solves the "explosion of subclasses" problem by:
- Wrapping objects instead of subclassing
- Adding behavior at runtime
- Combining features flexibly
- Keeping classes focused and reusable
Key Takeaway: "Wrap objects to add behavior without creating subclass explosion."
When to use: Multiple optional, combinable features
When not to: Core features (use inheritance), simple additions
Next: Try the Singleton Pattern →