Bird
Raised Fist0
Interview Prepoop-design-patternsmediumAmazonGoogleFlipkartSwiggyCRED

Decorator Pattern - Wrapping Behaviour Without Subclassing

Choose your preparation mode3 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
🎯
Decorator Pattern - Wrapping Behaviour Without Subclassing
mediumOOPAmazonGoogleFlipkart

Imagine you want to add new features to a coffee order, like milk or sugar, without creating a new subclass for every combination. How can you dynamically add these behaviors at runtime?

💡 This problem is about adding new behavior to objects without changing their class or creating many subclasses. Beginners often struggle because they try to use inheritance for every variation, which leads to class explosion and rigid designs. The decorator pattern solves this by wrapping objects to add features dynamically.
📋
Problem Statement

Design a system where you can add responsibilities to objects dynamically without subclassing. Implement the Decorator pattern to wrap an object and extend its behavior at runtime. The system should allow stacking multiple decorators transparently.

The base component and decorators must share a common interface.Decorators should be able to wrap both concrete components and other decorators.The design should avoid subclass explosion and allow flexible behavior composition.
💡
Example
Input"Create a Coffee object, then wrap it with MilkDecorator and SugarDecorator."
OutputThe final cost and description reflect coffee with milk and sugar added.

The Coffee object is wrapped first by MilkDecorator which adds milk cost and description, then by SugarDecorator which adds sugar cost and description. The calls delegate through the wrappers, accumulating behavior.

  • Decorating an object with no decorators → returns base behavior
  • Multiple decorators wrapping each other → combined behavior
  • Decorator wrapping null or invalid component → should handle gracefully
  • Decorator that adds no behavior → should not alter output
⚠️
Common Mistakes
Trying to add behavior by modifying the base class directly

Leads to rigid code and violates open-closed principle

Use decorators to add behavior without changing existing classes

Not delegating calls properly in decorator methods

Behavior is lost or incomplete, causing incorrect results

Always delegate to the wrapped component before or after adding behavior

Creating decorators that do not implement the component interface

Breaks polymorphism and causes runtime errors

Ensure decorators implement the same interface as components

Stacking decorators incorrectly or in wrong order

Unexpected behavior or incorrect results

Carefully order decorators to reflect desired behavior composition

Using inheritance instead of composition for decorators

Leads to subclass explosion and inflexible design

Use composition to wrap components dynamically

🧠
Brute Force (Subclassing for Every Variation)
💡 This approach shows the naive way beginners often try: creating a subclass for every combination of features. It helps understand why this is impractical and motivates the decorator pattern.

Intuition

Create a subclass for each combination of added behaviors, e.g., CoffeeWithMilk, CoffeeWithSugar, CoffeeWithMilkAndSugar. Each subclass overrides methods to add behavior.

Algorithm

  1. Define a base component class with core behavior.
  2. Create subclasses for each added feature, overriding methods to extend behavior.
  3. For combinations, create subclasses inheriting from other subclasses to combine features.
  4. Use instances of these subclasses to get desired behavior.
💡 This approach is straightforward but quickly becomes complex and hard to maintain as features combine.
</>
Code
class Coffee:
    def cost(self):
        return 5
    def description(self):
        return "Coffee"

class CoffeeWithMilk(Coffee):
    def cost(self):
        return super().cost() + 1
    def description(self):
        return super().description() + ", Milk"

class CoffeeWithSugar(Coffee):
    def cost(self):
        return super().cost() + 0.5
    def description(self):
        return super().description() + ", Sugar"

class CoffeeWithMilkAndSugar(CoffeeWithMilk):
    def cost(self):
        return super().cost() + 0.5
    def description(self):
        return super().description() + ", Sugar"

# Driver code
if __name__ == "__main__":
    c = CoffeeWithMilkAndSugar()
    print(c.description())  # Coffee, Milk, Sugar
    print(c.cost())         # 6.5
Line Notes
class Coffee:Defines the base component with core behavior to represent plain coffee
def cost(self):Returns the base cost of coffee without any additions
class CoffeeWithMilk(Coffee):Subclass that adds milk behavior by overriding cost and description
return super().cost() + 1Adds the cost of milk to the base coffee cost
class Coffee {
    public double cost() {
        return 5;
    }
    public String description() {
        return "Coffee";
    }
}

class CoffeeWithMilk extends Coffee {
    @Override
    public double cost() {
        return super.cost() + 1;
    }
    @Override
    public String description() {
        return super.description() + ", Milk";
    }
}

class CoffeeWithSugar extends Coffee {
    @Override
    public double cost() {
        return super.cost() + 0.5;
    }
    @Override
    public String description() {
        return super.description() + ", Sugar";
    }
}

class CoffeeWithMilkAndSugar extends CoffeeWithMilk {
    @Override
    public double cost() {
        return super.cost() + 0.5;
    }
    @Override
    public String description() {
        return super.description() + ", Sugar";
    }
}

public class Main {
    public static void main(String[] args) {
        Coffee c = new CoffeeWithMilkAndSugar();
        System.out.println(c.description()); // Coffee, Milk, Sugar
        System.out.println(c.cost());        // 6.5
    }
}
Line Notes
class Coffee {Base component class with core methods representing plain coffee
public double cost() {Returns the base cost of coffee
class CoffeeWithMilk extends Coffee {Subclass that adds milk behavior by overriding methods
return super.cost() + 1;Adds milk cost to the base coffee cost
#include <iostream>
#include <string>

class Coffee {
public:
    virtual double cost() { return 5; }
    virtual std::string description() { return "Coffee"; }
    virtual ~Coffee() {}
};

class CoffeeWithMilk : public Coffee {
public:
    double cost() override { return Coffee::cost() + 1; }
    std::string description() override { return Coffee::description() + ", Milk"; }
};

class CoffeeWithSugar : public Coffee {
public:
    double cost() override { return Coffee::cost() + 0.5; }
    std::string description() override { return Coffee::description() + ", Sugar"; }
};

class CoffeeWithMilkAndSugar : public CoffeeWithMilk {
public:
    double cost() override { return CoffeeWithMilk::cost() + 0.5; }
    std::string description() override { return CoffeeWithMilk::description() + ", Sugar"; }
};

int main() {
    Coffee* c = new CoffeeWithMilkAndSugar();
    std::cout << c->description() << std::endl; // Coffee, Milk, Sugar
    std::cout << c->cost() << std::endl;        // 6.5
    delete c;
    return 0;
}
Line Notes
class Coffee {Base component with virtual methods for polymorphism representing plain coffee
virtual double cost() { return 5; }Returns base cost of coffee
class CoffeeWithMilk : public Coffee {Subclass that adds milk behavior by overriding methods
double cost() override { return Coffee::cost() + 1; }Adds milk cost to base coffee cost
class Coffee {
    cost() {
        return 5;
    }
    description() {
        return "Coffee";
    }
}

class CoffeeWithMilk extends Coffee {
    cost() {
        return super.cost() + 1;
    }
    description() {
        return super.description() + ", Milk";
    }
}

class CoffeeWithSugar extends Coffee {
    cost() {
        return super.cost() + 0.5;
    }
    description() {
        return super.description() + ", Sugar";
    }
}

class CoffeeWithMilkAndSugar extends CoffeeWithMilk {
    cost() {
        return super.cost() + 0.5;
    }
    description() {
        return super.description() + ", Sugar";
    }
}

// Driver code
const c = new CoffeeWithMilkAndSugar();
console.log(c.description()); // Coffee, Milk, Sugar
console.log(c.cost());        // 6.5
Line Notes
class Coffee {Defines base component class representing plain coffee
cost() {Returns base cost of coffee
class CoffeeWithMilk extends Coffee {Subclass that adds milk behavior by overriding methods
return super.cost() + 1;Adds milk cost to base coffee cost
Complexity
TimeO(1) per call
SpaceO(k) for k subclasses created

Each subclass adds fixed overhead, but the number of subclasses grows exponentially with feature combinations.

💡 For 3 features, you might need up to 8 subclasses, which is unmanageable for many features.
Interview Verdict: Accepted but impractical

This approach works but is not scalable or maintainable, motivating the decorator pattern.

🧠
Decorator Pattern Using Composition
💡 This approach introduces the decorator pattern, which solves the subclass explosion by wrapping objects dynamically. It teaches composition over inheritance and flexible behavior extension.

Intuition

Instead of subclassing, create decorator classes that hold a reference to a component and add behavior before or after delegating calls to it.

Algorithm

  1. Define a component interface with core methods.
  2. Implement a concrete component class.
  3. Create decorator classes implementing the same interface and holding a reference to a component.
  4. In decorator methods, add behavior and delegate calls to the wrapped component.
💡 The key is delegation and wrapping, which may be tricky to grasp initially but enables flexible behavior stacking.
</>
Code
from abc import ABC, abstractmethod

class Coffee(ABC):
    @abstractmethod
    def cost(self):
        pass
    @abstractmethod
    def description(self):
        pass

class SimpleCoffee(Coffee):
    def cost(self):
        return 5
    def description(self):
        return "Coffee"

class CoffeeDecorator(Coffee):
    def __init__(self, coffee):
        self._coffee = coffee
    def cost(self):
        return self._coffee.cost()
    def description(self):
        return self._coffee.description()

class MilkDecorator(CoffeeDecorator):
    def cost(self):
        return super().cost() + 1
    def description(self):
        return super().description() + ", Milk"

class SugarDecorator(CoffeeDecorator):
    def cost(self):
        return super().cost() + 0.5
    def description(self):
        return super().description() + ", Sugar"

# Driver code
if __name__ == "__main__":
    coffee = SimpleCoffee()
    coffee = MilkDecorator(coffee)
    coffee = SugarDecorator(coffee)
    print(coffee.description())  # Coffee, Milk, Sugar
    print(coffee.cost())         # 6.5
Line Notes
class Coffee(ABC):Defines the component interface abstractly to enforce method implementation
def __init__(self, coffee):Decorator holds reference to wrapped component for delegation
def cost(self):Decorator delegates cost call to the wrapped component
return super().cost() + 1MilkDecorator adds milk cost on top of wrapped component's cost
interface Coffee {
    double cost();
    String description();
}

class SimpleCoffee implements Coffee {
    public double cost() { return 5; }
    public String description() { return "Coffee"; }
}

abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;
    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }
    public double cost() {
        return coffee.cost();
    }
    public String description() {
        return coffee.description();
    }
}

class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }
    public double cost() {
        return super.cost() + 1;
    }
    public String description() {
        return super.description() + ", Milk";
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }
    public double cost() {
        return super.cost() + 0.5;
    }
    public String description() {
        return super.description() + ", Sugar";
    }
}

public class Main {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        coffee = new MilkDecorator(coffee);
        coffee = new SugarDecorator(coffee);
        System.out.println(coffee.description()); // Coffee, Milk, Sugar
        System.out.println(coffee.cost());        // 6.5
    }
}
Line Notes
interface Coffee {Defines the component interface to ensure consistent methods
protected Coffee coffee;Decorator holds reference to wrapped component for delegation
public double cost() {Decorator delegates cost call to wrapped component
return super.cost() + 1;MilkDecorator adds milk cost on top of wrapped component
#include <iostream>
#include <string>
#include <memory>

class Coffee {
public:
    virtual double cost() = 0;
    virtual std::string description() = 0;
    virtual ~Coffee() {}
};

class SimpleCoffee : public Coffee {
public:
    double cost() override { return 5; }
    std::string description() override { return "Coffee"; }
};

class CoffeeDecorator : public Coffee {
protected:
    std::shared_ptr<Coffee> coffee;
public:
    CoffeeDecorator(std::shared_ptr<Coffee> c) : coffee(c) {}
    double cost() override { return coffee->cost(); }
    std::string description() override { return coffee->description(); }
};

class MilkDecorator : public CoffeeDecorator {
public:
    MilkDecorator(std::shared_ptr<Coffee> c) : CoffeeDecorator(c) {}
    double cost() override { return CoffeeDecorator::cost() + 1; }
    std::string description() override { return CoffeeDecorator::description() + ", Milk"; }
};

class SugarDecorator : public CoffeeDecorator {
public:
    SugarDecorator(std::shared_ptr<Coffee> c) : CoffeeDecorator(c) {}
    double cost() override { return CoffeeDecorator::cost() + 0.5; }
    std::string description() override { return CoffeeDecorator::description() + ", Sugar"; }
};

int main() {
    std::shared_ptr<Coffee> coffee = std::make_shared<SimpleCoffee>();
    coffee = std::make_shared<MilkDecorator>(coffee);
    coffee = std::make_shared<SugarDecorator>(coffee);
    std::cout << coffee->description() << std::endl; // Coffee, Milk, Sugar
    std::cout << coffee->cost() << std::endl;        // 6.5
    return 0;
}
Line Notes
class Coffee {Abstract component interface defining required methods
std::shared_ptr<Coffee> coffee;Decorator holds shared pointer to wrapped component for safe memory management
double cost() override { return coffee->cost(); }Delegates cost call to wrapped component
double cost() override { return CoffeeDecorator::cost() + 1; }MilkDecorator adds milk cost on top of wrapped component
class Coffee {
    cost() {
        throw new Error("Method not implemented");
    }
    description() {
        throw new Error("Method not implemented");
    }
}

class SimpleCoffee extends Coffee {
    cost() {
        return 5;
    }
    description() {
        return "Coffee";
    }
}

class CoffeeDecorator extends Coffee {
    constructor(coffee) {
        super();
        this.coffee = coffee;
    }
    cost() {
        return this.coffee.cost();
    }
    description() {
        return this.coffee.description();
    }
}

class MilkDecorator extends CoffeeDecorator {
    cost() {
        return super.cost() + 1;
    }
    description() {
        return super.description() + ", Milk";
    }
}

class SugarDecorator extends CoffeeDecorator {
    cost() {
        return super.cost() + 0.5;
    }
    description() {
        return super.description() + ", Sugar";
    }
}

// Driver code
const coffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
console.log(coffee.description()); // Coffee, Milk, Sugar
console.log(coffee.cost());        // 6.5
Line Notes
class Coffee {Defines component interface with unimplemented methods to enforce implementation
constructor(coffee) {Decorator stores reference to wrapped component for delegation
cost() { return this.coffee.cost(); }Delegates cost call to wrapped component
return super.cost() + 1;MilkDecorator adds milk cost on top of wrapped component
Complexity
TimeO(k) for k decorators stacked
SpaceO(k) for k decorator objects

Each decorator adds a small overhead and delegates calls, so cost and description calls traverse the decorator chain.

💡 For 3 decorators, 3 method calls are chained, which is efficient and scalable.
Interview Verdict: Accepted and recommended

This is the canonical solution for flexible behavior extension without subclass explosion.

🧠
Decorator Pattern with Dynamic Behavior Injection
💡 This approach extends the decorator pattern by allowing decorators to accept parameters or functions to customize behavior dynamically, increasing flexibility.

Intuition

Decorators can be parameterized or accept lambdas to add behavior dynamically, e.g., adding variable amounts of milk or sugar or logging dynamically.

Algorithm

  1. Define component interface and concrete component as before.
  2. Create decorators that accept parameters or functions in their constructor.
  3. In decorator methods, use these parameters to modify behavior dynamically.
  4. Stack decorators as needed to compose complex behavior.
💡 This approach shows how decorators can be more than static wrappers, enabling runtime customization.
</>
Code
from abc import ABC, abstractmethod

class Coffee(ABC):
    @abstractmethod
    def cost(self):
        pass
    @abstractmethod
    def description(self):
        pass

class SimpleCoffee(Coffee):
    def cost(self):
        return 5
    def description(self):
        return "Coffee"

class CoffeeDecorator(Coffee):
    def __init__(self, coffee):
        self._coffee = coffee
    def cost(self):
        return self._coffee.cost()
    def description(self):
        return self._coffee.description()

class MilkDecorator(CoffeeDecorator):
    def __init__(self, coffee, amount=1):
        super().__init__(coffee)
        self.amount = amount
    def cost(self):
        return super().cost() + 0.5 * self.amount
    def description(self):
        return super().description() + f", Milk({self.amount})"

class SugarDecorator(CoffeeDecorator):
    def __init__(self, coffee, amount=1):
        super().__init__(coffee)
        self.amount = amount
    def cost(self):
        return super().cost() + 0.2 * self.amount
    def description(self):
        return super().description() + f", Sugar({self.amount})"

# Driver code
if __name__ == "__main__":
    coffee = SimpleCoffee()
    coffee = MilkDecorator(coffee, amount=2)
    coffee = SugarDecorator(coffee, amount=3)
    print(coffee.description())  # Coffee, Milk(2), Sugar(3)
    print(coffee.cost())         # 5 + 1.0 + 0.6 = 6.6
Line Notes
def __init__(self, coffee, amount=1):Decorator accepts parameter to customize behavior dynamically
self.amount = amountStores amount to use in cost and description calculations
return super().cost() + 0.5 * self.amountCost scales with amount of milk added
return super().description() + f", Milk({self.amount})"Description shows amount dynamically for clarity
interface Coffee {
    double cost();
    String description();
}

class SimpleCoffee implements Coffee {
    public double cost() { return 5; }
    public String description() { return "Coffee"; }
}

abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;
    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }
    public double cost() { return coffee.cost(); }
    public String description() { return coffee.description(); }
}

class MilkDecorator extends CoffeeDecorator {
    private int amount;
    public MilkDecorator(Coffee coffee, int amount) {
        super(coffee);
        this.amount = amount;
    }
    public double cost() { return super.cost() + 0.5 * amount; }
    public String description() { return super.description() + ", Milk(" + amount + ")"; }
}

class SugarDecorator extends CoffeeDecorator {
    private int amount;
    public SugarDecorator(Coffee coffee, int amount) {
        super(coffee);
        this.amount = amount;
    }
    public double cost() { return super.cost() + 0.2 * amount; }
    public String description() { return super.description() + ", Sugar(" + amount + ")"; }
}

public class Main {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        coffee = new MilkDecorator(coffee, 2);
        coffee = new SugarDecorator(coffee, 3);
        System.out.println(coffee.description()); // Coffee, Milk(2), Sugar(3)
        System.out.println(coffee.cost());        // 6.6
    }
}
Line Notes
private int amount;Stores amount parameter for dynamic behavior customization
public MilkDecorator(Coffee coffee, int amount) {Constructor accepts amount to customize milk addition
return super.cost() + 0.5 * amount;Cost scales with amount of milk added
return super.description() + ", Milk(" + amount + ")";Description shows amount dynamically for clarity
#include <iostream>
#include <string>
#include <memory>

class Coffee {
public:
    virtual double cost() = 0;
    virtual std::string description() = 0;
    virtual ~Coffee() {}
};

class SimpleCoffee : public Coffee {
public:
    double cost() override { return 5; }
    std::string description() override { return "Coffee"; }
};

class CoffeeDecorator : public Coffee {
protected:
    std::shared_ptr<Coffee> coffee;
public:
    CoffeeDecorator(std::shared_ptr<Coffee> c) : coffee(c) {}
    double cost() override { return coffee->cost(); }
    std::string description() override { return coffee->description(); }
};

class MilkDecorator : public CoffeeDecorator {
    int amount;
public:
    MilkDecorator(std::shared_ptr<Coffee> c, int amt) : CoffeeDecorator(c), amount(amt) {}
    double cost() override { return CoffeeDecorator::cost() + 0.5 * amount; }
    std::string description() override { return CoffeeDecorator::description() + ", Milk(" + std::to_string(amount) + ")"; }
};

class SugarDecorator : public CoffeeDecorator {
    int amount;
public:
    SugarDecorator(std::shared_ptr<Coffee> c, int amt) : CoffeeDecorator(c), amount(amt) {}
    double cost() override { return CoffeeDecorator::cost() + 0.2 * amount; }
    std::string description() override { return CoffeeDecorator::description() + ", Sugar(" + std::to_string(amount) + ")"; }
};

int main() {
    std::shared_ptr<Coffee> coffee = std::make_shared<SimpleCoffee>();
    coffee = std::make_shared<MilkDecorator>(coffee, 2);
    coffee = std::make_shared<SugarDecorator>(coffee, 3);
    std::cout << coffee->description() << std::endl; // Coffee, Milk(2), Sugar(3)
    std::cout << coffee->cost() << std::endl;        // 6.6
    return 0;
}
Line Notes
int amount;Stores amount parameter for dynamic behavior customization
MilkDecorator(std::shared_ptr<Coffee> c, int amt) : CoffeeDecorator(c), amount(amt) {}Constructor accepts amount to customize milk addition
double cost() override { return CoffeeDecorator::cost() + 0.5 * amount; }Cost scales with amount of milk added
std::string description() override { return CoffeeDecorator::description() + ", Milk(" + std::to_string(amount) + ")"; }Description shows amount dynamically for clarity
class Coffee {
    cost() {
        throw new Error("Method not implemented");
    }
    description() {
        throw new Error("Method not implemented");
    }
}

class SimpleCoffee extends Coffee {
    cost() {
        return 5;
    }
    description() {
        return "Coffee";
    }
}

class CoffeeDecorator extends Coffee {
    constructor(coffee) {
        super();
        this.coffee = coffee;
    }
    cost() {
        return this.coffee.cost();
    }
    description() {
        return this.coffee.description();
    }
}

class MilkDecorator extends CoffeeDecorator {
    constructor(coffee, amount = 1) {
        super(coffee);
        this.amount = amount;
    }
    cost() {
        return super.cost() + 0.5 * this.amount;
    }
    description() {
        return super.description() + `, Milk(${this.amount})`;
    }
}

class SugarDecorator extends CoffeeDecorator {
    constructor(coffee, amount = 1) {
        super(coffee);
        this.amount = amount;
    }
    cost() {
        return super.cost() + 0.2 * this.amount;
    }
    description() {
        return super.description() + `, Sugar(${this.amount})`;
    }
}

// Driver code
const coffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee(), 2), 3);
console.log(coffee.description()); // Coffee, Milk(2), Sugar(3)
console.log(coffee.cost());        // 6.6
Line Notes
constructor(coffee, amount = 1) {Decorator accepts amount parameter to customize behavior dynamically
this.amount = amount;Stores amount for use in cost and description calculations
return super.cost() + 0.5 * this.amount;Cost scales with amount of milk added
return super.description() + `, Milk(${this.amount})`;Description shows amount dynamically for clarity
Complexity
TimeO(k) for k decorators stacked
SpaceO(k) for k decorator objects

Similar to previous approach but with added flexibility from parameters.

💡 Allows dynamic customization without subclassing or code duplication.
Interview Verdict: Accepted and highly flexible

This approach is best for real-world scenarios requiring dynamic behavior customization.

📊
All Approaches - One-Glance Tradeoffs
💡 In interviews, always prefer the decorator pattern using composition (Approach 2 or 3). Approach 1 is useful to mention as a naive baseline but not to code.
ApproachTimeSpaceStack RiskReconstructUse In Interview
1. Brute Force (Subclassing for Every Variation)O(1) per callO(2^k) subclasses for k featuresNoN/AMention only - never code due to scalability issues
2. Decorator Pattern Using CompositionO(k) for k decorators stackedO(k) decorator objectsNoYes, behavior can be composed and inspectedPreferred approach to implement and explain
3. Decorator Pattern with Dynamic Behavior InjectionO(k) for k decorators stackedO(k) decorator objects with parametersNoYes, with added flexibilityUse when dynamic customization is required
💼
Interview Strategy
💡 Use this guide to understand the decorator pattern from naive subclassing to flexible composition. Practice explaining why subclass explosion is bad and how decorators solve it. Code the decorator pattern fluently in your preferred language before interviews.

How to Present

Step 1: Clarify the problem and confirm the need for dynamic behavior extension.Step 2: Present the naive subclassing approach and its drawbacks.Step 3: Introduce the decorator pattern using composition and delegation.Step 4: Show how decorators can be stacked to combine behaviors.Step 5: Optionally, demonstrate parameterized decorators for dynamic customization.Step 6: Write clean, modular code and test with multiple decorators.

Time Allocation

Clarify: 2min → Approach: 5min → Code: 10min → Test: 3min. Total ~20min

What the Interviewer Tests

Understanding of composition over inheritance, ability to design flexible and maintainable code, knowledge of design patterns, and clean delegation.

Common Follow-ups

  • How to remove a decorator at runtime? → Keep references and unwrap as needed.
  • Can decorators change the interface? → No, they must conform to the component interface.
  • Difference between decorator and proxy? → Decorator adds behavior, proxy controls access.
  • How to implement decorators in languages without interfaces? → Use duck typing or abstract base classes.
💡 These follow-ups test deeper understanding of the pattern's flexibility, limitations, and differences from related patterns.
🔍
Pattern Recognition

When to Use

1. Need to add responsibilities to objects dynamically. 2. Avoid subclass explosion from combinatorial feature additions. 3. Want to extend behavior transparently without changing existing code. 4. Require flexible stacking or chaining of behaviors.

Signature Phrases

add responsibilities to objects dynamicallywrap an object to extend behavioravoid subclass explosion

NOT This Pattern When

Inheritance-based extension, Strategy pattern (encapsulates algorithms), Chain of Responsibility (passes requests)

Similar Problems

Proxy Pattern - controls access but does not add behaviorAdapter Pattern - changes interface rather than adding behavior

Practice

(1/5)
1. In a large software system, when would applying the Interface Segregation Principle (ISP) be most beneficial?
easy
A. When clients depend on interfaces that contain methods they do not use, causing unnecessary implementation burden.
B. When all clients require the exact same set of methods, so a single fat interface simplifies design.
C. When you want to enforce a strict inheritance hierarchy with minimal interfaces.
D. When you want to reduce the number of interfaces to simplify the codebase.

Solution

  1. Step 1: Understand ISP's goal

    ISP aims to prevent clients from depending on methods they don't use, avoiding fat interfaces that force unnecessary implementations.
  2. Step 2: Analyze options

    When clients depend on interfaces that contain methods they do not use, causing unnecessary implementation burden. correctly identifies the scenario where ISP helps by splitting fat interfaces. When all clients require the exact same set of methods, so a single fat interface simplifies design. describes a scenario where ISP is less needed. When you want to enforce a strict inheritance hierarchy with minimal interfaces. confuses inheritance hierarchy with interface segregation. When you want to reduce the number of interfaces to simplify the codebase. incorrectly assumes fewer interfaces always simplify design, ignoring interface misuse.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    ISP is about splitting interfaces to avoid forcing clients to implement unused methods.
Hint: ISP splits interfaces so clients only depend on what they use.
Common Mistakes:
  • Believing fewer interfaces always mean better design
  • Thinking ISP applies when all clients use all methods
  • Confusing ISP with inheritance hierarchy rules
2. You are designing a system where multiple components need to be notified when certain events occur, but each component only wants to receive notifications for specific event types. Which design approach best ensures loose coupling and efficient event delivery to interested components only?
easy
A. Using a publish-subscribe pattern where components subscribe to event types and get notified only for those
B. Implementing a centralized event queue that all components read from regardless of event type
C. Polling each component periodically to check for event changes
D. Using a brute force approach where the subject notifies all components for every event

Solution

  1. Step 1: Understand the problem constraints

    The system requires notifying multiple components selectively based on event types, ensuring loose coupling.
  2. Step 2: Identify the design pattern that supports selective notification

    The publish-subscribe pattern allows components to subscribe to specific event types and receive notifications only for those, avoiding unnecessary updates and tight coupling.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Publish-subscribe enables selective, decoupled notifications [OK]
Hint: Selective notification requires publish-subscribe pattern [OK]
Common Mistakes:
  • Assuming polling is efficient for event-driven updates
3. Identify the bug in the following Builder pattern code snippet that constructs a luxury house:
medium
A. get_result method returns the wrong object
B. Constructor does not initialize the House object
C. Director does not call build_roof method
D. Line with 'pass' in build_pool method lacks adding 'Pool' part

Solution

  1. Step 1: Inspect build_pool method

    The build_pool method contains only 'pass', so it does not add the 'Pool' part to the house.
  2. Step 2: Check Director.construct_luxury_house calls

    The Director calls build_pool expecting the pool to be added, but due to missing implementation, it is not.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Missing pool addition causes incomplete luxury house [OK]
Hint: Empty build_pool method -> missing part added [OK]
Common Mistakes:
  • Confusing constructor initialization
  • Ignoring missing method implementation
4. What is the time complexity of notifying observers in the thread-safe observer pattern implementation where observers are stored in a dictionary mapping event types to sets of observers, and a snapshot list is created during notification?
medium
A. O(n) where n is total number of all observers across all event types
B. O(k) where k is the number of observers subscribed to the notified event type
C. O(k + n) where k is observers for event type and n is total observers
D. O(1) constant time due to hash set usage

Solution

  1. Step 1: Identify the data structure and notification process

    Observers are stored per event type in sets. Notification locks and copies only the observers for the specific event type.
  2. Step 2: Analyze complexity of notify()

    Notify creates a snapshot list of observers for the event type (size k) and iterates over it, so time is proportional to k, not total n.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Notify cost depends only on observers for that event type [OK]
Hint: Notify complexity depends on observers for event type only [OK]
Common Mistakes:
  • Assuming notify iterates over all observers regardless of event type
5. Suppose you want to implement a prototype pattern for an object that contains references to other objects which themselves may reference back to the original object (cyclic references). Which approach correctly handles deep copying in this scenario?
hard
A. Use a deep copy implementation with memoization to track already copied objects and avoid infinite recursion
B. Use a naive recursive deep copy without memoization, which will eventually copy all objects
C. Use shallow copy to avoid recursion issues, accepting shared nested references
D. Serialize the object to JSON and deserialize it, which naturally handles cyclic references

Solution

  1. Step 1: Understand cyclic references problem

    Naive recursion without tracking copied objects causes infinite recursion on cycles.
  2. Step 2: Evaluate solutions

    Memoization tracks already copied objects, preventing infinite loops and ensuring correct deep copy. Shallow copy shares references, breaking independence. JSON serialization cannot handle cycles and will fail.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Memoization prevents infinite recursion in cyclic graphs [OK]
Hint: Memoization is essential for deep copying cyclic object graphs [OK]
Common Mistakes:
  • Ignoring cycles causes infinite recursion or stack overflow