DEV Community

Cover image for The Inheritance Trap: How Laravel Developers Can Build More Flexible Systems
Anas Hussain
Anas Hussain

Posted on

The Inheritance Trap: How Laravel Developers Can Build More Flexible Systems

1. Hook & Problem Statement

You're building a Laravel application. You have a User model that extends Authenticatable. Everything works great. Then you need a Admin user with additional permissions, so you extend User. Then you need a PremiumUser with special features, so you extend Admin. Then you need a GuestUser that inherits some behavior, so you extend User.

Before you know it, your inheritance tree looks like this:

Authenticatable
    └── User
        ├── Admin
        │   └── SuperAdmin
        └── PremiumUser
            ├── VipUser
            └── EnterpriseUser
Enter fullscreen mode Exit fullscreen mode

Now, you need to add a Moderator that has some admin features but not all. You start thinking: "Should Moderator extend Admin? But then it gets SuperAdmin methods it shouldn't have. Should I duplicate code? Should I copy-paste the methods?"

You're stuck in the inheritance trap. Your class hierarchy has become a rigid, fragile web of dependencies. A change in User breaks SuperAdmin. Adding a new feature requires refactoring the entire tree. Your code is screaming, but you don't know how to fix it.

This is the problem with inheritance-first thinking.

Inheritance is the most overused and misunderstood tool in object-oriented programming. It's often the first solution developers reach for when they see shared behavior. But real-world applications are rarely best modeled with deep class hierarchies.

There's a better way. It's called Composition over Inheritance, and it's the secret to building flexible, maintainable, and scalable systems.


2. Why This Pattern/Concept Exists

The Problem It Solves

Inheritance creates a "is-a" relationship. A Car is a Vehicle. A PremiumUser is a User. This seems natural—until you realize that many real-world relationships aren't strictly hierarchical. They're behavioral.

Consider: a User might have the behavior of being Notifiable, Authenticatable, Billable, and Loggable. These aren't types of users; they're capabilities a user can have. Forcing these capabilities into an inheritance hierarchy creates the "diamond problem," tight coupling, and fragile base classes.

The Pain That Existed Before

Before the Gang of Four book popularized composition, developers relied heavily on inheritance:

  1. The Fragile Base Class Problem: A change in a parent class could subtly break all child classes. This made maintenance terrifying.
  2. The Diamond Problem: Multiple inheritance (in languages that allow it) created ambiguity when two parent classes had the same method.
  3. Deep Hierarchies: Classes became deeply nested, making code hard to understand and navigate.
  4. Code Duplication: When behavior didn't fit the hierarchy, developers duplicated code across branches.
  5. Tight Coupling: Child classes were tightly coupled to their parents. Swapping behavior required re-architecting the hierarchy.

Why Large Applications Need It

As applications grow, requirements change. Today's PremiumUser is tomorrow's EnterpriseUser with a different billing model. Inheritance makes change expensive. Composition makes it cheap.

Composition enables:

  • Plug-and-play behavior: Add or remove capabilities without changing the class.
  • Horizontal reuse: Share behavior across unrelated classes.
  • Testing in isolation: Test behaviors independently.
  • Evolution: Change behavior by swapping components, not rewriting hierarchies.

3. Real World Analogy

The Swiss Army Knife vs. A Modular Toolkit

Imagine you're a craftsman. You need various tools: a knife, scissors, a can opener, a screwdriver, a file.

The Inheritance Approach (The Swiss Army Knife):
You buy a single Swiss Army Knife with all tools built in. It's a "Multi-Tool" that inherits from "Knife" and "Scissors" and "Screwdriver."

  • Problem: When the scissors break, the entire knife is compromised.
  • Problem: You can't replace the scissors with a better version.
  • Problem: You can't add a new tool (like a flashlight) without redesigning the knife.
  • Problem: If you need two screwdrivers, you need two Swiss Army Knives.

The Composition Approach (The Modular Toolkit):
You buy a tool case (the container) and separate, interchangeable tools (the components):

  • A knife blade that fits in a handle
  • Scissors that snap into a handle
  • A screwdriver bit set that clicks into a driver
  • A flashlight attachment that connects to the handle

  • Benefit: Broken scissors? Replace just the scissors.

  • Benefit: Need a new tool? Add it to the case. Existing tools unchanged.

  • Benefit: Need two screwdrivers? Buy two bits. Same handle.

  • Benefit: Each tool has a single, focused responsibility.

Analogy Mapping:

  • Tool Case: Your application class (e.g., User)
  • Interchangeable Tools: Behaviors and capabilities (e.g., Notifiable, Authenticatable)
  • Handle: The common interface that makes tools compatible
  • Attaching a tool: Dependency injection (the class receives the capability)
  • Buying a new tool: Adding a new behavior without modifying existing classes

The modular toolkit is more flexible, maintainable, and extensible than the all-in-one Swiss Army Knife.


4. The Pain (Bad Design)

Let's look at a classic inheritance disaster in a Laravel context.

// Base class
class Vehicle
{
    protected string $model;
    protected int $year;

    public function getInfo(): string
    {
        return "{$this->model} ({$this->year})";
    }

    public function startEngine(): string
    {
        return "Engine started";
    }

    public function honk(): string
    {
        return "Honk!";
    }
}

// Level 1
class Car extends Vehicle
{
    protected int $numberOfDoors = 4;
    protected bool $hasSunroof = false;

    public function lockDoors(): string
    {
        return "Doors locked";
    }
}

// Level 2
class ElectricCar extends Car
{
    protected int $batteryCapacity;

    public function startEngine(): string
    {
        // Override to use electric motor
        return "Electric motor started silently";
    }

    public function charge(): string
    {
        return "Charging battery...";
    }
}

// Level 3
class TeslaModel3 extends ElectricCar
{
    public function __construct()
    {
        $this->model = "Tesla Model 3";
        $this->year = 2024;
        $this->batteryCapacity = 75;
        $this->hasSunroof = true;
        $this->numberOfDoors = 4;
    }

    public function autopilot(): string
    {
        return "Autopilot engaged";
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, your boss says: "We need a FlyingCar that can both drive and fly. And a BoatCar for amphibious operations."

// Where does FlyingCar fit? It's a Car? But it flies like a Plane.
// It can't extend both Car and Plane.
class FlyingCar // extends Car? extends Plane? extends both?
{
    // We're stuck. Multiple inheritance isn't allowed in PHP.
}

// Solution: Duplicate code?
class FlyingCar extends Car
{
    // We have to copy the fly methods from Plane
    public function takeOff(): string
    {
        return "Taking off...";
    }

    public function land(): string
    {
        return "Landing...";
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Terrible

  1. Tight Coupling: Every class is bound to its parent. Changing Vehicle affects every child.

  2. Rigidity: Adding new behavior requires modifying the hierarchy. FlyingCar can't reuse Plane code.

  3. Code Duplication: FlyingCar duplicates the takeOff and land methods from Plane.

  4. Deep Hierarchies: TeslaModel3 is 4 levels deep. Understanding it requires tracing through all parents.

  5. Violates SOLID:

    • SRP: Vehicle is responsible for engine, honking, and all vehicle logic.
    • OCP: Adding FlyingCar required creating new subclasses and duplicating code.
    • LSP: ElectricCar overrides startEngine but still works as a Car (okay in this case, but often LSP violations happen).

Why Developers Write Code Like This

We write inheritance-heavy code because:

  • It's taught as the first OOP concept in many tutorials.
  • It feels natural ("A Car is a Vehicle").
  • It seems like code reuse without extra complexity.
  • We don't realize the costs until the hierarchy grows.
  • We haven't been introduced to composition.

5. Solution Overview

Composition over Inheritance is a design principle that favors assembling objects with capabilities (composition) over creating deep class hierarchies (inheritance).

Core Idea

Instead of saying "A User is a Notifiable entity," you say "A User has a Notification capability."

Instead of building a rigid tree, you build a flexible "Lego set." Each class is a Lego brick that can be combined with others in various ways.

Main Participants

  1. The Host/Container: The main class that aggregates capabilities.
  2. The Components: Small, focused classes that provide specific behaviors.
  3. The Interfaces: Contracts that define what a component can do.
  4. The Composition: The act of injecting components into the host.

How Objects Collaborate

User (Host)
    ├── NotificationTrait (Component)
    ├── AuthenticationTrait (Component)
    └── BillingTrait (Component)
Enter fullscreen mode Exit fullscreen mode

Each component handles one aspect of behavior. The host delegates to its components.

Mental Model

Think of a class as a ship.

  • Inheritance (The Mega-Yacht): You build a single, monolithic vessel that does everything. To change anything, you must rebuild the ship.
  • Composition (A Fleet of Ships): You have a command ship, a supply ship, a medical ship, and a repair ship. They work together but are independent. Swap out the medical ship for a better one without changing the others.

Benefits

  • Flexibility: Add, remove, or swap behaviors independently.
  • Reusability: Reuse the same component across completely different classes.
  • Maintainability: Components are small, focused, and easy to understand.
  • Testability: Test each component in isolation.

Trade-offs

  • Boilerplate: You may need more classes and more delegation code.
  • Complexity: Deciding how to compose objects requires careful design.
  • Learning Curve: Developers must learn to think in terms of capabilities, not hierarchies.

6. UML Diagram

Laravel Service Container Architecture

Dependency Injection Diagram

Diagram Explanation

  1. User is the host class. It doesn't extend anything. It has capabilities.
  2. User receives its components through constructor injection.
  3. User delegates behavior to its components.
  4. Components are interfaces (contracts) that define capabilities.
  5. Concrete implementations can be swapped (e.g., EmailNotifier or SmsNotifier for NotificationService).

The host class is small, focused, and flexible. The components are reusable across different hosts.


7. Vanilla PHP Example

Let's refactor the Vehicle disaster using composition.

Before Refactoring (The Inheritance Mess)

// Complex inheritance hierarchy as shown above
class Vehicle { /* ... */ }
class Car extends Vehicle { /* ... */ }
class ElectricCar extends Car { /* ... */ }
class TeslaModel3 extends ElectricCar { /* ... */ }
// FlyingCar? BoatCar? We're stuck.
Enter fullscreen mode Exit fullscreen mode

After Refactoring (Composition)

// Step 1: Define interfaces for capabilities
interface Startable
{
    public function start(): string;
}

interface Honkable
{
    public function honk(): string;
}

interface Flyable
{
    public function takeOff(): string;
    public function land(): string;
}

interface Floatable
{
    public function sail(): string;
}

interface Autopilotable
{
    public function autopilot(): string;
}

// Step 2: Implement components (behaviors)
class CombustionEngine implements Startable
{
    public function start(): string
    {
        return "Engine started with a roar!";
    }
}

class ElectricMotor implements Startable
{
    public function start(): string
    {
        return "Electric motor started silently...";
    }
}

class AirHorn implements Honkable
{
    public function honk(): string
    {
        return "HONK HONK!";
    }
}

class Chime implements Honkable
{
    public function honk(): string
    {
        return "Beep beep!";
    }
}

class FixedWing implements Flyable
{
    public function takeOff(): string
    {
        return "Lifting off the runway...";
    }

    public function land(): string
    {
        return "Approaching runway for landing...";
    }
}

class Hull implements Floatable
{
    public function sail(): string
    {
        return "Gliding across the water...";
    }
}

class TeslaAutopilot implements Autopilotable
{
    public function autopilot(): string
    {
        return "Navigating autonomously (Tesla AI)...";
    }
}

// Step 3: The host class (composition)
class Vehicle
{
    private Startable $engine;
    private Honkable $horn;
    private array $capabilities = [];

    public function __construct(
        Startable $engine,
        Honkable $horn,
        array $capabilities = []
    ) {
        $this->engine = $engine;
        $this->horn = $horn;
        $this->capabilities = $capabilities;
    }

    public function start(): string
    {
        return $this->engine->start();
    }

    public function honk(): string
    {
        return $this->horn->honk();
    }

    public function hasCapability(string $capability): bool
    {
        return isset($this->capabilities[$capability]);
    }

    public function do(string $action): string
    {
        if (isset($this->capabilities[$action])) {
            return $this->capabilities[$action]->$action();
        }
        throw new \Exception("Capability {$action} not available");
    }
}

// Step 4: Usage (composition in action)
// Create a regular car
$car = new Vehicle(
    new CombustionEngine(),
    new AirHorn()
);
echo $car->start(); // "Engine started with a roar!"
echo $car->honk(); // "HONK HONK!"

// Create a Tesla (electric car with autopilot)
$tesla = new Vehicle(
    new ElectricMotor(),
    new Chime(),
    ['autopilot' => new TeslaAutopilot()]
);
echo $tesla->start(); // "Electric motor started silently..."
echo $tesla->honk(); // "Beep beep!"
echo $tesla->do('autopilot'); // "Navigating autonomously (Tesla AI)..."

// Create a flying car!
$flyingCar = new Vehicle(
    new CombustionEngine(),
    new AirHorn(),
    [
        'takeOff' => new FixedWing(),
        'land' => new FixedWing(),
        'sail' => new Hull()
    ]
);
echo $flyingCar->do('takeOff'); // "Lifting off the runway..."
echo $flyingCar->do('sail'); // "Gliding across the water..."
Enter fullscreen mode Exit fullscreen mode

What We Improved

  1. No Inheritance: Vehicle extends nothing. It's a container for capabilities.
  2. Plug-and-Play: Want to add flying? Add Flyable component. Want to remove it? Don't include it.
  3. Reusable Components: ElectricMotor can be used in cars, boats, or anything that needs an electric start.
  4. Testable: Test ElectricMotor without needing a Vehicle. Test Vehicle with mock components.
  5. Open/Closed: To add a new capability, create a new component and use it. No need to modify existing classes.
  6. Liskov Substitution: Any Startable component can be substituted for another. Vehicle doesn't care which one.

8. Laravel Internal Example

Laravel is built on composition. Let's look at how the framework uses this principle.

Eloquent Models (A Masterclass in Composition)

Eloquent models don't inherit much behavior. Instead, they use traits and components.

namespace Illuminate\Database\Eloquent;

class Model
{
    use HasAttributes,
        HasEvents,
        HasRelationships,
        HasTimestamps,
        HasGlobalScopes,
        HasVisibility;

    // The class doesn't do much itself.
    // It delegates to traits (composition by another name).
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • Each trait is focused on one concern.
  • You can pick and choose which traits to use in your own models.
  • Adding a new feature (e.g., SoftDeletes) is as simple as adding a trait.

Middleware Pipeline (Composition in Action)

Middleware is a perfect example of composition using the Pipeline pattern.

// Middleware are small, focused components
class Authenticate
{
    public function handle($request, $next)
    {
        if (!auth()->check()) {
            return redirect('/login');
        }
        return $next($request);
    }
}

class EnsureEmailIsVerified
{
    public function handle($request, $next)
    {
        if (!auth()->user()->hasVerifiedEmail()) {
            return redirect('/verify-email');
        }
        return $next($request);
    }
}

// The Kernel composes these components
protected $middlewareGroups = [
    'web' => [
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\Authenticate::class,
        \App\Http\Middleware\EnsureEmailIsVerified::class,
    ],
];
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • Each middleware has one job (SRP).
  • They can be composed in any order.
  • Adding a new middleware doesn't affect others (OCP).
  • They can be reused across routes.

The Notifications System

Laravel's notifications are built on composition, not inheritance.

class OrderShipped extends Notification
{
    public function via($notifiable): array
    {
        // Choose channels based on the notifiable object
        return ['mail', 'database', 'broadcast'];
    }

    public function toMail($notifiable): MailMessage
    {
        return (new MailMessage)
            ->greeting('Hello!')
            ->line('Your order has been shipped.');
    }

    public function toDatabase($notifiable): array
    {
        return ['order_id' => $this->order->id];
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • You compose the notification from different channels.
  • Each toXxx method is a component that handles one channel.
  • Channels are swappable (switch from mail to SMS without changing the notification logic).

Service Providers (Configuration Composition)

Service providers compose your application's services.

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Compose services
        $this->app->bind(NotificationChannel::class, EmailChannel::class);
        $this->app->bind(PaymentGateway::class, StripeGateway::class);
    }
}
Enter fullscreen mode Exit fullscreen mode

The application is composed from these providers. Adding a new service means adding a new provider, not changing existing ones.


9. Real Laravel Application Example

Let's build a Content Moderation System using composition.

Scenario

Your application needs to moderate user content (comments, posts, images). Moderation includes:

  • Profanity filtering
  • Spam detection
  • Image NSFW detection
  • Sentiment analysis

Different types of content need different combinations of these checks.

Implementation

Step 1: Define the Core Interface
// app/Contracts/ModeratorInterface.php
namespace App\Contracts;

interface ModeratorInterface
{
    public function moderate(string $content, array $metadata = []): array;
}
Enter fullscreen mode Exit fullscreen mode
Step 2: Create Moderator Components
// app/Moderators/ProfanityFilter.php
namespace App\Moderators;

use App\Contracts\ModeratorInterface;

class ProfanityFilter implements ModeratorInterface
{
    private array $badWords = ['damn', 'hell', 'shoot']; // Simplified

    public function moderate(string $content, array $metadata = []): array
    {
        $result = ['blocked' => false, 'reasons' => []];

        foreach ($this->badWords as $word) {
            if (stripos($content, $word) !== false) {
                $result['blocked'] = true;
                $result['reasons'][] = "Contains profanity: {$word}";
            }
        }

        return $result;
    }
}

// app/Moderators/SpamDetector.php
namespace App\Moderators;

use App\Contracts\ModeratorInterface;

class SpamDetector implements ModeratorInterface
{
    public function moderate(string $content, array $metadata = []): array
    {
        $result = ['blocked' => false, 'reasons' => []];

        // Check for spam patterns
        if (preg_match('/buy now|click here|free money/i', $content)) {
            $result['blocked'] = true;
            $result['reasons'][] = 'Contains spam keywords';
        }

        if (strpos($content, 'http') !== false && count(explode(' ', $content)) < 10) {
            $result['blocked'] = true;
            $result['reasons'][] = 'Short content with suspicious links';
        }

        return $result;
    }
}

// app/Moderators/SentimentAnalyzer.php
namespace App\Moderators;

use App\Contracts\ModeratorInterface;

class SentimentAnalyzer implements ModeratorInterface
{
    public function moderate(string $content, array $metadata = []): array
    {
        // In a real app, this would call an external API
        $sentimentScore = $this->analyzeSentiment($content);

        return [
            'blocked' => $sentimentScore < -0.8,
            'score' => $sentimentScore,
            'reasons' => $sentimentScore < -0.8 ? ['Extremely negative sentiment'] : []
        ];
    }

    private function analyzeSentiment(string $content): float
    {
        // Simplified implementation
        $positiveWords = ['great', 'awesome', 'beautiful', 'nice'];
        $negativeWords = ['terrible', 'awful', 'hate', 'worst'];

        $positiveCount = 0;
        $negativeCount = 0;

        foreach ($positiveWords as $word) {
            if (strpos($content, $word) !== false) $positiveCount++;
        }
        foreach ($negativeWords as $word) {
            if (strpos($content, $word) !== false) $negativeCount++;
        }

        return ($positiveCount - $negativeCount) / max(1, $positiveCount + $negativeCount);
    }
}
Enter fullscreen mode Exit fullscreen mode
Step 3: The Composer (Orchestrator)
// app/Services/ModerationService.php
namespace App\Services;

use App\Contracts\ModeratorInterface;
use Illuminate\Support\Collection;

class ModerationService implements ModeratorInterface
{
    private Collection $moderators;

    public function __construct(array $moderators = [])
    {
        $this->moderators = collect($moderators);
    }

    public function addModerator(ModeratorInterface $moderator): self
    {
        $this->moderators->push($moderator);
        return $this;
    }

    public function moderate(string $content, array $metadata = []): array
    {
        $results = [];
        $blocked = false;
        $reasons = [];

        foreach ($this->moderators as $moderator) {
            $result = $moderator->moderate($content, $metadata);
            $results[] = $result;

            if ($result['blocked'] ?? false) {
                $blocked = true;
                $reasons = array_merge($reasons, $result['reasons'] ?? []);
            }
        }

        return [
            'blocked' => $blocked,
            'reasons' => $reasons,
            'details' => $results
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode
Step 4: Service Provider
// app/Providers/ModerationServiceProvider.php
namespace App\Providers;

use App\Contracts\ModeratorInterface;
use App\Moderators\ProfanityFilter;
use App\Moderators\SpamDetector;
use App\Moderators\SentimentAnalyzer;
use App\Services\ModerationService;
use Illuminate\Support\ServiceProvider;

class ModerationServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(ModerationService::class, function ($app) {
            $service = new ModerationService();

            // Compose the service with all moderators
            $service->addModerator(new ProfanityFilter());
            $service->addModerator(new SpamDetector());

            // Only add sentiment analyzer for text content
            if (config('moderation.sentiment_enabled')) {
                $service->addModerator(new SentimentAnalyzer());
            }

            return $service;
        });

        // Or bind the interface to the composed service
        $this->app->bind(ModeratorInterface::class, ModerationService::class);
    }
}
Enter fullscreen mode Exit fullscreen mode
Step 5: Controller
// app/Http/Controllers/CommentController.php
namespace App\Http\Controllers;

use App\Contracts\ModeratorInterface;
use App\Models\Comment;
use Illuminate\Http\Request;

class CommentController extends Controller
{
    public function __construct(
        private readonly ModeratorInterface $moderator
    ) {}

    public function store(Request $request)
    {
        $moderationResult = $this->moderator->moderate(
            $request->input('content'),
            ['user_id' => $request->user()->id]
        );

        if ($moderationResult['blocked']) {
            return response()->json([
                'error' => 'Comment rejected',
                'reasons' => $moderationResult['reasons']
            ], 422);
        }

        $comment = Comment::create([
            'content' => $request->input('content'),
            'user_id' => $request->user()->id,
            'moderation_score' => $moderationResult['details'] ?? null
        ]);

        return response()->json($comment, 201);
    }
}
Enter fullscreen mode Exit fullscreen mode

Why Composition Wins Here

  1. Flexibility: Add or remove moderators without changing the core logic.
  2. Reusability: Use ProfanityFilter for comments, posts, and images.
  3. Testability: Test each moderator independently.
  4. Configuration: Enable/disable moderators via configuration.
  5. Extensibility: Add a new ImageNsfwDetector without affecting existing code.

10. SOLID Principles Mapping

S - Single Responsibility Principle (SRP)

With composition, each component has a single responsibility:

  • ProfanityFilter: Detects bad words only.
  • SpamDetector: Detects spam patterns only.
  • SentimentAnalyzer: Analyzes sentiment only.

The ModerationService coordinates, but it doesn't do the actual moderation.

O - Open/Closed Principle (OCP)

The system is open for extension (add new moderators) but closed for modification (the core service never changes). Adding a new ImageNsfwDetector requires zero changes to ModerationService.

L - Liskov Substitution Principle (LSP)

All moderators implement the ModeratorInterface. The ModerationService can use any moderator interchangeably. If you pass a ImageNsfwDetector where a ProfanityFilter is expected, it works.

I - Interface Segregation Principle (ISP)

The ModeratorInterface is focused and minimal. It has exactly one method: moderate(). No class is forced to implement methods it doesn't need.

D - Dependency Inversion Principle (DIP)

High-level ModerationService depends on the ModeratorInterface abstraction, not concrete implementations. Low-level moderators depend on the same abstraction.


11. Trade-offs

Benefits

  1. Flexibility: Swap components at runtime. Add new components without changing existing code.
  2. Testability: Test components in isolation. Mock components during host testing.
  3. Reusability: Use the same component across different contexts.
  4. Maintainability: Changes are localized to specific components.
  5. Understanding: Small, focused classes are easier to understand than deep hierarchies.

Costs

  1. Boilerplate: More classes. More interfaces. More documentation.
  2. Indirection: To understand what a method does, you may need to trace through several components.
  3. Overhead: The act of composing objects requires careful design decisions.
  4. Cognitive Load: Developers must learn to think in terms of capabilities, not "is-a" relationships.

When Is Complexity Justified?

Use composition when:

  • The behavior is likely to change or extend.
  • The behavior is reusable across unrelated classes.
  • The class has multiple, independent concerns.
  • You need to test behavior in isolation.

Avoid composition when:

  • The relationship is truly hierarchical and stable.
  • The class has a single, simple behavior.
  • The performance overhead of delegation is significant (rarely a factor in web applications).

12. When NOT To Use It

3 Green Flags (USE COMPOSITION)

  1. Multiple Behaviors: A class needs to implement several unrelated behaviors (e.g., Notifiable, Authenticatable, Billable).
  2. Changing Requirements: The behavior is likely to change or expand over time.
  3. Cross-Cutting Concerns: The same behavior is needed across different classes (e.g., logging, caching, validation).

3 Red Flags (USE INHERITANCE)

  1. Truly Hierarchical: The relationship is "is-a" and unlikely to change (e.g., Dog extends Animal).
  2. Simple Extension: You're adding a small behavior to a framework class (e.g., App\User extends Authenticatable). Extending the framework class is the intended use.
  3. Shared State: The behavior relies heavily on the internal state of the parent class. Composition would require exposing that state.

13. Common Mistakes

1. Choosing Composition When Inheritance Was Simpler

// BAD: Over-engineered composition for a simple problem
class Logger
{
    private $formatter;
    public function __construct(FormatterInterface $formatter) { ... }
    public function log($message) { ... }
}

// The formatter never changes. Inheritance would have been fine.
Enter fullscreen mode Exit fullscreen mode

Problem: You've added complexity where none was needed.
Fix: Use inheritance when the relationship is stable and simple.

2. Forgetting to Delegate

// BAD: Composing but not delegating
class Car
{
    private Engine $engine;

    public function start()
    {
        // Forgot to use the engine!
        return "Car started";
    }
}
Enter fullscreen mode Exit fullscreen mode

Problem: The component isn't used. It's just dead weight.
Fix: Always delegate the behavior to the composed component.

3. Exposing Internal Components

// BAD: Exposing internal components
class User
{
    public NotificationService $notifier; // Public property!

    public function sendNotification($message)
    {
        $this->notifier->send($this->email, $message);
    }
}

// Outside code can change the notifier
$user->notifier = new BadNotifier(); // Danger!
Enter fullscreen mode Exit fullscreen mode

Problem: You've broken encapsulation. External code can manipulate internal state.
Fix: Make components private. Only expose methods that use them.

4. Interface Over-Design

// BAD: Every component must implement a huge interface
interface Loggable
{
    public function logInfo(string $message);
    public function logWarning(string $message);
    public function logError(string $message);
    public function logCritical(string $message);
    public function logDebug(string $message); // 5 methods!
}
Enter fullscreen mode Exit fullscreen mode

Problem: You're violating ISP. Classes must implement methods they don't need.
Fix: Use smaller, focused interfaces. For example, LogInfo, LogError, etc.

5. "Trait Abuse"

// BAD: Using traits for everything
class User
{
    use Loggable, Notifiable, Authenticatable, Billable, \
        Cacheable, Validatable, Searchable, Exportable;
}
Enter fullscreen mode Exit fullscreen mode

Problem: The class is now a dumping ground for behavior. You've just moved the problem from inheritance to traits.
Fix: Use traits only when they provide a single, well-defined behavior. Prefer composition over traits when you need to swap implementations.


14. Frequently Asked Interview Questions

Beginner/Intermediate

  1. Q: What is composition over inheritance?
    A: A design principle that favors assembling objects with capabilities (composition) rather than creating deep class hierarchies (inheritance).

  2. Q: When would you choose composition over inheritance?
    A: When behavior is likely to change, is needed across unrelated classes, or when a class has multiple independent concerns.

  3. Q: What's the difference between "is-a" and "has-a" relationships?
    A: "Is-a" is inheritance (a Car is a Vehicle). "Has-a" is composition (a Car has an Engine).

  4. Q: Can you use composition with inheritance in the same class?
    A: Yes! They're not mutually exclusive. Use inheritance for the core "is-a" relationship and composition for additional behaviors.

  5. Q: How does Laravel's Eloquent use composition?
    A: Eloquent uses traits for specific behaviors like HasAttributes, HasTimestamps, and SoftDeletes. These are forms of composition.

Senior/Architect

  1. Q: Explain the fragility problem with deep inheritance hierarchies.
    A: The fragile base class problem occurs when a change in a parent class breaks child classes. Because the child classes depend on implementation details of the parent, even minor changes can cascade and break multiple classes.

  2. Q: How does composition help with Liskov Substitution Principle violations?
    A: With inheritance, you must ensure the child class can substitute the parent. Composition avoids this entirely because there is no substitution relationship. You're delegating to interfaces.

  3. Q: Compare and contrast traits, interfaces, and composition in PHP.
    A: Interfaces define contracts. Traits provide horizontal code reuse. Composition is the act of injecting behavior via dependencies. Interfaces + composition is the most flexible approach. Traits are convenient but can lead to the same problems as multiple inheritance.

  4. Q: How would you refactor a large inheritance hierarchy to composition?
    A: Identify the behaviors in the hierarchy. Extract each behavior into a separate class. Inject those classes as dependencies. Use interfaces to decouple the host from the components. Delegate behavior in the host.

  5. Q: How does the Strategy pattern relate to composition?
    A: The Strategy pattern is an implementation of composition. You inject a strategy (component) into a context (host), and the context delegates to the strategy. This allows the strategy to be swapped at runtime.


15. Interactive Practice Challenge

The Requirement

You're building a Document Processing System for a law firm. The system handles different types of documents:

  • Contracts: Need versioning, digital signatures, and approval workflows.
  • Invoices: Need billing, payment tracking, and tax calculations.
  • Reports: Need data aggregation, formatting, and export capabilities.

The current implementation uses inheritance and is becoming a nightmare to maintain.

The Code (POOR DESIGN)

// Base class
class Document
{
    protected string $title;
    protected string $content;
    protected array $metadata;
    protected array $history = [];

    public function save(): void
    {
        // Save to database
        \DB::table('documents')->insert([
            'title' => $this->title,
            'content' => $this->content,
            'metadata' => json_encode($this->metadata),
            'history' => json_encode($this->history),
        ]);
    }

    public function addHistory(string $entry): void
    {
        $this->history[] = ['timestamp' => now(), 'entry' => $entry];
    }
}

// Level 1
class Contract extends Document
{
    protected string $signatureStatus = 'draft';
    protected array $signatures = [];
    protected string $version = '1.0';

    public function sign(string $name, string $signature): void
    {
        $this->signatures[] = ['name' => $name, 'signature' => $signature, 'signed_at' => now()];
        $this->signatureStatus = 'signed';
        $this->addHistory("Signed by {$name}");
    }

    public function addVersion(string $version): void
    {
        $this->version = $version;
        $this->addHistory("New version: {$version}");
    }
}

// Level 2
class EmploymentContract extends Contract
{
    protected string $position;
    protected float $salary;
    protected string $startDate;

    public function calculateBenefits(): array
    {
        // Calculate based on position and salary
        return [
            'health_insurance' => $this->salary * 0.05,
            'retirement' => $this->salary * 0.08,
            'vacation_days' => $this->position === 'executive' ? 25 : 15,
        ];
    }
}

class ServiceContract extends Contract
{
    protected string $serviceType;
    protected array $deliverables;
    protected string $paymentTerms;

    public function calculateMilestones(): array
    {
        // Calculate payment milestones based on deliverables
        $milestoneAmount = 100000 / count($this->deliverables);
        $milestones = [];
        foreach ($this->deliverables as $index => $deliverable) {
            $milestones[] = [
                'deliverable' => $deliverable,
                'amount' => $milestoneAmount,
                'due_date' => now()->addDays($index * 30),
            ];
        }
        return $milestones;
    }
}

// Level 3 (More complexity!)
class IndependentContractorContract extends EmploymentContract
{
    protected string $agency;
    protected float $agencyFee;
    protected float $hourlyRate;

    public function calculateBenefits(): array
    {
        // Override: Contractors don't get benefits
        return [
            'health_insurance' => 0,
            'retirement' => 0,
            'vacation_days' => 0,
        ];
    }

    public function calculateInvoice(): array
    {
        // Calculate invoice with agency fee
        return [
            'hours' => 160,
            'rate' => $this->hourlyRate,
            'total' => $this->hourlyRate * 160,
            'agency_fee' => $this->agencyFee,
            'payout' => ($this->hourlyRate * 160) - $this->agencyFee,
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

The Challenge

Now the law firm says:

  1. "We need to add NDA documents that need a specific approval workflow."
  2. "We need to add ComplianceReport that has both report formatting AND contract approval."
  3. "We want to add document encryption capabilities to ALL documents."

The inheritance hierarchy is breaking down. NDA doesn't fit neatly anywhere. ComplianceReport needs behaviors from two different hierarchies.

Your Task: Refactor this system using composition over inheritance.

Questions to Consider

  • What behaviors are independent of the document type?
  • What interfaces would you define?
  • How would you compose a document from these behaviors?
  • How would you handle documents that need capabilities from multiple domains?
  • How would you test this system?

(We won't provide the solution—refactor this code and experience the flexibility of composition!)


16. Final Mental Model

To keep it simple, memorize these three sentences:

  • One-sentence definition: Composition over inheritance is a design principle that builds objects by assembling independent components (has-a) rather than creating deep class hierarchies (is-a).
  • One-sentence intuition: Build with Lego bricks, not with a mold. You can rearrange bricks infinitely; you're stuck with the mold forever.
  • One-sentence decision rule: If the behavior could be reused across unrelated classes or might change in the future, compose it; if the relationship is truly hierarchical and stable, inherit it.

17. Related Concepts

SOLID Principles

  • Open/Closed Principle: Composition enables extension without modification.
  • Liskov Substitution: Composition avoids LSP violations entirely.
  • Single Responsibility: Components are focused on one behavior.
  • Dependency Inversion: High-level hosts depend on abstraction components.

Design Patterns

  • Strategy Pattern: Encapsulates interchangeable algorithms via composition.
  • Decorator Pattern: Adds behavior by wrapping components.
  • Adapter Pattern: Composes to adapt interfaces.
  • Composite Pattern: Treats individual objects and compositions uniformly.
  • Bridge Pattern: Separates abstraction from implementation via composition.

Laravel Internals

  • Traits: Laravel's primary composition mechanism for models.
  • Service Providers: Compose application services.
  • Middleware: Compose request handling.
  • Pipeline: Compose processing steps.
  • Notifications: Compose notification channels.

Enterprise Patterns

  • Encapsulation: Composition is the ultimate encapsulation tool.
  • Dependency Injection: The mechanism to inject composed components.
  • Inversion of Control: The framework manages component composition.

Final Thoughts

Inheritance isn't evil. It's a tool—and like any tool, it has its place. But it's overused and often misapplied. The "is-a" relationship is much rarer than developers think.

Most of the time, you're dealing with capabilities, behaviors, and concerns. These are best modeled with composition. You can mix and match capabilities without the fragility of deep hierarchies.

The next time you start typing class Something extends AnotherThing, ask yourself: "Is this truly a hierarchy, or am I just sharing behavior? Could I compose this instead?"

Your future self—and your team—will thank you for the flexibility and maintainability.

Stop creating family trees. Start building toolkits.

Github: Composition over Inheritance

Top comments (0)