18 min read Object Architecture Lesson 08 of 12
Course 04 · Part 3 — Object Relationships & Architecture

Composition — Building Systems with Objects

Assembling small, focused components into resilient, modular architectures

We have thoroughly explored the four classical pillars of OOP: Encapsulation, Abstraction, Inheritance, and Polymorphism. Inheritance taught us the is-a relationship (e.g., Car is a Vehicle). But in production systems, most entities are not sub-types of one another; rather, they are composed of multiple collaborating components (Car has an Engine). This foundational paradigm is Composition.

A Car is Not an Engine; It Contains an Engine

Consider an automobile. It contains an engine, a battery, wheels, hydraulic brakes, and a GPS unit. We would never say:

Car is an Engine # Semantically absurd!

Instead, we recognize that Car has an Engine. Like snapping Lego bricks together, composing smaller specialized objects to construct a larger, cohesive entity is the very essence of modular engineering.

01 What is Composition?

In clear, definitive terms:

Composition is a software design approach where complex objects are constructed by assembling simpler, self-contained, and focused component objects that collaborate to achieve a larger objective.

Rather than attempting to do everything itself, the composite object delegates specific sub-tasks to its internal components.

A Car's Composition Architecture
Car (Whole)
Engine start(), rev()
Battery charge(), supply()
Brakes apply(), release()
GPS locate(), route()

02 Inheritance vs. Composition: The Decisive Distinction

Distinguishing between these two paradigms is a milestone in your evolution as an architect:

Inheritance (IS-A)

“I am a specialized version of you.”

Constructs rigid, static taxonomy hierarchies (e.g., Car is a Vehicle). Produces tight coupling between base and derived classes.

Composition (HAS-A)

“I use you to get work done.”

Holds references to independent objects (e.g., Car has an Engine). Highly flexible, swappable, and loosely coupled.

03 Composition and Delegation in Python

The core mechanism underpinning composition is Delegation: handing off the responsibility for a task to a dedicated, specialized helper object:

car_composition.py Composition & Delegation
class Engine:
    def start(self):
        print("Engine ignition active: cylinders firing.")

class Battery:
    def supply_power(self):
        print("Battery supplying 12V direct current.")

class Car:
    def __init__(self):
        # Car encapsulates instances of Engine and Battery
        self.engine = Engine()
        self.battery = Battery()

    def start(self):
        print("Car ignition requested...")
        # Delegating the actual mechanical work to internal components:
        self.battery.supply_power()
        self.engine.start()
        print("Vehicle is fully operational and road-ready!")

# Usage
my_car = Car()
my_car.start()

Notice that Car.start() does not attempt to calculate cylinder timing; it cleanly delegates the ignition procedure to self.engine.start().

04 Why "Composition Over Inheritance" Conquered the Industry

Attempting to solve every software requirement with inheritance quickly leads to the dreaded Class Explosion anti-pattern:

Application ➔ BusinessApp ➔ EcommerceApp ➔ PremiumEcommerceApp ➔ InternationalPremiumEcommerceApp...

One tweak to a root base class can inadvertently break dozens of child classes. Composition liberates systems from this trap with four decisive benefits:

1 Loose Coupling

Components remain independent and communicate strictly through public contracts, keeping implementation details private.

2 Effortless Component Swappability

Swapping an internal combustion engine for an electric motor requires zero modifications to the car's inheritance tree—just plug in the new component.

3 Frictionless Mocking & Unit Testing

In automated unit tests, you can easily inject simulated mock components instead of spinning up live external payment APIs or databases.

4 Single Responsibility Principle (SRP)

Rather than creating bloated "God Classes", responsibilities are partitioned cleanly into small, testable, and reusable units.

05 Real-World E-commerce Architecture: The Modular Order

Junior developers often fall into the trap of cramming taxes, payments, shipping, and discount algorithms directly into the Order class. Here is how composition restores clean architectural boundaries:

modular_order.py E-commerce Composition
class OrderItem:
    def __init__(self, name, price, qty):
        self.name = name
        self.price = price
        self.qty = qty

    @property
    def subtotal(self):
        return self.price * self.qty

class DiscountCalculator:
    def apply(self, total):
        return total * 0.9  # 10% promotional discount

class Order:
    def __init__(self, customer, discount_service):
        self.customer = customer
        self.items = []  # Collection of OrderItem objects
        self.discount_service = discount_service  # Injected dependency

    def add_item(self, item):
        self.items.append(item)

    def calculate_final_total(self):
        raw_total = sum(item.subtotal for item in self.items)
        # Delegating discount computation to the dedicated service
        return self.discount_service.apply(raw_total)

# Usage
discount = DiscountCalculator()
order = Order("Salim", discount)
order.add_item(OrderItem("Python Mastery Book", 50, 2))
order.add_item(OrderItem("Engineering Notebook", 10, 1))

print(f"Final Total (with discount): ${order.calculate_final_total()}")
# Output: Final Total (with discount): $99.0

06 Dependencies & A Taste of Dependency Injection (DI)

Notice an essential subtlety in our order example: Order did not construct its own discount calculator internally. It was passed in through the constructor from the outside:

Tightly Bound (Hard-Coded Dependency)
class OrderService:
    def __init__(self):
        # Hard-coded: impossible to swap or unit test cleanly!
        self.payment = StripePayment()
Dependency Injection (DI)
class OrderService:
    def __init__(self, payment_service):
        # Injected from outside: easily accepts any gateway or mock!
        self.payment = payment_service

Passing dependencies into objects rather than instantiating them internally is known as Dependency Injection, the cornerstone of modern scalable engineering.

07 The Hardware Analogy: Personal Computers

Recall Course 01 (Computer Foundation). What is a personal desktop computer made of?

Computer = CPU + RAM + NVMe Storage + Motherboard + GPU + Power Supply

A computer is not a CPU, nor is RAM a computer. A computer is an elegant Composition of distinct hardware modules. In software engineering, view your systems with the same architectural clarity: an enterprise application is a collaborating ensemble of Authentication, Storage, Database, and Notification services.

Concept Check

Verify your mastery of Lesson 08 concepts

Question 1: Which of the following relationships is best modeled using Composition?

Question 2: What is the primary motivation behind the "Composition over Inheritance" design principle?

Key Takeaways

HAS-A Relationship: Composition is formed when an object encapsulates or uses instances of other objects.
Delegation: Instead of doing everything alone, objects delegate specialized work to dedicated internal components.
Loose Coupling: Eliminates the fragile base class problem by keeping component contracts stable and swappable.
Architectural Balance: Use Inheritance where a genuine is-a taxonomy exists; use Composition everywhere else.