0

D — Dependency Inversion Principle: depend on abstractions

Intermediate5 min read·eng-01-010
interviewsolidcompare

Concept

The Dependency Inversion Principle states two things: high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

DIP is about the direction of dependencies, not just about using interfaces. The word "inversion" is deliberate: in traditional layered architectures, high-level business logic depends on low-level infrastructure (database, email, filesystem). DIP inverts this — the business logic defines what it needs through an interface, and the infrastructure implements that interface. The business logic becomes stable; the infrastructure becomes swappable.

The practical consequence: your domain and application layers should contain only interfaces (contracts). Your infrastructure layer contains the concrete implementations. The infrastructure depends on the domain; the domain depends on nothing.

DIP is often conflated with Dependency Injection (DI). They are related but different:

  • DIP is a design principle about the direction of dependencies
  • DI is a technique (constructor injection, setter injection) for supplying dependencies
  • DI implements DIP when the injected dependency is an abstraction rather than a concrete type

Depending on a concrete class is not automatically a violation. It depends on whether that class is likely to change and whether it represents a "detail" (storage technology, delivery mechanism, external vendor) or a stable domain concept. Depending on array or DateTime is fine. Depending on PDO or Stripe\ApiClient in your domain logic violates DIP.

Code Example

php
<?php
declare(strict_types=1);

// VIOLATION: High-level domain logic depends on low-level MySql implementation
final class InvoiceService
{
    // Constructor DIRECTLY instantiates infrastructure — cannot swap DB, cannot test
    private \PDO $pdo;

    public function __construct()
    {
        $this->pdo = new \PDO(
            'mysql:host=localhost;dbname=billing',
            'root',
            'password'
        );
    }

    public function getUnpaidInvoices(int $customerId): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT * FROM invoices WHERE customer_id = ? AND paid_at IS NULL'
        );
        $stmt->execute([$customerId]);
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }
}

// CORRECT: Domain defines the abstraction; infrastructure implements it

// Abstraction defined in the domain layer — belongs to the domain, not to any DB
interface InvoiceRepository
{
    /** @return Invoice[] */
    public function findUnpaidByCustomer(int $customerId): array;
    public function save(Invoice $invoice): void;
    public function findById(int $id): Invoice;
}

// High-level service depends on the abstraction, not the detail
final class InvoiceService
{
    public function __construct(
        private readonly InvoiceRepository $invoices,
        private readonly PaymentGateway    $payments,
        private readonly InvoiceMailer     $mailer,
    ) {}

    public function chargeOverdueInvoices(int $customerId): void
    {
        $unpaid = $this->invoices->findUnpaidByCustomer($customerId);

        foreach ($unpaid as $invoice) {
            if ($invoice->isOverdue()) {
                $result = $this->payments->charge($invoice);
                if ($result->succeeded()) {
                    $invoice->markAsPaid($result->chargeId);
                    $this->invoices->save($invoice);
                    $this->mailer->sendReceipt($invoice);
                }
            }
        }
    }
}

// Infrastructure layer implements the abstraction
// The DETAIL depends on the ABSTRACTION (inverted direction)
final class EloquentInvoiceRepository implements InvoiceRepository
{
    public function findUnpaidByCustomer(int $customerId): array
    {
        return InvoiceModel::where('customer_id', $customerId)
            ->whereNull('paid_at')
            ->get()
            ->map(fn($model) => Invoice::fromModel($model))
            ->all();
    }

    public function save(Invoice $invoice): void
    {
        InvoiceModel::updateOrCreate(
            ['id' => $invoice->id],
            $invoice->toArray()
        );
    }

    public function findById(int $id): Invoice
    {
        return Invoice::fromModel(InvoiceModel::findOrFail($id));
    }
}

// For tests, inject an in-memory implementation
final class InMemoryInvoiceRepository implements InvoiceRepository
{
    private array $invoices = [];

    public function findUnpaidByCustomer(int $customerId): array
    {
        return array_values(array_filter(
            $this->invoices,
            fn(Invoice $i) => $i->customerId === $customerId && $i->paidAt === null
        ));
    }

    public function save(Invoice $invoice): void
    {
        $this->invoices[$invoice->id] = $invoice;
    }

    public function findById(int $id): Invoice
    {
        return $this->invoices[$id] ?? throw new InvoiceNotFoundException($id);
    }
}

// Tests use the in-memory repository — no database required
$service = new InvoiceService(
    invoices: new InMemoryInvoiceRepository(),
    payments: new FakePaymentGateway(),
    mailer:   new NullInvoiceMailer(),
);

Interview Q&A

Q: What is the difference between Dependency Injection and Dependency Inversion?

Dependency Injection is a technique: instead of a class constructing its dependencies with new, the dependencies are supplied from outside (via constructor, setter, or method parameter). Dependency Inversion is a principle: the direction of dependency should flow from low-level to high-level, mediated by abstractions owned by the high-level module. DI implements DIP when the injected type is an interface defined by the high-level module. If you inject a concrete MySQLConnection directly, you are using DI but violating DIP. If you inject a DatabaseConnection interface, you are using DI and respecting DIP. Most PHP developers practice DI daily; DIP is the more nuanced principle behind it.


Q: Who should define the interface — the caller or the implementor?

The caller (the high-level module) should own and define the interface. This is the "inversion": normally you think "here is my database class, what interface should it expose?" DIP says "here is my domain service, what does it need? Define that as an interface." The interface represents the requirements of the caller, not the capabilities of the implementor. An InvoiceRepository interface defined in the domain layer expresses "this is what invoice persistence means to me as a business service." The EloquentInvoiceRepository in the infrastructure layer then adapts its Eloquent capabilities to satisfy that contract.


Q: How does Laravel's service container enforce DIP?

Laravel's container is a DIP enabler. You register InvoiceRepository::class bound to EloquentInvoiceRepository::class in a service provider. Every constructor that type-hints InvoiceRepository receives the concrete implementation automatically. Swapping the implementation (e.g., for a different database or a test fake) requires a single container binding change, not touching the high-level classes at all. Laravel's own codebase is DIP-compliant: Illuminate\Contracts\Cache\Repository is the abstraction; FileStore, RedisStore, DatabaseStore are the infrastructure details. You can swap the cache driver without touching a single line of application code.