Think of Spring Boot like a restaurant kitchen: your class should not run out and gather ingredients in the middle of cooking.
Question: What is Dependency Injection?
Answer: Dependency Injection means Spring creates the objects your class needs and gives them to it, instead of your class creating them with new. In Spring Boot, the ApplicationContext (the container) manages these objects, called beans, and connects them for you. This makes code easier to test, easier to change, and less tightly coupled to one specific implementation.
Interview-Ready Answer: I use Dependency Injection when I want Spring Boot to provide a class’s dependencies instead of my code building them manually. Spring’s container creates the beans, chooses the right implementation, and injects them, usually through the constructor. The big advantage is loose coupling: I can swap a real payment service for a mock in tests without changing the service code.
Detailed Explanation: Dependency Injection is a way to apply Inversion of Control, which means your code does not control object creation and wiring by itself. Instead, Spring Boot’s container owns that job. A bean is just an object managed by Spring, and the container keeps track of how beans depend on each other.
@Component, @Service, @Repository, @Controller, and @Bean methods.@Primary or @Qualifier to break the tie.If a required dependency is missing, startup fails fast with an error like NoSuchBeanDefinitionException. If more than one bean fits and Spring cannot decide, you get NoUniqueBeanDefinitionException. That fail-fast behavior is good because it catches wiring mistakes early.
| Style | Best for | Pros | Cons |
|---|---|---|---|
| Constructor | Required dependencies | Best for tests, clear, immutable | Can expose circular design problems |
| Setter | Optional dependencies | Flexible, can reconfigure later | Object may exist half-wired |
| Field | Quick demos | Shortest code | Hidden dependencies, harder to test |
In modern Spring Boot, constructor injection is the default choice for most services. Since Spring 4.3, if a class has only one constructor, @Autowired is not required. That small detail is often asked in interviews.
DI is mainly a startup-time cost. Spring resolves the bean graph once, so the work is roughly linear in the number of beans and dependency links, not on every method call. In a small app with 50-100 beans, the cost is usually tiny; in a larger app with hundreds of beans, startup can take seconds mostly because of classpath scanning, reflection, and proxy creation, not because injection itself is slow.
Some important edge cases: circular dependencies are a design smell, and Spring Boot 2.6+ disables many of them by default; optional dependencies can be handled with ObjectProvider or setter injection; and if you have multiple beans of the same type, be explicit with @Primary or @Qualifier. The mental model to remember is simple: Spring builds the object graph, and your code just uses the finished objects.
Real-World Example: Imagine an ecommerce checkout service. It needs a payment gateway, a tax calculator, and maybe a fraud checker. With DI, the checkout service depends on interfaces, so Spring can inject a sandbox gateway in development and a live gateway in production, while your business code stays the same.
What goes wrong when someone misunderstands DI? A developer hard-codes new StripePaymentGateway() inside the service. Now tests accidentally depend on real credentials, staging calls the wrong endpoint, and swapping providers becomes a code change instead of a config change. In production, the symptom is usually checkout failures, logs showing the wrong gateway, and support tickets from orders stuck in PENDING because the service could not be mocked or replaced cleanly.
The outage pattern is very recognizable: unit tests start making network calls, CI becomes flaky, and logs show NullPointerException or unexpected external API errors because an object was created outside Spring and never injected with its collaborators. DI prevents that by making dependencies explicit and centrally managed.
package com.example.demo;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
CommandLineRunner demo(CheckoutService checkoutService) {
return args -> {
System.out.println("=== Checkout examples ===");
System.out.println("Total for 12,000 = " + checkoutService.finalTotal(12000));
System.out.println("Total for 4,000 = " + checkoutService.finalTotal(4000));
// Edge case: a bad input is handled cleanly instead of failing later with a mystery bug.
try {
checkoutService.finalTotal(-1);
} catch (IllegalArgumentException ex) {
System.out.println("Handled edge case: " + ex.getMessage());
}
};
}
}
interface PaymentGateway {
String charge(int amountInCents);
}
@Primary
@Component
class SandboxPaymentGateway implements PaymentGateway {
@Override
public String charge(int amountInCents) {
// If we had two PaymentGateway beans and removed @Primary,
// Spring would fail fast at startup because it would not know which one to inject.
return "SANDBOX approved: " + amountInCents + " cents";
}
}
@Component
class LivePaymentGateway implements PaymentGateway {
@Override
public String charge(int amountInCents) {
return "LIVE approved: " + amountInCents + " cents";
}
}
interface TaxCalculator {
int taxFor(int subtotal);
}
@Service
class CheckoutService {
private final PaymentGateway paymentGateway;
private final ObjectProvider<TaxCalculator> taxCalculatorProvider;
// Single-constructor injection is preferred: the dependency is obvious and easy to test.
CheckoutService(PaymentGateway paymentGateway, ObjectProvider<TaxCalculator> taxCalculatorProvider) {
this.paymentGateway = paymentGateway;
this.taxCalculatorProvider = taxCalculatorProvider;
}
int finalTotal(int subtotal) {
if (subtotal < 0) {
throw new IllegalArgumentException("Subtotal cannot be negative");
}
// Optional dependency: if no TaxCalculator bean exists, Spring still starts and we default to zero tax.
TaxCalculator taxCalculator = taxCalculatorProvider.getIfAvailable();
int tax = (taxCalculator == null) ? 0 : taxCalculator.taxFor(subtotal);
String confirmation = paymentGateway.charge(subtotal + tax);
System.out.println("Payment result: " + confirmation);
return subtotal + tax;
}
}
Follow-up & Tricky Questions:
@Primary or @Qualifier to choose one. If it still cannot decide, startup fails fast.ObjectProvider, Optional, or setter injection when absence is valid. Optional dependencies should not force the whole app to fail at startup.new? No. Only Spring-managed beans are wired by the container; manually created objects bypass DI completely.@Autowired required on a single constructor? No, not in modern Spring. Since Spring 4.3, a class with one constructor is injected automatically.Common Mistakes:
new inside a Spring service. Correction: inject the dependency so Spring can manage it and tests can replace it.@Qualifier or @Primary when multiple beans implement the same interface. Correction: tell Spring exactly which bean you want.Memory Hook: You cook; Spring stocks the pantry. Your class does the work, but Spring supplies the ingredients, so the recipe stays clean and the kitchen can swap ingredients without rewriting the chef.
Cheat Sheet:
ApplicationContext.@Primary or @Qualifier when several beans match.ObjectProvider or setter injection for optional dependencies.Practice Tasks:
new and convert it to constructor injection.@Primary or @Qualifier.