DEV Community

Manoj Khatri
Manoj Khatri

Posted on

The Everyday Backend Engineer: Step 5 — The Decorator Pattern

Welcome back to The Everyday Backend Engineer: Practical Design Patterns. In our last post, we unlocked modular flexibility using Dependency Injection. Today, we continue exploring Category 2: Structural Patterns with a clever design approach to expanding code functionality: The Decorator Pattern.

Let’s look at how to cleanly stack extra layers of business logic onto existing objects without altering a single line of their original code.


🔴 The Problem: Class Explosion & Monolithic Bloat

Imagine you are managing an order processing system for an e-commerce platform. You have a foundational class that calculates the core price of a standard checkout order:

class BasicOrder {
    getPrice() {
        return 1000; // Base price of Rs. 1000
    }
}

Enter fullscreen mode Exit fullscreen mode

The business logic escalates...

Now, the business requirements evolve:

  1. Some orders need a 13% VAT tax added.
  2. Some orders qualify for premium, insured express shipping (+ Rs. 150).
  3. Some orders need both tax and express shipping.

If you try to handle this via traditional inheritance, you will end up creating a messy web of sub-classes: TaxedOrder, ExpressShippingOrder, TaxedAndExpressShippingOrder. This is called Class Explosion.

Alternatively, if you try to pack all this conditional logic inside the original BasicOrder class using multiple if/else flags, your simple class transforms into a fragile, unmaintainable monolith.


🟢 The Solution: Wrapper Layers (The Ice Cream Approach)

Instead of altering the original class or inheriting from it recursively, the Decorator Pattern allows you to dynamically wrap the original object inside a "Decorator" object.

The decorator class implements the same interface (exposes the exact same methods) as the target object, intercepts the execution, adds its custom behavior, and passes the result down the chain.

🍦 The Real-World Analogy: "Ice Cream Toppings"

Think of buying a scoop of simple Vanilla Ice Cream. If you want a different flavor experience, the shop doesn't melt down the vanilla scoop and rebuild a brand-new hybrid batch from scratch. Instead, they keep the core vanilla scoop exactly as it is and dynamically drop Extra Toppings on top—like chocolate chips or a cherry. The core asset stays untouched, but its structural features are upgraded.


💻 The Clean Node.js Implementation

Let’s implement this cleanly in Node.js by wrapping our base ordering logic with dynamic decorators.

First, we establish our core baseline class:

// services/basicOrder.js
class BasicOrder {
    getPrice() {
        return 1000; // Foundational price
    }
}

module.exports = BasicOrder;

Enter fullscreen mode Exit fullscreen mode

Next, we create our decorators. Each decorator accepts an instance of an order object and extends its behavior:

// decorators/orderDecorators.js

// Decorator 1: Adding Government Tax
class TaxOrderDecorator {
    constructor(orderInstance) {
        this.order = orderInstance;
    }

    getPrice() {
        const basePrice = this.order.getPrice();
        const taxAmount = basePrice * 0.13; // 13% VAT
        return basePrice + taxAmount;
    }
}

// Decorator 2: Adding Express Premium Shipping
class ExpressShippingDecorator {
    constructor(orderInstance) {
        this.order = orderInstance;
    }

    getPrice() {
        const structuralPrice = this.order.getPrice();
        const shippingFee = 150; // Extra charge for quick delivery
        return structuralPrice + shippingFee;
    }
}

module.exports = { TaxOrderDecorator, ExpressShippingDecorator };

Enter fullscreen mode Exit fullscreen mode

🎯 How to compose them dynamically on runtime

Now, look at how easily we can mix, match, and stack these structural features together at runtime depending on the user's checkout choices:

// controllers/checkoutController.js
const BasicOrder = require('../services/basicOrder');
const { TaxOrderDecorator, ExpressShippingDecorator } = require('../decorators/orderDecorators');

async function processCheckout(req, res) {
    // 1. Start with the plain base asset
    let finalOrder = new BasicOrder();
    console.log(`Base Price: Rs. ${finalOrder.getPrice()}`); // Rs. 1000

    // 2. Dynamically add Tax layer if requested
    if (req.body.applyTax) {
        finalOrder = new TaxOrderDecorator(finalOrder);
    }

    // 3. Dynamically add Premium Delivery layer if requested
    if (req.body.expressDelivery) {
        finalOrder = new ExpressShippingDecorator(finalOrder);
    }

    // 🎯 The Magic: getPrice calls execute down the chain sequentially
    const computedTotal = finalOrder.getPrice();

    console.log(`Final Calculated Checkout Cost: Rs. ${computedTotal}`);
    res.json({ total: computedTotal });
}

Enter fullscreen mode Exit fullscreen mode

What happens under the hood?

If the user checks both boxes, the structural wrapping looks like this:
ExpressShippingDecorator -> TaxOrderDecorator -> BasicOrder

When you invoke .getPrice(), the shipping wrapper asks the tax wrapper for the price, which asks the basic order class. The calculation filters back up the chain: 1000 -> 1130 -> 1280.


🧠 Code Smell Test: When should you use a Decorator?

Keep your eyes open for this anti-pattern:

  • The Smell: You look at a core service method, and it is choked with conditional flags (if (isPremium), if (isDiscounted)) whose sole purpose is to alter or inject extra secondary calculation metrics onto a baseline object.
  • The Fix: Strip those flags out of the baseline class. Spin them up into standalone decorator wrappers that encase the core target object dynamically.

That wraps up Step 5! Your codebase can now expand indefinitely with a library of interchangeable feature toppings.

Up next in our structural exploration is Step 6: The Facade Pattern, where we will look at how to hide terrifyingly complex background workflows behind a single, beautifully simple structural doorway.

Do you use explicit runtime decorator wrappers in your code, or do you prefer leveraging JavaScript/TypeScript syntactic decorators (@decorator) directly inside your class metadata? Let's talk in the comments below!

Top comments (0)