Design Patterns

85. Dependency Injection Basics

Dependency Injection (DI) supplies objects with their collaborators from outside rather than creating them internally โ€” making code testable, loosely coupled, and easy to reconfigure; constructor injection is the preferred form.

Java 21 (core language)

Explanation

Without DI: new OrderService() calls new PaymentGateway() inside โ€” tightly coupled, untestable without the real gateway. With DI: new OrderService(paymentGateway) โ€” the caller supplies the collaborator. In tests, you supply a mock/fake; in production, the real implementation.

Constructor injection (preferred): dependencies passed as constructor parameters, stored in final fields. The class is fully initialised by the constructor, dependencies are explicit, and the class is easily tested with a mock. This is what frameworks like Spring and Quarkus do under the hood.

Field injection (avoid): @Autowired private PaymentGateway gw; โ€” hidden dependency, untestable without the DI container, and the field cannot be final. Anti-pattern in test-driven code.

Method injection: dependency passed to a specific method rather than the constructor โ€” useful for optional or per-call dependencies.

Example

import java.util.List;
import java.util.UUID;

public class DependencyInjection {
    // Interfaces โ€” the dependency contracts
    interface PaymentGateway { boolean charge(String userId, double amount); }
    interface NotificationService { void notify(String userId, String message); }

    // Implementations
    static class StripeGateway implements PaymentGateway {
        @Override
        public boolean charge(String userId, double amount) {
            System.out.printf("Stripe: charging %s $%.2f%n", userId, amount);
            return true; // simulate success
        }
    }

    static class EmailNotifier implements NotificationService {
        @Override
        public void notify(String userId, String message) {
            System.out.println("Email to " + userId + ": " + message);
        }
    }

    // DI via constructor injection: dependencies are explicit and final
    static class OrderService {
        private final PaymentGateway    payment;
        private final NotificationService notifier;

        // Constructor injection โ€” testable with any implementation
        OrderService(PaymentGateway payment, NotificationService notifier) {
            this.payment  = payment;
            this.notifier = notifier;
        }

        String placeOrder(String userId, double amount) {
            String orderId = UUID.randomUUID().toString().substring(0, 8);
            if (payment.charge(userId, amount)) {
                notifier.notify(userId, "Order " + orderId + " placed for $" + amount);
                return orderId;
            }
            throw new RuntimeException("Payment failed");
        }
    }

    public static void main(String[] args) {
        // Production wiring โ€” by hand (or by a DI container like Spring)
        PaymentGateway    gateway  = new StripeGateway();
        NotificationService email  = new EmailNotifier();
        OrderService orders        = new OrderService(gateway, email);

        String orderId = orders.placeOrder("alice", 99.99);
        System.out.println("Order placed: " + orderId);

        // Test wiring โ€” swap in fake implementations (no framework needed)
        PaymentGateway fakeGateway = (userId, amount) -> {
            System.out.println("[TEST] Fake charge: " + userId + " $" + amount);
            return true;
        };
        NotificationService fakeNotifier = (userId, msg) ->
            System.out.println("[TEST] Fake notify: " + msg);

        OrderService testOrders = new OrderService(fakeGateway, fakeNotifier);
        testOrders.placeOrder("bob", 49.50);
    }
}

Sources