Dependency inversion

Depend on an interface, not on a concrete class, so you can swap the database or the payment provider.

Pattern: SOLID20 min read
Quick recall3 questions
  1. 01What is the clearest sign of an interface that is too wide?

    A class implements a method it cannot do, so the method throws "not supported" or does nothing.

  2. 02Open-closed: what should a new payment method cost you?

    One new class that implements an existing interface. No edits to code that already works.

  3. 03Why prefer composition over inheritance?

    An object that holds a part can swap that part. A parent class is fixed when you write the child.

A food delivery app takes payments through Razorpay. The checkout code creates the Razorpay client itself, right where it needs it. It is short and it works.

Then two requests arrive in the same week: take international cards through Stripe, and test checkout without charging a real card. Both mean surgery on the checkout class. This lesson shows the one change that makes both requests easy.

The problem

OrderService holds the business rule: charge the customer, then confirm the order. It creates its own RazorpayGateway.

class RazorpayGateway {
    boolean charge(String orderId, int amount) {
        System.out.println("Razorpay charged " + amount + " for " + orderId);
        return true;
    }
}

class OrderService {
    private final RazorpayGateway gateway = new RazorpayGateway();

    String placeOrder(String orderId, int amount) {
        boolean paid = gateway.charge(orderId, amount);
        return paid ? "confirmed" : "payment failed";
    }
}

placeOrder("A17", 450) prints Razorpay charged 450 for A17 and returns confirmed. (The real gateway would call Razorpay's servers. Here it prints a line.)

The business rule on top points straight at one vendor's class below it.
  • A class says what the app does.
  • A class says how one detail gets done.

Here the high-level class depends on the low-level one. A detail now decides when the business rule must change.

The change requests

"Take international cards through Stripe." You must open OrderService, change the field type, and change the new. The class with the business rule changes, though the rule did not.

"Test that a failed payment shows payment failed." You cannot. OrderService always builds a real RazorpayGateway, so every test talks to Razorpay. You cannot make it say no on demand.

Python and TypeScript test tools can patch the class out of the module, for example unittest.mock.patch or jest.mock. That works, but the test has to know how OrderService is built inside. The design still fights you.

The idea

High-level code and low-level code should both depend on an interface. The high-level side owns that interface.

Both arrows now point at the interface in the middle. The vendors point up to it, not down from the business rule.
  • PaymentGateway states what checkout needs: charge an amount, say yes or no. It names no vendor.
  • OrderService uses only PaymentGateway. It never writes new RazorpayGateway().
  • RazorpayGateway, StripeGateway, FakeGateway implement it. Each one is a detail.

Why "inversion"? Before, the arrow went from the business rule down to Razorpay. Now Razorpay's arrow points up, at an interface that the business side defined. The detail depends on the rule, not the other way around.

The fix

Step 1: an interface, passed in from outside

interface PaymentGateway {
    boolean charge(String orderId, int amount);
}

class RazorpayGateway implements PaymentGateway {
    public boolean charge(String orderId, int amount) {
        System.out.println("Razorpay charged " + amount + " for " + orderId);
        return true;
    }
}

class OrderService {
    private final PaymentGateway gateway;

    OrderService(PaymentGateway gateway) {
        this.gateway = gateway;
    }

    String placeOrder(String orderId, int amount) {
        boolean paid = gateway.charge(orderId, amount);
        return paid ? "confirmed" : "payment failed";
    }
}
  • PaymentGateway is the interface from the diagram.
  • OrderService takes a gateway in its constructor and keeps it in a field. placeOrder did not change at all.
  • Handing an object its parts from outside has a name: . It gets its own lesson later in the course.

Step 2: the Stripe request again

Stripe is now one new class, plus one line at app start, the one place that picks the gateway.

class StripeGateway implements PaymentGateway {
    public boolean charge(String orderId, int amount) {
        System.out.println("Stripe charged " + amount + " for " + orderId);
        return true;
    }
}

// at app start
OrderService service = new OrderService(new StripeGateway());

OrderService does not change. placeOrder("A18", 900) prints Stripe charged 900 for A18 and returns confirmed.

Step 3: the test request again

A test passes in a . It never calls a real server, and you choose its answer.

class FakeGateway implements PaymentGateway {
    private final boolean approve;
    int calls = 0;

    FakeGateway(boolean approve) {
        this.approve = approve;
    }

    public boolean charge(String orderId, int amount) {
        calls++;
        return approve;
    }
}

// in a test
FakeGateway fake = new FakeGateway(false);
OrderService service = new OrderService(fake);
System.out.println(service.placeOrder("A19", 450)); // payment failed
System.out.println(fake.calls);                     // 1

The test checks two things: the order shows payment failed, and the service charged exactly once. It runs in milliseconds, with no network and no card.

How it runs

Checkoutcustomer taps Pay
OrderServicethe business rule
StripeGatewaya PaymentGateway
App startpicks the parts
  1. App start to StripeGateway:
    new()
  2. App start to OrderService:
    new(gateway)
  3. Checkout to OrderService:
    placeOrder(A18)
  4. OrderService to StripeGateway:
    charge(900)
  5. StripeGateway to OrderService:
    true
  6. OrderService to Checkout:
    confirmed
Read it top to bottom. App start builds the gateway and hands it in. After that, OrderService only calls charge, and never learns which vendor answered.

In a test, the same picture holds with FakeGateway in the third column and the test code in the fourth.

When to use it

Signals in the code

  • A business class writes new for something outside your code: a database, a payment provider, an email or SMS sender, the clock, an HTTP client.
  • A vendor's name sits in a business class: MySqlDatabase inside OrderService.
  • You cannot test a rule without a network, a real database or a real card.
  • Swapping one vendor means editing classes that hold business rules.

When it is too much

  • Do not hide plain values and the standard library behind interfaces. new ArrayList<>(), a Money class or a date format can stay concrete.
  • An interface with one implementation and no fake in tests may be noise. Add it when the second implementation, or the first test, needs it.

Dependency inversion or dependency injection?

Dependency inversionDependency injection
What it isa design rulea coding technique
Saysdepend on an interfacepass parts in from outside
Where it showsthe class diagramthe constructor
Works alone?needs a way to get the partscan inject concrete classes too

They work as a team. Inversion decides which way the arrows point. Injection is the usual way to hand over the part.

SOLID on one screen

This lesson closes the SOLID chapter. Here are all five side by side.

PrincipleThe ruleThe smellThe fix
Single responsibilityone reason to changeone class, many jobssplit by job
Open-closedadd code, do not edita growing if-else on typeinterface plus new class
Liskov substitutiona child keeps its parent's promisesoverrides that throw or surprisefix the hierarchy
Interface segregationuse every method you depend onstub methods that throwsmall role interfaces
Dependency inversiondepend on interfacesnew of a vendor classinterface, passed in
Remember4 points
  1. 01

    Depend on an interface, not on a concrete class. The business rule owns the interface, and the vendors implement it.

  2. 02

    The arrows invert: the detail points up at the interface, instead of the rule pointing down at the detail.

  3. 03

    Pass the implementation in from outside, through the constructor. A vendor switch becomes one new class and one line at app start.

  4. 04

    A fake that implements the same interface tests the rule with no network and no real card.

Read more

Next chapter: creational design patterns, starting with what design patterns are, and are not.

PreviousInterface segregation
Coming soonWhat design patterns are, and are not