Dependency Inversion in Practice

OOP · lesson 29 of 43 · 3 min read

Constructor injection in plain Java, and why it makes tests need no mock library.

Open this lesson in the learning hub

Key points

  • High level policy should not depend on low level detail. Both should depend on a small interface.
  • The interface belongs to the policy, not to the detail. SignupService declares the Clock it needs.
  • Take collaborators through the constructor and store them final. The object is then always usable.
  • Never call new on a collaborator inside the class: that is the line you cannot swap in a test.
  • A fake is often five lines and clearer than a mock: implement the interface and record what it was told.
  • This is exactly what constructor injection in Spring does. The pattern works fine with no framework at all.

Example

import java.util.ArrayList;
import java.util.List;

public class Main {
    // The policy declares the small interfaces it needs. Details implement them.
    interface Clock { String now(); }
    interface Audit { void record(String line); }

    static class SignupService {
        private final Clock clock;
        private final Audit audit;

        SignupService(Clock clock, Audit audit) {     // injected, never constructed inside
            this.clock = clock;
            this.audit = audit;
        }

        void signup(String user) { audit.record(clock.now() + "  signup " + user); }
    }

    static class RecordingAudit implements Audit {    // a test double, no mock library
        final List<String> lines = new ArrayList<>();
        @Override public void record(String line) { lines.add(line); }
    }

    public static void main(String[] args) {
        // Production wiring: this is exactly what a framework does for you.
        new SignupService(() -> "2026-01-01T09:00", System.out::println).signup("ada");

        // Test wiring: same class, fixed clock, captured output.
        RecordingAudit spy = new RecordingAudit();
        new SignupService(() -> "FIXED-TIME", spy).signup("grace");
        System.out.println("captured -> " + spy.lines);
    }
}

Depend on a small interface you own, and take it through the constructor.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the OOP course, and every lesson in it is listed on the OOP contents page.