Decorator, composite and proxy

Part of the free Low-Level Design course on LogicWiz, in the chapter: Structural patterns. The class-diagram exercises run in the app.

Week 6, Day 3: Gift wrap, eco bag, note card

"I now have ItemWithGiftWrap, ItemWithEcoBag and ItemWithGiftWrapAndEcoBag. Marketing wants a note card too."

Arjun, counting subclasses on the whiteboard


The subclass explosion

For festivals, SwiftKart lets customers add extras to an item:

  • gift wrap: 30 rupees,
  • eco bag: 10 rupees,
  • note card: 15 rupees.

Arjun's first design used inheritance: a subclass of CartItem for each extra, and for each combination of extras. With three extras there are seven combinations. With four there are fifteen. Every new extra roughly doubles the count. The note card alone adds four classes: the card by itself, and the card with each existing combination. Each subclass also repeats the price logic of the others.

The deeper problem is timing. Inheritance fixes the combination when you compile. But the customer chooses extras while the app runs, one item at a time.

Decorator: wrap, add, pass along

A decorator is an object that implements the same interface as the object it wraps. It holds a reference to that object, forwards each call to it, and adds its own behaviour before or after.

public interface Priced {
    long pricePaise();
    String describe();
}

public abstract class AddOn implements Priced {
    protected final Priced inner;

    protected AddOn(Priced inner) {
        this.inner = inner;
    }
}

public class GiftWrap extends AddOn {
    public GiftWrap(Priced inner) {
        super(inner);
    }

    public long pricePaise() { return inner.pricePaise() + 3000; }
    public String describe() { return inner.describe() + " + gift wrap"; }
}

CartItem implements Priced too. EcoBag and NoteCard look like GiftWrap. Now you build any combination at runtime:

Priced chocolates = new NoteCard(new GiftWrap(new CartItem("Dark chocolate box", 45000)));
long total = chocolates.pricePaise();   // 45000 + 3000 + 1500 = 49500
classDiagram
  class Priced {
    <<interface>>
    +pricePaise() long
  }
  Priced <|.. CartItem
  Priced <|.. AddOn
  AddOn --> Priced : wraps
  AddOn <|-- GiftWrap
  AddOn <|-- EcoBag
  AddOn <|-- NoteCard

Four extras now need four classes, not fifteen. You may know this shape from Java I/O: new BufferedInputStream(new FileInputStream(file)) adds buffering around a file stream.

A candidate in a mock interview said, "Decorator is inheritance with extra steps." It is the opposite. A decorator uses composition: it holds another object. That is why you can stack wrappers per object while the program runs. Inheritance in the diagram only gives the add-ons a shared type.

Decorators have costs:

  • Order can matter. A "10% off" decorator outside the gift wrap discounts the wrap too. Inside, it does not.
  • Type checks fail. chocolates instanceof CartItem is false, because the outer object is a NoteCard.
  • Debugging is harder. A long chain means a deep stack trace.

If extras only add a fixed price, a simpler design may be enough: a list of extras on the cart line, summed. Decorator shines when each layer changes behaviour, not only a number.

Decorators also suit services. A LoggingPaymentGateway can wrap any PaymentGateway and log every charge. A RetryingPaymentGateway can retry timeouts. Each does one job, and main stacks the ones you need.

Composite: one item or a whole tree, same call

SwiftKart's catalogue is a tree. "Breakfast" contains "Dairy" and "Bakery". "Dairy" contains "Milk", which contains products. The home page shows a product count for every category.

You could write code that asks "is this a product or a category?" at every level. A composite removes that question. A single item (a leaf) and a group implement the same interface. The group holds children of that interface and forwards calls to them.

public interface CatalogNode {
    int productCount();
}

public class Product implements CatalogNode {
    private final String sku;
    public Product(String sku) { this.sku = sku; }
    public int productCount() { return 1; }
}

public class Category implements CatalogNode {
    private final List<CatalogNode> children = new ArrayList<>();

    public void add(CatalogNode child) { children.add(child); }

    public int productCount() {
        int total = 0;
        for (CatalogNode child : children) {
            total += child.productCount();
        }
        return total;
    }
}

breakfast.productCount() walks the whole tree. The caller never checks types. Notice that add is only on Category. If Product had an add that throws, you would have the Liskov problem from Week 4 again.

Composite works for more than counting. Hiding a category hides everything under it. A "show only in-stock items" filter runs on every leaf. Each new operation is one method on the interface, written once for the leaf and once for the group.

Do not use composite for a flat list. It also fits badly when leaves and groups need very different operations. You will meet it again in the problem library, in the file system problem.

Proxy: same interface, a guard in front

Each product page calls PricingService.pricePaise(sku) thirty times. The real service calls a remote pricing system and takes 40 milliseconds per call.

A proxy is an object with the same interface as a real object, which controls access to it. Common reasons are caching, creating the real object only when needed, checking permissions, and talking to an object on another machine.

public class CachingPricingProxy implements PricingService {
    private final PricingService real;
    private final Map<String, Long> cache = new ConcurrentHashMap<>();

    public CachingPricingProxy(PricingService real) {
        this.real = real;
    }

    @Override
    public long pricePaise(String sku) {
        return cache.computeIfAbsent(sku, real::pricePaise);
    }
}

This cache never expires, so a price change never reaches the page. A real one needs a time limit or an invalidation step. An access-checking proxy has the same shape: AdminReportsProxy checks the user's role, then calls the real reports service.

A lazy proxy is another common case. A product's full photo gallery is slow to load. A GalleryProxy creates the real gallery only when the customer opens it. Most customers never do, so most galleries are never loaded. Frameworks use proxies as well. Spring, for example, can wrap your service in a proxy that starts and ends a database transaction around each call. Knowing the pattern helps you read their stack traces.

The code of a proxy and a decorator looks almost the same. The difference is intent:

Decorator Proxy
Purpose adds features controls access to the real object
Who sets it up the caller, often several stacked the wiring in main; callers do not know

💡 Tip: In an interview, name the intent. "This proxy caches prices" tells the interviewer more than the class diagram does.

📌 Summary: A decorator wraps an object with the same interface to add behaviour, avoiding a subclass per combination. A composite treats a leaf and a tree alike. A proxy keeps the interface but controls access.

What Arjun learns next

These patterns arrange objects. Next week is about behaviour: how SwiftKart chooses a delivery-fee rule while the app runs, with the strategy pattern.

Check your understanding

  1. SwiftKart offers four extras, and a customer can pick any combination. How many classes does each design need for the extras?
  2. A "10% off" decorator can wrap GiftWrap(item), or GiftWrap can wrap the discounted item. What is true?
  3. Which statement best separates a proxy from a decorator?
  4. In the catalogue composite, should Product get an add(child) method so every node has the same methods?