System Design
1. System Prerequisites

Dependency Injection

⏱ 0s active on this topic

In simple words

Dependency Injection means a class is handed the things it needs, instead of building them itself. Picture a restaurant chef: he doesn't grow the vegetables or forge the knives — the restaurant supplies everything and he just cooks. In code, instead of a class creating its own database or payment connection with new, that connection is passed in from outside. This makes the class easy to test and easy to change.


The problem DI solves

// WITHOUT DI — the class builds its own dependencies
class OrderService {
    private final OrderRepository repo    = new MySqlOrderRepository();   // hardcoded
    private final PaymentGateway  payment = new RazorpayGateway();        // can't swap
    private final EmailService    email   = new SmtpEmailService();       // can't mock

    Order placeOrder(OrderRequest r) {
        Order o = new Order(r);
        repo.save(o);
        payment.charge(o);
        email.sendConfirmation(o);
        return o;
    }
}
  • Switch MySQL → Mongo? Rewrite OrderService.
  • Test placeOrder() without a real DB / real Razorpay / real SMTP? You can't — it builds them internally.
  • Every dependency is welded in.
// WITH DI — dependencies passed in
class OrderService {
    private final OrderRepository repo;
    private final PaymentGateway  payment;
    private final EmailService    email;

    OrderService(OrderRepository repo, PaymentGateway payment, EmailService email) {
        this.repo = repo;
        this.payment = payment;
        this.email = email;
    }

    Order placeOrder(OrderRequest r) { /* same body */ }
}

Now the caller (or a framework) wires it:

// production
new OrderService(new MySqlOrderRepository(), new RazorpayGateway(), new SmtpEmailService());

// test — swap in fakes, no infrastructure needed
new OrderService(new InMemoryOrderRepository(), new FakePaymentGateway(), new FakeEmailService());

Three ways to inject

1. Constructor injection — the default

class UserService {
    private final UserRepository repo;          // final — set once, never changes
    private final EmailService email;

    UserService(UserRepository repo, EmailService email) {
        this.repo = repo;
        this.email = email;
    }
}
  • Dependencies are explicit and required — you can't construct the object without them.
  • Fields can be final → the object is effectively immutable after construction.
  • All dependencies visible in one place (the constructor signature).
  • Trivial to test — just pass mocks.

In Spring, if there's one constructor you don't even need an annotation; Lombok's @RequiredArgsConstructor generates the constructor from the final fields.

2. Setter injection — for genuinely optional dependencies

class ReportService {
    private final DataSource dataSource;        // required, via constructor
    private Logger logger;                       // optional

    ReportService(DataSource ds) { this.dataSource = ds; }

    void setLogger(Logger logger) { this.logger = logger; }   // may or may not be set
}

Use it only when the object genuinely works without the dependency. Downside: the object can exist half-configured.

3. Field injection — avoid in real code

@Service
class PaymentService {
    @Autowired private PaymentRepository repo;       // Spring sets this via reflection
    @Autowired private NotificationService notifier;
}
  • Fields can't be final.
  • Can't test without the Spring container running.
  • Dependencies are hidden — not visible in a constructor.

Fine for a throwaway prototype; not for production code.

TypeVerdict
Constructor✅ default — explicit, immutable, testable
Setter⚠️ optional dependencies only
Field (@Autowired on field)❌ avoid in new code

Why final + constructor injection matters (first principles)

private UserService userService;                    // someone can later do userService = null;
private final UserService userService;              // compiler forbids reassignment after construction

A controller should use the same service for its whole life. final lets Java enforce that at compile time. That's immutability — state can't change after construction — and immutable objects are safer, more predictable, and thread-safe.

Field injection can't support final because the assignment happens after the object is built (via reflection). Constructor injection assigns during construction, so final works. That's the core reason modern Spring prefers it.


DI vs DIP — don't confuse them (interviewers test this)

What it is
DIP (Dependency Inversion Principle)a design principle — depend on abstractions, not concrete classes
DI (Dependency Injection)a technique — pass dependencies in from outside
DIP says:  OrderService should depend on the OrderRepository INTERFACE, not MySqlOrderRepository.
DI says:   don't let OrderService create that dependency — hand it in.

DI is HOW you implement DIP. You inject an abstraction (DIP) from outside (DI).

IoC — the bigger picture

Inversion of Control: normally your class controls what it gets (new inside). With IoC, something external (a framework/container) creates and provides the dependencies — control is inverted. DI is one way to do IoC.


How Spring's IoC container does it

@Repository
class MySqlUserRepository implements UserRepository { }   // Spring makes ONE instance

@Service
class UserService {
    private final UserRepository repo;
    UserService(UserRepository repo) { this.repo = repo; } // Spring injects the repo
}

@RestController
class UserController {
    private final UserService userService;
    UserController(UserService userService) { this.userService = userService; }
}

Spring reads the annotations, creates each bean once (singleton scope), and wires the whole object graph via constructors. You never call new.


The payoffs

Swap implementations by config, not code:

interface NotificationService { void send(String userId, String message); }

@Component class EmailNotificationService implements NotificationService { /* ... */ }
@Component class SmsNotificationService   implements NotificationService { /* ... */ }

Pick which one gets injected via a property — OrderService doesn't change.

Unit-test business logic with no infrastructure:

OrderRepository mockRepo    = mock(OrderRepository.class);
PaymentGateway  mockPayment = mock(PaymentGateway.class);
EmailService    mockEmail   = mock(EmailService.class);

OrderService service = new OrderService(mockRepo, mockPayment, mockEmail);
service.placeOrder(request);

verify(mockRepo).save(any(Order.class));
verify(mockPayment).charge(any(Order.class));

No MySQL, no Razorpay account, no SMTP server — just the logic.


At system-design scale

  • Repository pattern — Service → Repository interface ← { Postgres impl, Redis-cached impl, mock impl }. Environment decides which is injected.
  • Feature flags — bind PaymentGateway to NewGateway or LegacyGateway at startup based on a flag; every consumer gets the right one.
  • Multi-region / multi-tenant — inject a region-specific FileStorage (S3 Mumbai vs S3 Frankfurt) chosen at startup.

DI frameworks: Spring (Java), Guice/Dagger (Java), NestJS/Angular (TypeScript), ASP.NET Core (C#).


How This Connects

  • SOLID — DI is the mechanism behind D (Dependency Inversion) and enables O (swap implementations without editing callers).
  • Polymorphism / Abstraction — you inject an interface; polymorphism routes the call to whatever implementation was wired in.
  • Cohesion & Coupling — DI is the main tool for lowering coupling: a class names a small contract, not a concrete class it builds.
  • Composition vs Inheritance — "compose collaborators and receive them from outside" is composition + DI.
  • Classes Objects & Instances — Spring beans are singletons; injecting stateless collaborators (not storing mutable request state) is why that's safe.
  • Design Patterns — the container is a Factory + Singleton manager; Strategy/Repository/Gateway all rely on DI for their wiring step.

Trade-offs & common mistakes

  • Don't call new for a collaborator inside a service — it re-introduces the coupling DI removes.
  • Don't use field injection in code you'll maintain — prefer constructor injection with final.
  • Don't let a class pile up 6+ constructor dependencies — that's a Single Responsibility smell; split the class.
  • Don't create circular dependencies (A needs B, B needs A) — redesign the relationship.
  • Note the cost: DI adds a wiring layer (a container, or manual assembly in main) and one level of indirection when reading code. For a small script it's overhead; for anything that needs testing or swappable parts it pays for itself immediately.

In the Interview

When to say it. Any time you talk about testing a service, or swapping a DB / provider / channel:

  • "OrderService takes a repository interface via its constructor, so tests inject a fake"
  • "we bind the payment gateway at startup — switching providers is a config change"

The ~20-second version:

"Dependency Injection means a class receives its collaborators from outside instead of creating them with new. OrderService shouldn't do new MySqlOrderRepository() — it should accept an OrderRepository interface in its constructor. That implements the Dependency Inversion Principle, and the practical wins are: swap MySQL for Mongo by changing one wiring line, unit-test the business logic with mocks and no real database, and let Spring's IoC container assemble the whole object graph. I use constructor injection with final fields — dependencies are explicit and the object is immutable after construction."

Follow-ups to expect:

  1. "DI vs DIP?" — DIP is the principle (depend on abstractions); DI is the technique (inject them from outside). DI is how you achieve DIP.
  2. "Why constructor over field injection?" — Explicit dependencies, final fields (immutability), and it tests without the Spring container.
  3. "What is IoC?" — Inversion of Control: the framework, not your class, decides what gets created and injected. DI is one form of it.
  4. "A class has 8 injected dependencies — problem?" — Yes, it's doing too much (SRP violation). Split it.

Mistake to avoid: saying "DI is a design pattern". It's a technique/principle. The Singleton and Factory are patterns; DI is the broader idea that makes them wireable.


Quick Revision — 7 lines

  1. DI = pass dependencies in from outside; don't new them inside the class.
  2. Three types: constructor (default — explicit, final, testable), setter (optional deps), field (avoid).
  3. DIP is the principle (depend on abstractions); DI is the technique that delivers it. IoC = the framework is in control.
  4. final + constructor injection = the dependency can't be reassigned → immutable, safer, thread-safe.
  5. Spring's IoC container creates beans (singleton) and wires the object graph via constructors — you never call new.
  6. Payoffs: swap implementations by config, and unit-test business logic with mocks and zero infrastructure.
  7. Smell: 6+ injected dependencies in one class → split it (SRP).

Quick-revision quiz

Question 1 of 18
What is Dependency Injection?