Dependency Injection
Question Variations
- "What is Dependency Injection, and how does it relate to Inversion of Control (IoC)?"
- "Explain the difference between Transient, Scoped, and Singleton lifetimes in a DI container."
- "Why is constructor injection generally preferred over property or method injection?"
- "How does Dependency Injection improve the testability of your code?"
Why This Is Asked
DI is a foundational pattern in modern software. Interviewers want to confirm you understand Inversion of Control, can explain the benefits of loose coupling and testability, and know how DI containers manage object lifetimes.
Key Concepts
- Dependency Injection is a form of Inversion of Control (IoC) where dependencies are provided externally
- Three injection styles: constructor injection (most common), method injection, property injection
- Service lifetimes: Transient (new instance per request), Scoped (per scope/HTTP request), Singleton (shared)
- Enables unit testing by allowing mock/stub substitution
- The composition root is where the entire object graph is wired up
Question Variations
- “What is Dependency Injection, and how does it relate to Inversion of Control (IoC)?”
- “Explain the difference between Transient, Scoped, and Singleton lifetimes in a DI container.”
- “Why is constructor injection generally preferred over property or method injection?”
- “How does Dependency Injection improve the testability of your code?”
Answers by Technology
+ Add VariantExpected Answer (.NET 10 / C# 14)
ASP.NET Core provides a built-in dependency injection (DI) container that manages the lifetime and resolution of services. The three primary service lifetimes are:
- Transient: Services are created every time they are requested from the service container. This lifetime works best for lightweight, stateless services.
- Scoped: Services are created once per client request (connection). All components processing the same web request share the same instance.
- Singleton: Services are created the first time they are requested (or when
Startupruns) and every subsequent request uses the same instance.
Why It Matters
Proper DI management is crucial for loose coupling and testability. It allows you to swap implementations (e.g., using a Mock repository in tests) without changing the consuming class. Misunderstanding lifetimes—especially “Captive Dependencies” (injecting a Scoped service into a Singleton)—can lead to memory leaks, stale data, and concurrency bugs.
Example Code
// Registration in Program.cs
builder.Services.AddTransient<IMyTransientService, MyTransientService>();
builder.Services.AddScoped<IMyScopedService, MyScopedService>();
builder.Services.AddSingleton<IMySingletonService, MySingletonService>();
// Usage in a Controller
public class MyController : ControllerBase
{
private readonly IMyScopedService _scopedService;
public MyController(IMyScopedService scopedService)
{
_scopedService = scopedService;
}
}
Common Mistakes
- Captive Dependency: Injecting a Scoped service into a Singleton. The Scoped service will effectively become a Singleton, which might break its intended behavior (e.g., sharing a database context across multiple requests).
- Service Locator Anti-pattern: Manually resolving services from
IServiceProviderinside your business logic instead of using constructor injection. This makes the code harder to test and hides dependencies.
Follow-up Questions
- How do you resolve a service manually when constructor injection isn’t available? (Answer: Use
IServiceScopeFactoryto create a new scope and then resolve the service from the scope’sIServiceProvider). - What is the difference between
AddSingleton<TService, TImplementation>()andAddSingleton(new TImplementation())? (Answer: The first is lazily created on first request; the second is eagerly created during registration).
References
Expected Answer (Java 26)
In the Java ecosystem, Dependency Injection is primarily associated with Spring Framework and Jakarta EE (CDI).
- Spring Context: The IoC container manages beans and their dependencies.
- Annotation-Driven: Uses
@Autowired(Spring) or@Inject(Jakarta EE) to resolve dependencies. - Service Lifetimes (Scopes):
- Singleton: One instance per Spring container. The same instance is injected into all dependent beans. (Default).
- Prototype: A new instance is created every time the bean is requested from the container.
- Request: A new instance is created for each HTTP request (only valid in web-aware Spring contexts). Similar to .NET’s “Scoped”.
- Session: A new instance is created for each HTTP session.
Why It Matters
Java enterprise applications rely heavily on DI to manage complexity. Spring’s implementation of IoC allows for decoupled architectures where infrastructure (like DB transactions) can be injected via AOP (Aspect Oriented Programming) without polluting business logic.
Code Example
// Spring Framework Constructor Injection
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired // Optional in modern Spring for constructor injection
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
Common Mistakes
- Field Injection: Using
@Autowiredon private fields makes unit testing difficult without a Spring context. Constructor injection is the recommended best practice. - Circular Dependencies: Spring will throw a
BeanCurrentlyInCreationExceptionif two beans depend on each other via constructors.
Follow-up Questions
- Constructor vs Field Injection? (Answer: Constructor injection ensures required dependencies are present and allows for
finalfields). - What is a BeanPostProcessor? (Answer: An interface that allows for custom modification of new bean instances).
Expected Answer (PHP 8.5)
In modern PHP, Dependency Injection is handled by a Service Container (IoC container).
- Constructor Injection: The preferred method. The container uses reflection to automatically instantiate and inject dependencies.
- Laravel Service Container: Allows binding interfaces to concrete implementations via Service Providers.
- Symfony DependencyInjection Component: Uses YAML, XML, or PHP configuration to define how services are instantiated.
- Service Lifetimes:
- Singleton: The container resolves the dependency once and returns the same instance for the rest of the request cycle.
- Transient (Binding): A new instance is created every time it is resolved.
- Scoped: In PHP (Shared Nothing), most services are effectively scoped to the single HTTP request by default.
Why It Matters
PHP started as a script-based language but evolved into a robust enterprise language. DI is critical for decoupling web applications, allowing for easier testing (e.g., swapping a real mailer for a MockMailer) and managing the “shared nothing” architecture of PHP.
Code Example
// Laravel Constructor Injection
namespace App\Services;
use App\Repositories\UserRepository;
class UserService
{
protected $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
}
Common Mistakes
- Using the Service Locator pattern: Calling the container directly (e.g.,
app('service')or$container->get('service')) inside a class. This hides dependencies and makes testing harder. - Static methods for business logic: Relying on static methods instead of injected services prevents mocking and leads to tight coupling.
Follow-up Questions
- Binding Interfaces: How do you tell the container which implementation to use for an interface? (Answer: In Laravel, using
$this->app->bind(Interface::class, Implementation::class)in a Service Provider). - Singleton vs Transient: How are they handled in PHP? (Answer: Most services are singletons within the scope of a single request).