2Brute2Best
πŸ”₯0
F4

Composition Over Inheritance

High-frequency interview topicHas-A vs Is-ADelegation
⚑ 60-Second Architecture SummaryF4 · Foundation
4 Core ConceptsC++23 Verified Demo4 Rules of Thumb
Why It Matters in High-Scale Systems

The Gang of Four's primary architectural tenet is favor object composition over class inheritance. While inheritance provides compile-time code reuse, it binds derived classes tightly to base class internals (the fragile base class problem) and causes an exponential explosion of classes when features combine. Composition models relationships as pluggable collaborators (has-a), allowing components to vary independently and reconfigure at runtime.

Core Mental Model

Think of inheritance as a molded plastic unibody toy and composition as interlocking Lego bricks. In a unibody car, modifying the wheel size requires remolding the entire chassis. With Lego bricks, you snap off the standard wheels (StandardDeliveryPolicy) and snap on high-speed treads (ExpressDeliveryPolicy), or plug in a turbo engine (PercentageDiscountRule), without redesigning the car frame.

01

Why this matters in LLD interviews

The Gang of Four's primary architectural tenet is favor object composition over class inheritance. While inheritance provides compile-time code reuse, it binds derived classes tightly to base class internals (the fragile base class problem) and causes an exponential explosion of classes when features combine. Composition models relationships as pluggable collaborators (has-a), allowing components to vary independently and reconfigure at runtime.

Mental model

Think of inheritance as a molded plastic unibody toy and composition as interlocking Lego bricks. In a unibody car, modifying the wheel size requires remolding the entire chassis. With Lego bricks, you snap off the standard wheels (StandardDeliveryPolicy) and snap on high-speed treads (ExpressDeliveryPolicy), or plug in a turbo engine (PercentageDiscountRule), without redesigning the car frame.

02

Core concepts

2.1Is-A vs Has-A: When Inheritance Goes Wrong

Inheritance represents an is-a relationship (BikeDeliveryPartner is a DeliveryPartner), while composition represents a has-a relationship (FoodOrder has a PricingRule and has a DeliveryPolicy).

A common beginner mistake is subclassing to acquire behavior: making ExpressDiscountedOrder inherit from DiscountedOrder, which inherits from Order. This couples the order lifecycle to billing and logistics, creating rigid hierarchies where changes in one domain break unrelated features.

C++23 snippet
// Anti-pattern: inheritance for behavior variation
class ExpressDiscountedOrder : public DiscountedOrder { /* rigid */ };

// Clean design: composition of independent policies
class FoodOrder {
    std::unique_ptr<PricingRule> pricing_;
    std::unique_ptr<DeliveryPolicy> delivery_;
};

2.2The Combinatorial Explosion of Subclasses

Suppose your food delivery system has 3 delivery modes (Standard, Express, Midnight) and 4 pricing rules (Full Price, Flat Voucher, Percentage Coupon, Loyalty Cashback).

If you use inheritance, you need $3 Γ— 4 = 12$ distinct classes (ExpressFlatDiscountOrder, MidnightLoyaltyOrder, etc.). Adding 1 new delivery mode requires creating 4 new subclasses. With composition, you write only $3 + 4 = 7$ classes. Adding a new delivery mode requires writing exactly 1 class.

C++23 snippet
// Inheritance: M * N classes (exponential explosion)
// Composition: M + N classes (additive linear growth)

2.3Runtime Reconfigurability vs Compile-Time Rigidity

In C++, an object's dynamic type is permanently fixed upon construction. If an order is instantiated as a StandardOrder, it cannot morph its type at runtime when the customer taps 'Upgrade to Express Delivery' on the checkout screen. You would have to allocate a brand-new object and copy all fields.

With composition, swapping behavior is a simple pointer reassignment: order.setDeliveryPolicy(std::make_unique<ExpressDeliveryPolicy>()). The order retains its identity, history, and items while its delivery behavior changes seamlessly.

C++23 snippet
void setDeliveryPolicy(std::unique_ptr<DeliveryPolicy> newPolicy) noexcept {
    deliveryPolicy_ = std::move(newPolicy);
}

2.4Delegation: Forwarding Work to Collaborators

Instead of executing calculations directly, a composite object delegates responsibilities to its composed members. FoodOrder::calculateTotalPaise() asks pricingRule_ for the discount amount and deliveryPolicy_ for the shipping surcharge, then aggregates the result.

This keeps each class small, focused on a single responsibility, and trivially testable in isolation.

C++23 snippet
int64_t calculateTotalPaise() const noexcept {
    int64_t discount = pricingRule_->calculateDiscountPaise(subtotalPaise_);
    return std::max<int64_t>(0, subtotalPaise_ - discount) + deliveryPolicy_->deliveryFeePaise();
}
03

UML class structure

FoodOrder composes pluggable PricingRule and DeliveryPolicy interfaces via unique_ptr, eliminating combinatorial class explosion.

FoodOrder
Context composing pluggable policies
β—† composesPricingRuleβ—† composesDeliveryPolicy
  • - orderId_: string
  • - subtotalPaise_: int64_t
  • - pricingRule_: unique_ptr<PricingRule>
  • - deliveryPolicy_: unique_ptr<DeliveryPolicy>
  • + calculateTotalPaise(): int64_t const
  • + setPricingRule(rule): void
  • + setDeliveryPolicy(policy): void
  • + printReceipt(): void const
Β«interfaceΒ»
PricingRule
Discount contract
  • + ~PricingRule() = default
  • + calculateDiscountPaise(subtotal: int64_t): int64_t const = 0
  • + ruleName(): string const = 0
Used by:FoodOrder(β—†)FlatDiscountRule(β–³)PercentageDiscountRule(β–³)
Β«interfaceΒ»
DeliveryPolicy
Logistics & fee contract
  • + ~DeliveryPolicy() = default
  • + deliveryFeePaise(): int64_t const = 0
  • + estimatedMinutes(): int const = 0
  • + policyName(): string const = 0
Used by:FoodOrder(β—†)ExpressDeliveryPolicy(β–³)
FlatDiscountRule
β–³ implementsPricingRule
  • - discountPaise_: int64_t
  • - minSubtotalPaise_: int64_t
  • + calculateDiscountPaise(subtotal): int64_t const override
PercentageDiscountRule
β–³ implementsPricingRule
  • - percent_: int
  • - maxDiscountPaise_: int64_t
  • + calculateDiscountPaise(subtotal): int64_t const override
ExpressDeliveryPolicy
β–³ implementsDeliveryPolicy
  • + deliveryFeePaise(): int64_t const override
  • + estimatedMinutes(): int const override
Relationships (UML arrows)
FoodOrder◆──▢PricingRuleowns unique_ptr
FoodOrder◆──▢DeliveryPolicyowns unique_ptr
FlatDiscountRuleβ•Œβ•Œβ–·PricingRuleimplements
PercentageDiscountRuleβ•Œβ•Œβ–·PricingRuleimplements
ExpressDeliveryPolicyβ•Œβ•Œβ–·DeliveryPolicyimplements
04

Runnable C++23 example

What the demo shows
  1. 1.

    Component Interfaces: PricingRule and DeliveryPolicy define clean strategy contracts for pricing deductions and shipping constraints.

  2. 2.

    Composition in FoodOrder: FoodOrder exclusively owns an instance of each policy via std::unique_ptr.

  3. 3.

    Delegated Calculation: calculateTotalPaise() delegates discount and shipping computations to its composed policies without hardcoding branch logic.

  4. 4.

    Dynamic Policy Swapping: Calling setPricingRule() and setDeliveryPolicy() instantly adapts order totals at runtime when user promo codes or delivery options change.

Foundation Demo Β· demo.cpp
#include <algorithm>
#include <cstdint>
#include <format>
#include <memory>
#include <print>
#include <string>
#include <string_view>
#include <utility>

// --- Pluggable Strategy 1: Pricing Rule ---
class PricingRule {
public:
    virtual ~PricingRule() = default;
    [[nodiscard]] virtual int64_t calculateDiscountPaise(int64_t subtotalPaise) const = 0;
    [[nodiscard]] virtual std::string ruleName() const = 0;
};

class NoDiscountRule final : public PricingRule {
public:
    [[nodiscard]] int64_t calculateDiscountPaise(int64_t) const override {
        return 0;
    }
    [[nodiscard]] std::string ruleName() const override {
        return "No Discount";
    }
};

class FlatDiscountRule final : public PricingRule {
private:
    int64_t discountPaise_;
    int64_t minSubtotalPaise_;

public:
    FlatDiscountRule(int64_t discountPaise, int64_t minSubtotalPaise)
        : discountPaise_(discountPaise), minSubtotalPaise_(minSubtotalPaise) {}

    [[nodiscard]] int64_t calculateDiscountPaise(int64_t subtotalPaise) const override {
        return (subtotalPaise >= minSubtotalPaise_) ? discountPaise_ : 0;
    }

    [[nodiscard]] std::string ruleName() const override {
        return std::format("Flat Rs.{:.2f} Off (min Rs.{:.2f})",
                           static_cast<double>(discountPaise_) / 100.0,
                           static_cast<double>(minSubtotalPaise_) / 100.0);
    }
};

class PercentageDiscountRule final : public PricingRule {
private:
    int percent_;
    int64_t maxDiscountPaise_;

public:
    PercentageDiscountRule(int percent, int64_t maxDiscountPaise)
        : percent_(percent), maxDiscountPaise_(maxDiscountPaise) {}

    [[nodiscard]] int64_t calculateDiscountPaise(int64_t subtotalPaise) const override {
        int64_t raw = (subtotalPaise * percent_) / 100;
        return std::min(raw, maxDiscountPaise_);
    }

    [[nodiscard]] std::string ruleName() const override {
        return std::format("{}% Off (capped at Rs.{:.2f})",
                           percent_,
                           static_cast<double>(maxDiscountPaise_) / 100.0);
    }
};

// --- Pluggable Strategy 2: Delivery Policy ---
class DeliveryPolicy {
public:
    virtual ~DeliveryPolicy() = default;
    [[nodiscard]] virtual int64_t deliveryFeePaise() const = 0;
    [[nodiscard]] virtual int estimatedMinutes() const = 0;
    [[nodiscard]] virtual std::string policyName() const = 0;
};

class StandardDeliveryPolicy final : public DeliveryPolicy {
public:
    [[nodiscard]] int64_t deliveryFeePaise() const override { return 3'000; } // Rs.30.00
    [[nodiscard]] int estimatedMinutes() const override { return 35; }
    [[nodiscard]] std::string policyName() const override { return "Standard Delivery (35 mins)"; }
};

class ExpressDeliveryPolicy final : public DeliveryPolicy {
public:
    [[nodiscard]] int64_t deliveryFeePaise() const override { return 7'500; } // Rs.75.00
    [[nodiscard]] int estimatedMinutes() const override { return 15; }
    [[nodiscard]] std::string policyName() const override { return "Express Priority (15 mins)"; }
};

// --- Composite Context: FoodOrder HAS-A PricingRule and HAS-A DeliveryPolicy ---
class FoodOrder {
private:
    std::string orderId_;
    int64_t subtotalPaise_;
    std::unique_ptr<PricingRule> pricingRule_;
    std::unique_ptr<DeliveryPolicy> deliveryPolicy_;

public:
    FoodOrder(std::string orderId,
              int64_t subtotalPaise,
              std::unique_ptr<PricingRule> pricing,
              std::unique_ptr<DeliveryPolicy> delivery)
        : orderId_(std::move(orderId)),
          subtotalPaise_(subtotalPaise),
          pricingRule_(std::move(pricing)),
          deliveryPolicy_(std::move(delivery)) {}

    void setPricingRule(std::unique_ptr<PricingRule> newRule) noexcept {
        pricingRule_ = std::move(newRule);
    }

    void setDeliveryPolicy(std::unique_ptr<DeliveryPolicy> newPolicy) noexcept {
        deliveryPolicy_ = std::move(newPolicy);
    }

    [[nodiscard]] int64_t calculateTotalPaise() const noexcept {
        int64_t discount = pricingRule_->calculateDiscountPaise(subtotalPaise_);
        int64_t discountedSubtotal = std::max<int64_t>(0, subtotalPaise_ - discount);
        return discountedSubtotal + deliveryPolicy_->deliveryFeePaise();
    }

    void printReceipt() const {
        int64_t discount = pricingRule_->calculateDiscountPaise(subtotalPaise_);
        int64_t fee = deliveryPolicy_->deliveryFeePaise();
        int64_t total = calculateTotalPaise();

        std::println("--- Order Receipt: {} ---", orderId_);
        std::println("Subtotal:       Rs.{:.2f}", static_cast<double>(subtotalPaise_) / 100.0);
        std::println("Pricing Rule:   {} (-Rs.{:.2f})", pricingRule_->ruleName(), static_cast<double>(discount) / 100.0);
        std::println("Delivery Mode:  {} (+Rs.{:.2f})", deliveryPolicy_->policyName(), static_cast<double>(fee) / 100.0);
        std::println("Final Total:    Rs.{:.2f} (ETA: {} mins)",
                     static_cast<double>(total) / 100.0,
                     deliveryPolicy_->estimatedMinutes());
    }
};

int main() {
    FoodOrder order("ORD-902",
                    60'000, // Rs.600 subtotal
                    std::make_unique<NoDiscountRule>(),
                    std::make_unique<StandardDeliveryPolicy>());
    order.printReceipt();

    std::println("\n[Customer applies coupon 'SAVE20' and selects Express Delivery]");
    order.setPricingRule(std::make_unique<PercentageDiscountRule>(20, 10'000)); // 20% capped at Rs.100
    order.setDeliveryPolicy(std::make_unique<ExpressDeliveryPolicy>());
    order.printReceipt();

    return 0;
}
Program output (stdout)
--- Order Receipt: ORD-902 ---
Subtotal:       Rs.600.00
Pricing Rule:   No Discount (-Rs.0.00)
Delivery Mode:  Standard Delivery (35 mins) (+Rs.30.00)
Final Total:    Rs.630.00 (ETA: 35 mins)

[Customer applies coupon 'SAVE20' and selects Express Delivery]
--- Order Receipt: ORD-902 ---
Subtotal:       Rs.600.00
Pricing Rule:   20% Off (capped at Rs.100.00) (-Rs.100.00)
Delivery Mode:  Express Priority (15 mins) (+Rs.75.00)
Final Total:    Rs.575.00 (ETA: 15 mins)
06

Rules of thumb

  • Favor composition over inheritance whenever two or more independent axes of change (e.g. pricing, shipping, tax) must combine.

  • Use inheritance strictly when a genuine Liskov-substitutable is-a relationship exists and base class code reuse is free of side-effects.

  • Store pluggable strategies inside the owner using std::unique_ptr when exclusive ownership applies, or std::shared_ptr when shared across multiple contexts.

  • Expose setter methods to reconfigure composed policies at runtime instead of re-instantiating the parent object.

07

Don't make this mistake

Subclass explosion for cross-cutting features
βœ— class StandardOrder {}; class ExpressOrder : public StandardOrder {}; class DiscountedExpressOrder : public ExpressOrder {}; class TaxedDiscountedExpressOrder : public DiscountedExpressOrder {};
βœ“ class Order { std::unique_ptr<PricingPolicy> pricing_; std::unique_ptr<DeliveryPolicy> delivery_; std::unique_ptr<TaxPolicy> tax_; };

Combining N delivery options with M pricing rules and K tax regimes via inheritance requires N * M * K classes, creating an unmaintainable codebase.

Violating Liskov Substitution Principle via subclassing
βœ— class ReadOnlyOrder : public Order { public: void addItem(Item) override { throw std::runtime_error("Cannot add"); } };
βœ“ class Order { bool isFinalized_; // Reject mutations based on internal state, not an artificial subclass hierarchy. };

Inheriting simply to disable or throw on base methods violates LSP and breaks callers expecting valid base behavior.

Hardcoding concrete policy creation inside constructors
βœ— FoodOrder::FoodOrder(...) { pricingRule_ = std::make_unique<FlatDiscountRule>(50, 200); }
βœ“ FoodOrder::FoodOrder(..., std::unique_ptr<PricingRule> rule) : pricingRule_(std::move(rule)) {}

Hardcoding concrete types inside the constructor defeats the purpose of composition, coupling the context to concrete subclasses.

08

Quick interview check

09

Mark your progress

Systems Integration

Related C++ Systems & Algorithmic Foundations

Deep dive into underlying layers