Composition over Inheritance
Build objects from focused, composable pieces rather than extending class hierarchies.
Composition over Inheritance: A Complete Guide with Examples in 11 Programming Languages
Composition over Inheritance — often abbreviated CoI — is a software design principle that states: when you need to reuse or share behavior across types, prefer building objects from smaller, focused pieces (composition) over deriving new types from existing ones (inheritance). Instead of expressing "A is a B" through class A extends B, express "A has a B" through class A { b: B }.
The principle does not declare inheritance wrong. It declares inheritance frequently misused — applied to problems that composition solves better. Inheritance is the right tool when a genuine "is-a" relationship exists and the Liskov Substitution Principle holds. Composition is the right tool when the goal is simply to reuse behavior, combine capabilities, or vary behavior at runtime — none of which require a subtype relationship.
The cost of getting this choice wrong is real. Deep inheritance hierarchies become rigid structures where a change to a superclass ripples unpredictably through every subclass. Behavior that was reused through inheritance becomes impossible to replace or test in isolation. New combinations of behavior require new classes rather than new configurations. Composition avoids all of these problems by keeping components small, independent, and recombineable.
In this comprehensive guide, we'll explore Composition over Inheritance with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and the judgment to choose the right tool in every situation.
Table of Contents
- What is Composition over Inheritance?
- Why Favor Composition?
- Composition over Inheritance Comparison
- Composition over Inheritance in Depth
- Concept Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is Composition over Inheritance?
Composition over Inheritance is a principle first articulated explicitly in the 1994 Gang of Four book Design Patterns, where the authors wrote: "Favor object composition over class inheritance." It addresses a recurring failure mode in object-oriented design: reaching for inheritance — the most visible and syntactically convenient OOP mechanism — in situations where it creates more problems than it solves.
Inheritance establishes a permanent, compile-time relationship between a subclass and its superclass. The subclass depends on the superclass's implementation. When the superclass changes, all subclasses are affected. The subclass exposes the superclass's full interface — including parts that may not be appropriate. And if a new combination of behavior is needed that does not fit the hierarchy, a new subclass must be created.
Composition builds behavior from smaller, independent objects. A class contains references to collaborators and delegates work to them. Each collaborator is focused on one responsibility. Collaborators can be swapped, combined, and reconfigured. The containing class depends on the collaborator's interface, not its implementation — so different implementations can be substituted without any change to the containing class.
The clearest illustration is a Logger added to a service. The inheritance approach extends the service with a LoggingService subclass that overrides methods to add logging. Now logging is tightly coupled to the service's implementation. Testing the service without logging means testing a different class. Changing the logging format means changing the subclass. Adding a second cross-cutting concern (auditing) requires another subclass or multiple inheritance.
The composition approach gives the service a Logger field. It delegates logging calls to the logger. Testing the service without logging means passing a NullLogger. Changing the logging format means passing a different Logger implementation. Adding auditing means adding an Auditor field. The capabilities are independent and combineable.
Why Favor Composition?
The advantages of composition over inheritance are concrete and show up in day-to-day development:
-
Loose coupling: A class that composes a collaborator depends on the collaborator's interface, not its implementation. Changing the implementation does not affect the containing class. Inheritance creates tight coupling — the subclass depends on the superclass's internal structure and is affected by any change to it.
-
Runtime flexibility: Composed collaborators can be swapped at runtime. A
PaymentServicethat holds aPaymentGatewayreference can use Stripe in production, Braintree in staging, and aFakeGatewayin tests — without changing the service. Inheritance fixes the behavior at compile time. -
Single Responsibility: Composed components are focused on one thing. A
Logger, aCache, aValidator, and anEncryptoreach do one job. The class that composes them delegates each concern. Inheritance combines concerns in a single class hierarchy, making individual concerns harder to isolate. -
Testability: Each composed component can be tested in isolation and easily replaced in tests with a stub or fake. A class that inherits behavior from a superclass cannot be tested without the superclass's behavior — and cannot replace it.
-
Avoids the fragile base class problem: When a superclass changes, subclasses that depend on its internals break. Composed objects interact only through their public interface — an internal change in a collaborator does not affect the classes that use it.
-
Avoids deep hierarchies: Inheritance hierarchies grow as new combinations of behavior are needed. Three independent capabilities (flying, swimming, running) produce eight potential combinations — each requiring a subclass in an inheritance hierarchy, but trivially handled by composing the capabilities as needed.
-
Enables the Decorator and Strategy patterns: Both patterns, which are among the most practical design patterns, are fundamentally composition patterns. Decorators wrap components; strategies are injected components. Neither is possible with inheritance alone.
Composition over Inheritance Comparison
| Approach | Relationship | Coupling | Flexibility | Extension Mechanism |
|---|---|---|---|---|
| Composition | "has-a" | Loose (via interface) | High — swap at runtime | Add new collaborators |
| Inheritance | "is-a" | Tight (depends on superclass internals) | Low — fixed at compile time | Add new subclasses |
| Interface only | "can-do" | None | Highest | Any type can implement |
| Mixin / Trait | "also-has" | Moderate | Moderate — fixed at definition time | Mix in additional traits |
| Delegation | "uses-a" | Loose | High | Change the delegate |
Composition vs. Inheritance: Composition delegates work to collaborating objects through interfaces. Inheritance receives behavior from parent classes through the class hierarchy. The key difference is where the behavior lives and how tightly it is coupled to its users. Composition separates the behavior from its user; inheritance merges them into a hierarchy.
Composition vs. Interface: An interface alone defines a contract with no implementation. Composition uses interfaces to define how components interact — the containing class depends on an interface, and the composed object implements that interface. They work together: composition is the mechanism; interfaces are the contracts that make it flexible.
Composition vs. Mixin: Mixins add behavior to a class without forming an "is-a" relationship — they are closer to composition than inheritance. The difference is that mixins are resolved at class definition time (statically), while composition is wired at construction time (dynamically). Composition is more flexible; mixins are more concise.
Composition vs. Delegation: Delegation is composition in action — the containing class delegates a specific method call to a collaborating object. In languages with explicit delegation support (Kotlin's by keyword, for example), the compiler generates the delegation boilerplate automatically.
Composition over Inheritance in Depth
The Core Mechanics
Composition works through three elements:
Interface (or abstract type): defines what a collaborator can do — its contract. The containing class depends on this interface, never on a concrete type. This is what enables swapping.
Concrete implementations: classes that implement the interface with specific behavior. The containing class never imports or names them — it receives them from outside (via constructor injection or similar).
Injection point: where the concrete implementation enters the containing class. Usually the constructor. The caller decides which implementation to use; the class itself has no opinion.
┌──────────────────────┐ ┌─────────────────────┐
│ OrderService │ depends │ «interface» │
│ │────────▶│ PaymentGateway │
│ - gateway: Gateway │ on │ + charge(amount) │
│ + placeOrder(...) │ └─────────────────────┘
└──────────────────────┘ ▲
│ implements
┌───────────────┼───────────────┐
│ │ │
StripeGateway BraintreeGateway FakeGateway
(production) (staging) (testing)
OrderService depends on PaymentGateway — an interface. It is constructed with whichever implementation the caller chooses. It never knows which one it has. Testing, staging, and production all use the same OrderService code.
Composition Patterns Built on This Principle
Strategy Pattern: The collaborator is the algorithm. Swapping the collaborator changes the behavior. A Sorter that holds a SortStrategy uses whichever sorting algorithm was injected. Adding a new sort algorithm means adding a new SortStrategy implementation — zero changes to Sorter.
Decorator Pattern: Wrap a component to add behavior before or after its methods, without modifying it. A LoggingPaymentGateway wraps a real gateway, logs every call, and delegates to the real gateway. Stack decorators to combine logging, caching, and retry logic — all without touching the original class.
Dependency Injection: The systematic application of composition at the application level. Every component receives its collaborators through its constructor. No component creates its dependencies — they are composed at a single Composition Root. This is composition over inheritance applied as an architectural principle.
Component Model (UI frameworks): React, Flutter, SwiftUI, and Jetpack Compose all build UIs through composition. A complex widget is built by composing simpler widgets — not by subclassing a base widget. Adding behavior means wrapping a component, not extending it.
When Inheritance Still Wins
Composition over inheritance is a principle, not a rule. Inheritance is the better choice when:
- The "is-a" relationship is genuinely true and the Liskov Substitution Principle holds — a
Dogtruly is anAnimaland substitutes for it everywhere - You are implementing a framework hook:
Activity,Fragment,TestCase,Controller— the framework requires extension and provides substantial base behavior - You need a sealed hierarchy for exhaustive pattern matching — Kotlin
sealed class, Rustenum, Swiftenum with associated values - The Template Method pattern is appropriate: the superclass defines a fixed algorithm skeleton; subclasses fill in specific steps
- A shallow hierarchy (one or two levels) with a clear "is-a" relationship and no foreseeable need to combine behaviors independently
Concept Diagram
Beginner-Friendly Example
The clearest illustration: an animal locomotion system where animals can fly, swim, and/or run. With inheritance, each combination of abilities needs its own subclass — three abilities produce eight potential classes. With composition, each ability is an independent component that can be combined freely.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Protocol
# ════════════════════════════════════════════════════════════════
# WITHOUT Composition — inheritance hierarchy explodes
# ════════════════════════════════════════════════════════════════
class AnimalBase:
def __init__(self, name: str) -> None:
self.name = name
class FlyingAnimal(AnimalBase):
def move(self) -> str: return f"{self.name} flies through the air"
class SwimmingAnimal(AnimalBase):
def move(self) -> str: return f"{self.name} swims through the water"
class RunningAnimal(AnimalBase):
def move(self) -> str: return f"{self.name} runs across the ground"
# Need flying + swimming? New class. Flying + running? Another new class.
# Three abilities → 7 non-trivial subclasses. Four abilities → 15.
# And Python's multiple inheritance becomes the only escape — messy.
# ════════════════════════════════════════════════════════════════
# WITH Composition — behaviors are independent, combineable objects
# ════════════════════════════════════════════════════════════════
# ── Locomotion interface (Protocol — duck-typed in Python) ─────
class Locomotion(Protocol):
def move(self, animal_name: str) -> str: ...
# ── Concrete locomotion behaviors ──────────────────────────────
class FlyBehavior:
def move(self, animal_name: str) -> str:
return f"{animal_name} soars through the air"
class SwimBehavior:
def move(self, animal_name: str) -> str:
return f"{animal_name} glides through the water"
class RunBehavior:
def move(self, animal_name: str) -> str:
return f"{animal_name} sprints across the ground"
class FlyAndSwimBehavior:
"""Combines flying and swimming — no subclass needed."""
def move(self, animal_name: str) -> str:
return f"{animal_name} dives from air into water seamlessly"
class NoLocomotion:
def move(self, animal_name: str) -> str:
return f"{animal_name} stays perfectly still"
# ── Animal composes its locomotion behavior ────────────────────
class Animal:
"""
Animal HAS locomotion behavior — it does not inherit it.
The behavior can be any object satisfying the Locomotion protocol.
Different animals get different behaviors at construction time.
"""
def __init__(self, name: str, species: str, locomotion: Locomotion) -> None:
self.name = name
self.species = species
self._locomotion = locomotion
def move(self) -> str:
return self._locomotion.move(self.name)
def change_locomotion(self, locomotion: Locomotion) -> None:
"""
Behavior can be swapped at runtime — impossible with inheritance.
Useful for: animal that grows wings, environmental changes, etc.
"""
self._locomotion = locomotion
def describe(self) -> str:
return f"{self.name} ({self.species}): {self.move()}"
# ── Combine behaviors freely — no class explosion ──────────────
eagle = Animal("Eagle", "Aquila chrysaetos", FlyBehavior())
salmon = Animal("Salmon", "Salmo salar", SwimBehavior())
cheetah = Animal("Cheetah", "Acinonyx jubatus", RunBehavior())
duck = Animal("Duck", "Anas platyrhynchos", FlyAndSwimBehavior())
sloth = Animal("Sloth", "Bradypus tridactylus", NoLocomotion())
animals = [eagle, salmon, cheetah, duck, sloth]
print("=== Animal Locomotion (Composition) ===")
for animal in animals:
print(animal.describe())
# Runtime behavior change — eagle gets injured, can only walk
print("\n[Eagle injured — swapping behavior]")
eagle.change_locomotion(RunBehavior())
print(eagle.describe())
# New behavior without touching Animal class
class GlideAndRunBehavior:
def move(self, animal_name: str) -> str:
return f"{animal_name} glides to a landing and runs the rest"
flying_squirrel = Animal("Squirrel", "Glaucomys volans", GlideAndRunBehavior())
print(f"\nNew animal: {flying_squirrel.describe()}")
Production-Ready Example
The most instructive composition challenge in production: a notification system where different channels (email, SMS, push, Slack), retry strategies, and formatting policies must be combined independently. Inheritance would produce a class for every combination. Composition makes each concern a swappable component.
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
import time
import uuid
# ════════════════════════════════════════════════════════════════
# Domain types
# ════════════════════════════════════════════════════════════════
@dataclass(frozen=True)
class Notification:
id: str
recipient: str
subject: str
body: str
priority: str = "normal" # "high" | "normal" | "low"
created_at: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc))
@classmethod
def create(cls, recipient: str, subject: str, body: str, priority: str = "normal") -> "Notification":
return cls(id=str(uuid.uuid4())[:8], recipient=recipient,
subject=subject, body=body, priority=priority)
@dataclass
class DeliveryResult:
success: bool
channel: str
message_id: Optional[str] = None
error: Optional[str] = None
attempts: int = 1
# ════════════════════════════════════════════════════════════════
# Component interfaces — each a focused, swappable concern
# ════════════════════════════════════════════════════════════════
class NotificationChannel(ABC):
"""How a notification is delivered."""
@abstractmethod
def send(self, notification: Notification) -> DeliveryResult: ...
@property
@abstractmethod
def channel_name(self) -> str: ...
class MessageFormatter(ABC):
"""How a notification body is formatted before sending."""
@abstractmethod
def format(self, notification: Notification) -> Notification: ...
class RetryStrategy(ABC):
"""How delivery failures are retried."""
@abstractmethod
def should_retry(self, attempt: int, error: str) -> bool: ...
@abstractmethod
def wait_seconds(self, attempt: int) -> float: ...
class NotificationFilter(ABC):
"""Whether a notification should be sent at all."""
@abstractmethod
def should_send(self, notification: Notification) -> bool: ...
# ════════════════════════════════════════════════════════════════
# Concrete implementations — each focused on one responsibility
# ════════════════════════════════════════════════════════════════
# ── Channels ────────────────────────────────────────────────────
class EmailChannel(NotificationChannel):
def __init__(self, smtp_host: str, from_address: str) -> None:
self._smtp_host = smtp_host
self._from_address = from_address
@property
def channel_name(self) -> str: return "email"
def send(self, notification: Notification) -> DeliveryResult:
print(f" [Email] Sending to {notification.recipient} via {self._smtp_host}")
print(f" [Email] Subject: {notification.subject}")
# Real: smtplib.SMTP(self._smtp_host).sendmail(...)
return DeliveryResult(True, self.channel_name, f"msg_{uuid.uuid4().hex[:8]}")
class SmsChannel(NotificationChannel):
def __init__(self, api_key: str, sender_number: str) -> None:
self._api_key = api_key
self._sender_number = sender_number
@property
def channel_name(self) -> str: return "sms"
def send(self, notification: Notification) -> DeliveryResult:
print(f" [SMS] Sending to {notification.recipient} from {self._sender_number}")
# Real: twilio.messages.create(to=notification.recipient, body=notification.body)
return DeliveryResult(True, self.channel_name, f"SM{uuid.uuid4().hex[:10]}")
class SlackChannel(NotificationChannel):
def __init__(self, webhook_url: str, default_channel: str) -> None:
self._webhook_url = webhook_url
self._default_channel = default_channel
@property
def channel_name(self) -> str: return "slack"
def send(self, notification: Notification) -> DeliveryResult:
print(f" [Slack] Posting to #{self._default_channel}: {notification.subject}")
return DeliveryResult(True, self.channel_name, f"slack_{uuid.uuid4().hex[:6]}")
class NullChannel(NotificationChannel):
"""Test double — discards all notifications silently."""
@property
def channel_name(self) -> str: return "null"
def send(self, notification: Notification) -> DeliveryResult:
return DeliveryResult(True, self.channel_name, "null_msg")
# ── Formatters ──────────────────────────────────────────────────
class PlainTextFormatter(MessageFormatter):
def format(self, n: Notification) -> Notification:
return n # no transformation
class HtmlFormatter(MessageFormatter):
def format(self, n: Notification) -> Notification:
html_body = (
f"<html><body>"
f"<h2>{n.subject}</h2>"
f"<p>{n.body}</p>"
f"<small>Sent at {n.created_at.strftime('%Y-%m-%d %H:%M UTC')}</small>"
f"</body></html>"
)
return Notification(n.id, n.recipient, n.subject, html_body, n.priority, n.created_at)
class TemplateFormatter(MessageFormatter):
def __init__(self, template: str) -> None:
self._template = template # e.g. "Dear {recipient},\n\n{body}\n\nRegards"
def format(self, n: Notification) -> Notification:
body = self._template.format(recipient=n.recipient, body=n.body, subject=n.subject)
return Notification(n.id, n.recipient, n.subject, body, n.priority, n.created_at)
# ── Retry strategies ────────────────────────────────────────────
class NoRetry(RetryStrategy):
def should_retry(self, attempt: int, error: str) -> bool: return False
def wait_seconds(self, attempt: int) -> float: return 0.0
class ExponentialBackoff(RetryStrategy):
def __init__(self, max_attempts: int = 3, base_seconds: float = 1.0) -> None:
self._max_attempts = max_attempts
self._base_seconds = base_seconds
def should_retry(self, attempt: int, error: str) -> bool:
return attempt < self._max_attempts
def wait_seconds(self, attempt: int) -> float:
return self._base_seconds * (2 ** (attempt - 1))
class ImmediateRetry(RetryStrategy):
def __init__(self, max_attempts: int = 3) -> None:
self._max_attempts = max_attempts
def should_retry(self, attempt: int, error: str) -> bool:
return attempt < self._max_attempts
def wait_seconds(self, attempt: int) -> float: return 0.0
# ── Filters ─────────────────────────────────────────────────────
class AllowAllFilter(NotificationFilter):
def should_send(self, notification: Notification) -> bool: return True
class PriorityFilter(NotificationFilter):
def __init__(self, min_priority: str) -> None:
self._order = {"low": 0, "normal": 1, "high": 2}
self._min = self._order[min_priority]
def should_send(self, notification: Notification) -> bool:
return self._order.get(notification.priority, 0) >= self._min
class BusinessHoursFilter(NotificationFilter):
def __init__(self, start_hour: int = 8, end_hour: int = 18) -> None:
self._start = start_hour
self._end = end_hour
def should_send(self, notification: Notification) -> bool:
current_hour = datetime.now().hour
return self._start <= current_hour < self._end
# ════════════════════════════════════════════════════════════════
# NotificationService — composes all concerns
# ════════════════════════════════════════════════════════════════
class NotificationService:
"""
Sends notifications by composing independent, swappable components:
- channel: HOW to deliver (email, SMS, Slack, null)
- formatter: HOW to format the message body
- retry: HOW to handle failures
- filter: WHETHER to send at all
NONE of these are inherited. Each is injected and independently replaceable.
Testing requires only swapping one component — not subclassing.
Production and staging configurations differ only in which components are wired.
"""
def __init__(
self,
channel: NotificationChannel,
formatter: MessageFormatter = None,
retry: RetryStrategy = None,
filter_: NotificationFilter = None,
) -> None:
self._channel = channel
self._formatter = formatter or PlainTextFormatter()
self._retry = retry or NoRetry()
self._filter = filter_ or AllowAllFilter()
def send(self, notification: Notification) -> DeliveryResult:
if not self._filter.should_send(notification):
print(f" [Filter] Notification {notification.id} suppressed")
return DeliveryResult(False, self._channel.channel_name,
error="Suppressed by filter")
formatted = self._formatter.format(notification)
attempt = 0
while True:
attempt += 1
try:
result = self._channel.send(formatted)
result.attempts = attempt
print(f" ✓ Delivered via {self._channel.channel_name} "
f"(attempt {attempt}, id={notification.id})")
return result
except Exception as e:
error = str(e)
if not self._retry.should_retry(attempt, error):
print(f" ✗ Failed after {attempt} attempt(s): {error}")
return DeliveryResult(False, self._channel.channel_name,
error=error, attempts=attempt)
wait = self._retry.wait_seconds(attempt)
print(f" ↻ Attempt {attempt} failed, retrying in {wait:.1f}s...")
if wait > 0:
time.sleep(wait)
# ════════════════════════════════════════════════════════════════
# Composition Root — wire different configurations
# ════════════════════════════════════════════════════════════════
if __name__ == "__main__":
# ── Production: email with HTML formatting, exponential backoff ──
prod_email = NotificationService(
channel = EmailChannel("smtp.company.com", "noreply@company.com"),
formatter = HtmlFormatter(),
retry = ExponentialBackoff(max_attempts=3, base_seconds=0.1),
filter_ = AllowAllFilter(),
)
# ── Ops alerts: Slack, high-priority only, no retry ──────────
ops_slack = NotificationService(
channel = SlackChannel("https://hooks.slack.com/...", "ops-alerts"),
formatter = PlainTextFormatter(),
retry = NoRetry(),
filter_ = PriorityFilter(min_priority="high"),
)
# ── SMS: plain text, immediate retry, business hours only ────
sms_service = NotificationService(
channel = SmsChannel("twilio_key", "+15550001111"),
formatter = TemplateFormatter("ALERT: {subject}\n{body}"),
retry = ImmediateRetry(max_attempts=2),
filter_ = BusinessHoursFilter(start_hour=8, end_hour=20),
)
# ── Test: null channel (discards silently) ────────────────────
test_service = NotificationService(
channel = NullChannel(),
formatter = PlainTextFormatter(),
)
notifications = [
Notification.create("user@example.com", "Order Confirmed",
"Your order #1234 is confirmed.", "normal"),
Notification.create("#ops", "Server Down",
"web-01 is not responding.", "high"),
Notification.create("+15559998888", "Appointment Reminder",
"Your appointment is tomorrow at 2pm.", "normal"),
]
print("=" * 55)
print("Production Email Service")
print("=" * 55)
prod_email.send(notifications[0])
print("\n" + "=" * 55)
print("Ops Slack Service (high priority only)")
print("=" * 55)
ops_slack.send(notifications[0]) # suppressed — normal priority
ops_slack.send(notifications[1]) # delivered — high priority
print("\n" + "=" * 55)
print("SMS Service")
print("=" * 55)
sms_service.send(notifications[2])
print("\n" + "=" * 55)
print("Test Service (null channel)")
print("=" * 55)
test_service.send(notifications[0])
# ── Swapping one component changes exactly one behavior ───────
print("\n" + "=" * 55)
print("Demonstration: swap channel only — everything else unchanged")
print("=" * 55)
# Same formatter, retry, filter — different channel
slack_fallback = NotificationService(
channel = SlackChannel("https://hooks.slack.com/...", "notifications"),
formatter = HtmlFormatter(),
retry = ExponentialBackoff(max_attempts=3, base_seconds=0.1),
filter_ = AllowAllFilter(),
)
slack_fallback.send(notifications[0])
Real-World Use Cases
Composition over inheritance appears wherever independent capabilities need to be combined, swapped, or tested in isolation.
UI component libraries: React, Flutter, SwiftUI, and Jetpack Compose are built entirely on composition. A complex screen is assembled from small, focused components — each doing one thing. Adding a loading indicator, error boundary, or scroll behavior means wrapping an existing component, not subclassing it. The entire paradigm shift from MVC to component-based UI is a shift from inheritance to composition.
Middleware and pipeline architectures: HTTP middleware stacks (Express.js, ASP.NET middleware, Django middleware) are composed chains of independent handlers. Each middleware handles one concern — authentication, logging, rate limiting, compression. They can be combined in any order without any knowing about the others. This is composition applied to request processing.
Decorator Pattern for cross-cutting concerns: Logging, caching, retry, metrics, and circuit breaking are all added by wrapping the real implementation in a decorator. CachedUserRepository wraps DatabaseUserRepository. RetryingPaymentGateway wraps StripePaymentGateway. Each concern stays in its own class; they combine without inheritance.
Plugin and extension systems: Plugin architectures use composition at the architectural level. The host application composes plugins at startup. Each plugin implements a known interface and is injected into the host. Adding a new plugin does not require modifying the host — the Composition Root is extended, not the host's class hierarchy.
Game entity-component systems: Modern game engines (Unity, Unreal, Godot) abandoned deep inheritance hierarchies for entity-component systems (ECS). A game object is a bare entity with an ID. Behavior is added by attaching components — PhysicsComponent, RenderComponent, AudioComponent, AIComponent. Any combination of components produces a new type of game object without any subclassing. Adding a behavior means writing a new component, not modifying a class hierarchy.
Strategy Pattern for algorithms: Sorting, pricing, routing, compression, authentication — all can be encapsulated as swappable strategy objects. The class that uses them holds a strategy interface reference and never knows the concrete type. Changing the algorithm means changing the injected strategy.
Test doubles: The practical benefit most developers feel first. Composition makes testing trivial — inject a FakeEmailSender, a InMemoryDatabase, a MockPaymentGateway. Inheritance-based designs require subclassing to override behavior in tests, which means tests are testing a different code path than production.
Language-Specific Mistakes and Anti-Patterns
❌ Mistake #1: Inheriting for Logging or Auditing (Cross-Cutting Concern)
# BAD: Inheritance used to add logging — tightly couples two unrelated concerns
class OrderService:
def place_order(self, order_id: str) -> None:
print(f"Placing order {order_id}")
class LoggingOrderService(OrderService):
def place_order(self, order_id: str) -> None:
print(f"[LOG] place_order called with {order_id}")
super().place_order(order_id)
print(f"[LOG] place_order completed")
# Problems:
# - LoggingOrderService and OrderService are permanently coupled
# - Testing OrderService without logging means testing a different class
# - Adding auditing requires a third level, or multiple inheritance
# GOOD: Compose logging as an independent collaborator
class Logger(Protocol):
def log(self, msg: str) -> None: ...
class ConsoleLogger:
def log(self, msg: str) -> None: print(f"[LOG] {msg}")
class NullLogger:
def log(self, msg: str) -> None: pass # silent in tests
class OrderService:
def __init__(self, logger: Logger) -> None:
self._logger = logger
def place_order(self, order_id: str) -> None:
self._logger.log(f"place_order called with {order_id}")
print(f"Placing order {order_id}")
self._logger.log(f"place_order completed")
# Tests: OrderService(NullLogger()) — clean, no logging noise
# Prod: OrderService(ConsoleLogger()) or OrderService(StructuredLogger())
❌ Mistake #2: Mixin Chains That Are Hard to Track
# BAD: Deep mixin chains create invisible behavior — hard to debug
class TimestampMixin:
def save(self): super().save(); print("Timestamped")
class AuditMixin:
def save(self): super().save(); print("Audited")
class ValidationMixin:
def save(self): super().save(); print("Validated")
class CacheMixin:
def save(self): super().save(); print("Cached")
class UserRepository(CacheMixin, ValidationMixin, AuditMixin, TimestampMixin):
def save(self): print("Saved to DB")
# Which save() runs when? What is the order? Good luck debugging.
# GOOD: Explicit decorators — each wrapper is clearly visible
class UserRepository:
def save(self, user): print(f"Saved {user} to DB")
class ValidatingRepository:
def __init__(self, inner): self._inner = inner
def save(self, user): print("Validating..."); self._inner.save(user)
class AuditingRepository:
def __init__(self, inner): self._inner = inner
def save(self, user): self._inner.save(user); print("Audited")
# Explicit composition — no MRO mystery
repo = AuditingRepository(ValidatingRepository(UserRepository()))
repo.save("Alice") # Output order is obvious from the wrapper structure
Frequently Asked Questions
Q1: Does "favor composition over inheritance" mean never use inheritance?
No. The principle is "favor" — a preference in ambiguous cases, not an absolute prohibition. Inheritance is the right tool when the "is-a" relationship is genuinely true and the Liskov Substitution Principle holds: a Dog truly is an Animal; a SavingsAccount truly is a BankAccount; a Circle truly is a Shape.
The principle is a corrective against overuse. Inheritance is syntactically convenient and immediately visible — it is easy to reach for it when the goal is simply code reuse. The principle reminds you to ask: is this genuinely "is-a", or do I just want to reuse some methods? If the latter, composition will serve better.
Q2: What are the concrete signs that I should use composition instead of inheritance?
Inheritance is being misused when: the subclass only uses some of the superclass's methods (it inherited more than it should); the subclass exposes methods that don't make sense for it (violating LSP); the only reason for the inheritance is code reuse, not a type relationship; you need to swap the "inherited" behavior at runtime; the hierarchy is growing because new combinations of behavior are needed; testing requires either a running database, a live API, or subclassing to override behavior; or you find yourself writing throw new UnsupportedOperationException() in a subclass method.
Q3: How does composition interact with Dependency Injection?
They are the same principle applied at different scales. Composition over inheritance says: build objects from smaller pieces rather than deriving from parent classes. Dependency Injection says: receive your composed pieces from outside rather than creating them yourself. DI is the systematic application of composition — it ensures that every component receives its collaborators through construction rather than creating them, which makes the composition flexible and testable.
Every class that takes its collaborators through its constructor is practicing both composition (it HAS collaborators rather than IS a subclass) and dependency injection (it receives collaborators rather than creating them).
Q4: What is the relationship between composition and the SOLID principles?
They are deeply aligned. Single Responsibility: composition naturally produces small, focused classes — each collaborator has one responsibility. Open/Closed: adding new behavior means adding a new implementation of a collaborator interface — zero changes to the containing class. Liskov Substitution: since composition uses interfaces rather than inheritance, LSP violations through inheritance cannot occur in composed relationships. Interface Segregation: composed collaborators define focused interfaces — clients depend only on the specific interface they use. Dependency Inversion: composition depends on abstractions (interfaces); the concrete implementation is injected — exactly what DIP prescribes.
Composition is the mechanism that makes SOLID principles structurally enforced rather than just guidelines.
Q5: Isn't composition more verbose? I need to write more code.
At the call site (where you construct objects and wire them together), composition requires more explicit wiring than inheritance. This is a feature, not a bug. Explicit wiring makes dependencies visible. Hidden dependencies in inheritance hierarchies are a maintenance problem.
In practice, the verbosity is modest and constrained to the Composition Root — the place where objects are wired together. A DI container (Spring, Dagger, Hilt, .NET DI, etc.) eliminates most of the wiring boilerplate entirely. Inside classes, composition is not more verbose than inheritance — and it is often less verbose because the class is smaller and more focused.
The verbosity argument also ignores the long-term cost difference: deeply inherited code is more verbose to understand, debug, and change than composed code.
Q6: How do I handle shared state across composed components?
Composed components should not share state — that would couple them through data rather than through interfaces. Each component owns its own state and exposes it only through its public interface.
When state genuinely needs to be shared across multiple components (a shared cache, a session, a unit-of-work transaction), the shared state is a separate component that is injected into all the components that need it. A UserRepository and an OrderRepository both need the same database connection — inject a shared ConnectionPool into both.
Q7: What is the Decorator Pattern and how does it relate to composition?
The Decorator Pattern is composition applied specifically to wrapping behavior. A Decorator implements the same interface as the class it wraps, receives an instance of that class, and adds behavior before or after calling through to the wrapped instance.
LoggingUserRepository implements UserRepository, holds a real UserRepository, and logs every call before delegating. CachingPaymentGateway implements PaymentGateway, holds a real gateway, and checks a cache before calling the real gateway. These decorators can be stacked freely: CachingUserRepository(LoggingUserRepository(DatabaseUserRepository())).
The key point: every decorator is independent. Adding a new cross-cutting concern means adding a new decorator class — no changes to any existing class.
Q8: Should I always inject every dependency through the constructor?
Constructor injection is the default and the most recommended form for required dependencies — it makes all dependencies explicit and ensures the object is fully constructed and valid from the moment it exists.
Setter or property injection is appropriate for optional dependencies with a sensible default (a Logger that defaults to a NullLogger). Method injection (passing the dependency as a parameter to a specific method) is appropriate when the dependency is specific to one call and varies call by call — a SortStrategy passed to a sort() method, for example.
Avoid service locator (the class calls a global registry to fetch its dependencies) — this hides dependencies and is essentially a global variable in disguise.
Q9: How does composition over inheritance apply to functional programming?
Functional programming applies the same principle through function composition rather than object composition. Instead of objects that hold other objects, functions are composed by passing them as arguments or combining them into pipelines.
A function that takes a formatter function, a retry function, and a filter function and returns a composed notification-sending function is the functional equivalent of the NotificationService that composes a Formatter, RetryStrategy, and Filter. The principle is the same — build complex behavior from small, independent pieces rather than inheriting it from a monolithic parent.
Higher-order functions, middleware pipelines, and function currying are all functional composition patterns that parallel the OOP composition patterns.
Q10: When is the right time to refactor from inheritance to composition?
There are clear triggers: the inheritance hierarchy has grown beyond two or three levels; new combinations of behavior require new classes rather than new configurations; testing requires running real infrastructure that the inherited code depends on; a superclass change broke subclasses unexpectedly; you need to share behavior from two unrelated hierarchies (multiple inheritance territory); or a subclass overrides a method in a way that violates the superclass contract.
The refactoring approach: identify the behavior being inherited; extract it into a focused interface and one or more implementations; replace the superclass reference with an interface field; inject the implementation through the constructor. The result is a class that has the same capabilities but does not depend on a superclass — it is smaller, more testable, and more flexible.
Key Takeaways
🎯 Core Concept: Composition over Inheritance is the principle that behavior should be assembled from small, focused, independently-swappable components rather than derived from parent class hierarchies. Instead of class A extends B (A is a B), write class A { b: B } (A has a B). The class holds a reference to a collaborator through an interface, delegates the specific work to it, and never knows the concrete type. The caller decides which implementation to wire. Different callers — production, staging, testing — wire different implementations to the same class, achieving flexibility that inheritance cannot provide.
🔑 Key Benefits:
- Loose coupling: The class depends on a collaborator interface, not an implementation — changing the implementation never affects the class
- Runtime flexibility: Swap collaborators at runtime — the same class can use Stripe in production, a fake in tests, and Braintree in staging
- Avoids the class explosion problem: Three independent capabilities produce 7 subclasses in an inheritance hierarchy, but only 3 behavior classes + composition in a composition system
- Testability: Each component is tested in isolation; swapping a real implementation for a fake in tests is one constructor argument
- Avoids the fragile base class problem: Composed objects interact only through interfaces — internal changes in a collaborator never break the classes that use it
- Single Responsibility: Each composed component does one thing; the class itself coordinates but does not implement multiple concerns
🌐 Language-Specific Highlights:
- Python: Use
Protocol(structural typing) for composition interfaces — no explicitimplementsneeded; dependency injection through__init__parameters; closures and functions as lightweight behavior implementations; avoid mixin chains deeper than one level - TypeScript:
interfacefor all composition contracts; object literals{ method: () => ... }as lightweight implementations;readonlyon composed fields to prevent reassignment; avoidabstract classwhereinterface + injectionsuffices - Java:
interfacefor composition contracts; constructor injection as the default; Java 8+ lambdas and method references as single-method interface implementations; avoid inheritance for repository boilerplate — inject aDataSourceorEntityManagerinstead - JavaScript: Object literals as behavior implementations — no class required for simple strategies; private class fields (
#field) protect internal collaborators; avoid prototype mutation as a substitute for composition - C#:
interfacefor contracts; primary constructors (C# 12) reduce injection boilerplate; avoid inheritingList<T>orDictionary<K,V>— compose them;Func<T>andAction<T>as lightweight single-method implementations - PHP: Named constructor arguments with interface type hints; avoid trait soup (more than 2–3 traits in one class) — replace with injected collaborators; closures wrapped in an implementing class as lightweight behaviors
- Go: Interfaces at the point of use (consumer package); struct fields for composed interfaces — not embedded structs for behavior sharing; function types implementing interfaces for lightweight behaviors; return interface types, accept interface types
- Rust: Traits for composition interfaces; generic type parameters bounded by traits for zero-cost composition;
Box<dyn Trait>for runtime polymorphism; avoid monolithic structs — break concerns into separate trait-implementing structs - Dart: Abstract classes or
typedeffor composition interfaces;..cascade notation for fluent wiring; avoidextendswhenimplements+ injection is sufficient; function typedefs as lightweight single-method interfaces - Swift: Protocols for composition interfaces;
any Protocol(Swift 5.7+) for existential types;structfor stateless behavior implementations (value semantics, no heap); avoid protocol default implementations for behavior that should be composable - Kotlin:
fun interface(SAM) for lambda-compatible single-method interfaces;bykeyword for automatic delegation — eliminates forwarding boilerplate;objectdeclarations for stateless singleton behaviors;data classfor value-type behavior configurations
📋 The Composition Decision Checklist: Use composition when you answer "yes" to any of these:
- Do I want to reuse behavior without asserting "A is a B"?
- Do I need to swap this behavior at runtime or in tests?
- Would testing this class require a real database, API, or external service?
- Is the hierarchy growing because new combinations of behavior are needed?
- Does the subclass expose methods from the superclass that don't make sense for it?
- Would inheriting here couple two unrelated concerns (logging, validation, domain logic)?
Use inheritance when you answer "yes" to all of these:
- Is "A is a B" genuinely true (not just "A reuses B's code")?
- Can an A substitute for a B everywhere B is used (LSP holds)?
- Is the hierarchy shallow (1–2 levels in application code)?
- Do you need the Template Method pattern — a fixed skeleton with overridable steps?
⚠️ Anti-Patterns to Avoid:
- Inheriting from concrete classes to reuse methods (use composition + interface instead)
- Trait/Mixin soup — more than 2–3 traits mixed into one class (extract to injected collaborators)
- Inheriting collection types (
List,ArrayList,Array) to name domain collections (compose the collection, expose a focused interface) - Inheriting to add cross-cutting concerns: logging, auditing, caching, metrics (use Decorator pattern instead)
- Prototype mutation (JavaScript) or static helper assignment as disguised composition (use proper injection)
- Manual delegation boilerplate in Kotlin (use the
bykeyword) - Monolithic structs in Rust with all behavior baked in (decompose into trait-implementing collaborators)
🛠️ Best Practices Across Languages:
- Depend on interfaces, not implementations: The class should import the interface type only — never the concrete class it will receive
- Inject through constructors: Required collaborators belong in the constructor — optional collaborators with sensible defaults can be setter-injected or defaulted
- Keep behavior components stateless where possible: Stateless behaviors can be singletons (Kotlin
object, Python module-level instances) — no allocation overhead per use - Name interfaces by capability, not by implementation:
PaymentGatewaynotStripeGateway;LoggernotConsoleLogger;FormatternotHtmlFormatter - Wire at the Composition Root: All concrete implementations are named and wired in one place (main function, DI container, test setup) — the rest of the code knows only interfaces
- Test with fakes, not mocks: Compose test doubles as real implementations of the interface that behave predictably — prefer
FakeEmailSenderover mock frameworks when the fake is simple
💡 Composition Patterns Built on This Principle:
- Strategy: Swappable algorithm as a composed behavior object — sort strategy, pricing strategy, routing strategy
- Decorator: Wrapping to add behavior — logging decorator, caching decorator, retry decorator, all implementing the same interface as what they wrap
- Dependency Injection: Systematic composition at the application level — every component receives all collaborators through its constructor from a central Composition Root
- Component Model (UI): React, Flutter, SwiftUI, Jetpack Compose — complex UIs assembled from small, independent components rather than subclassing base widget classes
- Entity-Component System (Games): Game objects composed from behavior components rather than placed in a class hierarchy
- Plugin Architecture: Host application composes plugins at startup — each plugin implements a known interface, none know about each other
The shift from inheritance to composition is a shift in how you think about code reuse. Instead of asking "what can I extend?", ask "what can I inject?" Instead of building taller hierarchies, build smaller components and wire them together. The result is code that is easier to test, easier to change, and easier to understand — because every dependency is explicit, every concern is focused, and every combination is visible in the Composition Root.
This guide pairs directly with:
- Inheritance — understanding when "is-a" genuinely applies
- Dependency Injection — the systematic application of composition at the architectural level
- Strategy Pattern — composition applied to swappable algorithms
- Decorator Pattern — composition applied to layering cross-cutting behavior
- SOLID Principles — the theoretical foundation that composition naturally satisfies
Each guide includes examples in all 11 programming languages with language-specific best practices.