Loading episodes…
0:00 0:00

Stop Modifying Your Classes: A Visual Guide to the Decorator Pattern in Java

00:00
BACK TO HOME

Stop Modifying Your Classes: A Visual Guide to the Decorator Pattern in Java

10xTeam November 14, 2025 10 min read

You’ve built a clean, simple class. It does one thing and does it well. But then, the business requirements start rolling in. “Can we add gift-wrapping?” “What about an optional extended warranty?” “We need to apply promotional discounts!”

Suddenly, your once-elegant class is bloated with boolean flags, if-else chains, and conditional logic. This violates the Open/Closed Principle, which states that software entities should be open for extension but closed for modification.

The Decorator Pattern is a structural design pattern that offers a clean solution. It allows you to attach new behaviors to objects dynamically by placing them inside special “wrapper” objects that contain the new functionality.

[!TIP] Think of it like dressing a doll. The doll is the core object. You can “decorate” it with a hat, a coat, or shoes. Each piece of clothing adds something new without changing the doll itself. You can also combine them in different orders.

The Problem: The Ever-Expanding Class

Let’s imagine we have a simple InvoiceItem class representing a product for sale.

// The initial, clean class
public class InvoiceItem {
    public String name;
    public float basePrice;

    public InvoiceItem(String name, float basePrice) {
        this.name = name;
        this.basePrice = basePrice;
    }

    public float getPrice() {
        return this.basePrice;
    }
}

Now, the business asks for gift-wrapping and discounts. A common but flawed approach is to modify the class directly.

// The WRONG way: Modifying the class directly
public class InvoiceItem {
    public String name;
    public float basePrice;

    // Feature 1: Gift Wrapping
    public boolean hasGiftWrap;
    public float giftWrapCost = 25.0f;

    // Feature 2: Discount
    public boolean hasDiscount;
    public float discountRate = 0.1f; // 10%

    public InvoiceItem(String name, float basePrice) {
        this.name = name;
        this.basePrice = basePrice;
    }

    public float getPrice() {
        float finalPrice = this.basePrice;

        if (hasGiftWrap) {
            finalPrice += giftWrapCost;
        }

        if (hasDiscount) {
            finalPrice *= (1 - discountRate);
        }
        return finalPrice;
    }
}

This quickly becomes a maintenance nightmare. What if we add insurance? Or different types of discounts? The getPrice method will become a complex mess of conditionals, and the class will be tightly coupled to every new feature.

The Solution: The Decorator Pattern

The Decorator pattern elegantly solves this by creating a set of “decorator” classes that wrap the original object.

Here’s the structure we’ll build:

classDiagram
    direction LR
    class IInvoiceItem {
        <<interface>>
        +getDetails() String
        +getPrice() float
    }
    class Product {
        +String name
        +float price
        +getDetails() String
        +getPrice() float
    }
    class BundleDecorator {
        <<abstract>>
        #IInvoiceItem wrappedItem
        +getDetails() String
        +getPrice() float
    }
    class GiftWrapDecorator {
        +getDetails() String
        +getPrice() float
    }
    class InsuranceDecorator {
        +getDetails() String
        +getPrice() float
    }
    class DiscountDecorator {
        +getDetails() String
        +getPrice() float
    }

    Product --|> IInvoiceItem
    BundleDecorator --|> IInvoiceItem
    BundleDecorator o-- "1" IInvoiceItem : wraps
    GiftWrapDecorator --|> BundleDecorator
    InsuranceDecorator --|> BundleDecorator
    DiscountDecorator --|> BundleDecorator
  1. IInvoiceItem (Component): An interface that defines the common methods for both the original object and the decorators.
  2. Product (Concrete Component): The base class we want to decorate.
  3. BundleDecorator (Abstract Decorator): An abstract class that implements the IInvoiceItem interface and holds a reference to an IInvoiceItem object. This is the “backbone” of our decorators.
  4. GiftWrapDecorator, InsuranceDecorator, DiscountDecorator (Concrete Decorators): These are the actual “wrappers.” Each one adds its own specific behavior before or after delegating the call to the wrapped object.

Step-by-Step Implementation

First, let’s set up our file structure.

decorator/
├── IInvoiceItem.java
├── Product.java
├── BundleDecorator.java
├── GiftWrapDecorator.java
├── InsuranceDecorator.java
└── DiscountDecorator.java

1. The Component Interface

This interface ensures that our decorators can be used interchangeably with the original object.

// decorator/IInvoiceItem.java
public interface IInvoiceItem {
    String getDetails();
    float getPrice();
}

2. The Concrete Component

This is our original, clean Product class.

// decorator/Product.java
public class Product implements IInvoiceItem {
    private String name;
    private float price;

    public Product(String name, float price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public String getDetails() {
        return String.format("%s (Price: $%.2f)", this.name, this.price);
    }

    @Override
    public float getPrice() {
        return this.price;
    }
}

3. The Abstract Decorator

This class acts as a base for all concrete decorators. It holds a reference to the object it wraps and delegates calls to it. Making it abstract means we don’t have to implement any logic here; it’s just a pass-through.

// decorator/BundleDecorator.java
public abstract class BundleDecorator implements IInvoiceItem {
    protected IInvoiceItem wrappedItem;

    public BundleDecorator(IInvoiceItem item) {
        this.wrappedItem = item;
    }

    @Override
    public String getDetails() {
        return wrappedItem.getDetails(); // Delegate to wrapped item
    }

    @Override
    public float getPrice() {
        return wrappedItem.getPrice(); // Delegate to wrapped item
    }
}

4. The Concrete Decorators

Here’s where the magic happens. Each decorator overrides the methods to add its own behavior.

GiftWrapDecorator: Adds a fixed cost for gift wrapping.

// decorator/GiftWrapDecorator.java
public class GiftWrapDecorator extends BundleDecorator {
    private float wrapCost;

    public GiftWrapDecorator(IInvoiceItem item, float wrapCost) {
        super(item);
        this.wrapCost = wrapCost;
    }

    @Override
    public String getDetails() {
        return super.getDetails() + String.format("\n + Gift Wrap ($%.2f)", this.wrapCost);
    }

    @Override
    public float getPrice() {
        return super.getPrice() + this.wrapCost;
    }
}

InsuranceDecorator: Adds a cost for insurance.

// decorator/InsuranceDecorator.java
public class InsuranceDecorator extends BundleDecorator {
    private float insuranceCost;

    public InsuranceDecorator(IInvoiceItem item, float insuranceCost) {
        super(item);
        this.insuranceCost = insuranceCost;
    }

    @Override
    public String getDetails() {
        return super.getDetails() + String.format("\n + Insurance ($%.2f)", this.insuranceCost);
    }

    @Override
    public float getPrice() {
        return super.getPrice() + this.insuranceCost;
    }
}

DiscountDecorator: Applies a percentage-based discount.

[!NOTE] Notice how this decorator modifies the final price by a multiplier, demonstrating how decorators can fundamentally alter the output of the wrapped object.

// decorator/DiscountDecorator.java
public class DiscountDecorator extends BundleDecorator {
    private float discountRate;

    public DiscountDecorator(IInvoiceItem item, float discountRate) {
        super(item);
        this.discountRate = discountRate;
    }

    @Override
    public String getDetails() {
        return super.getDetails() + String.format("\n - Discount (%.0f%%)", this.discountRate * 100);
    }

    @Override
    public float getPrice() {
        return super.getPrice() * (1 - this.discountRate);
    }
}

Putting It All Together

Now, we can dynamically “build” our final product by wrapping it in the decorators we need.

graph TD
    subgraph Calculation Flow
        A[Product: $1000] --> B(GiftWrapDecorator: +$25);
        B --> C(InsuranceDecorator: +$75);
        C --> D(DiscountDecorator: * 0.90);
        D --> E[Final Price: $990];
    end

Here’s the client code that demonstrates this stacking:

public class Main {
    public static void main(String[] args) {
        // Start with a base product
        IInvoiceItem laptop = new Product("Gaming Laptop", 1000.0f);

        // Now, let's decorate it!
        // 1. Add gift wrapping
        laptop = new GiftWrapDecorator(laptop, 25.0f);

        // 2. Add insurance
        laptop = new InsuranceDecorator(laptop, 75.0f);

        // 3. Apply a promotional discount
        laptop = new DiscountDecorator(laptop, 0.10f); // 10% off

        // Display the final details and price
        System.out.println("--- Final Invoice ---");
        System.out.println(laptop.getDetails());
        System.out.println("---------------------");
        System.out.printf("Total Price: $%.2f\n", laptop.getPrice());

        System.out.println("\n--- Example without discount ---");
        IInvoiceItem simpleLaptop = new Product("Work Laptop", 800.0f);
        simpleLaptop = new GiftWrapDecorator(simpleLaptop, 20.0f);
        System.out.println(simpleLaptop.getDetails());
        System.out.printf("Total Price: $%.2f\n", simpleLaptop.getPrice());
    }
}

Output:

--- Final Invoice ---
Gaming Laptop (Price: $1000.00)
 + Gift Wrap ($25.00)
 + Insurance ($75.00)
 - Discount (10%)
---------------------
Total Price: $990.00

--- Example without discount ---
Work Laptop (Price: $800.00)
 + Gift Wrap ($20.00)
Total Price: $820.00

As you can see, we can mix and match decorators as needed, creating complex objects with varied behaviors without ever touching the Product class. We have successfully extended its behavior while keeping it closed for modification.

Best Practices & Considerations

[!WARNING] Decorator order matters! Applying a percentage discount before adding a fixed fee will result in a different final price than applying it after. Always consider the chain of operations. For example, a 10% discount on a $100 item with $20 shipping is $108 (100*0.9 + 20), not $108 ((100+20)*0.9).

Deep Dive: Decorator vs. Subclassing
Why not just use inheritance? You could create a `GiftWrappedProduct` subclass. But then what if you also need insurance? You'd need a `GiftWrappedAndInsuredProduct`. What about a `DiscountedProduct`? Or a `DiscountedAndGiftWrappedProduct`? This leads to a "class explosion," where you have a huge number of subclasses to cover every possible combination of features. The Decorator pattern avoids this by allowing you to combine features dynamically at runtime.

Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?