Dependency injection (DI) is a design pattern that separates what a component does from how it gets the objects it needs to do it. Instead of a class instantiating its own dependencies, something else provides them. In an interview you can frame it as: "DI is a way to give a class the collaborators it needs from the outside, so the class doesn’t have to know how to build them."
Why Use Dependency Injection?
- Testability – When a class receives its collaborators, you can replace them with mocks or fakes in unit tests.
- Loose coupling – The class depends only on an interface or protocol, not on a concrete implementation.
- Flexibility – Switching implementations (e.g., from a local cache to a remote service) does not require touching the class that uses them.
- Single responsibility – The class focuses on its core logic, not on object construction.
These benefits come with a few trade‑offs:
- Indirection – The flow of control is less obvious; you need to trace who creates and wires objects.
- Configuration overhead – In larger codebases you often need a container or a factory to manage object graphs.
- Potential over‑engineering – For very small modules, DI can add unnecessary boilerplate.
Common Mechanisms
| Mechanism | Typical language feature | When to use |
|---|---|---|
| Constructor injection | init parameters (Swift, Java) | Preferred for required dependencies; guarantees they are set before use |
| Setter injection | public properties or methods | Useful for optional dependencies or when circular references exist |
| Interface/Protocol injection | protocol methods that receive the dependency | Rare; mainly when you cannot change the constructor signature |
Constructor Injection in Swift
protocol AuthService {
func login(username: String, password: String) async throws -> Bool
}
class LoginViewModel {
private let authService: AuthService
init(authService: AuthService) {
self.authService = authService
}
func performLogin(username: String, password: String) async -> Bool {
do {
return try await authService.login(username: username, password: password)
} catch {
return false
}
}
}
In a test you can inject a mock that conforms to AuthService without touching the production implementation.
Setter Injection Example
class AnalyticsTracker {
var logger: Logger? // optional dependency
func track(event: String) {
logger?.log(event)
}
}
You can set logger after the object is created, which is handy when the logger is a singleton that is not available at construction time.
Typical Interview Questions
- What is dependency injection and why does it matter? – Expect you to give the one‑sentence definition and mention testability and loose coupling.
- How would you inject a dependency in Swift/Java/Kotlin? – Walk through constructor injection code similar to the example above.
- What are the downsides of using a DI container? – Discuss indirection, runtime errors if the graph is mis‑configured, and the learning curve.
- Can you give a scenario where you would not use DI? – Small scripts, one‑off utilities, or performance‑critical sections where the overhead of abstraction outweighs benefits.
- How do you test a class that uses DI? – Show a test that supplies a mock implementation and verifies behavior.
A 60‑Second Spoken Answer
"Dependency injection is a pattern where a class receives the objects it works with from outside rather than creating them itself. The most common way to do this is constructor injection: you pass the dependencies through the initializer, which guarantees they’re available before any method runs. This makes the class easy to test because you can swap the real service for a mock that implements the same protocol. The trade‑off is that you introduce an extra layer of indirection and sometimes need a container to wire everything together, which can add complexity. In Swift, for example, a
LoginViewModelmight take anAuthServiceprotocol in its initializer; in a unit test you provide a fakeAuthServicethat returns a predetermined result, letting you verify the view model’s logic without hitting a network. Overall, DI improves modularity and testability, but you should avoid over‑engineering it for tiny modules."
When to Mention Call Assistant
If you want to rehearse this answer aloud before the interview, you can use Call Assistant to record yourself and get instant feedback on pacing and clarity. It also helps you keep the follow‑up discussion on point by reminding you of the key trade‑offs you wanted to hit.
How to Practice This
- Write the short definition on a sticky note and read it aloud until it feels natural.
- Implement a tiny project (e.g., a calculator view model) using constructor injection, then replace the service with a mock in a test.
- Do a mock interview with a peer or with Call Assistant, focusing on answering the typical questions listed above.
FAQ
Q: Is dependency injection the same as a service locator? A: No. A service locator hides the lookup inside the class, while DI passes the dependency directly, keeping the class unaware of where it comes from.
Q: Do I need a DI framework for every project? A: Not necessarily. Small codebases often benefit from simple constructor injection without a container. Frameworks become useful when the object graph grows large and you need lifecycle management.
Q: How does DI affect performance? A: The runtime cost is usually negligible; the biggest impact is compile‑time and memory overhead from additional abstractions. In performance‑critical loops, you can still use DI but keep the hot path free of indirection.
Q: Can DI be used with legacy code that wasn’t designed for it? A: Yes, you can refactor gradually by extracting interfaces and adding constructors that accept those interfaces, then wiring the old implementation via a simple factory.
Frequently asked questions
Is dependency injection the same as a service locator?
No. A service locator hides the lookup inside the class, while DI passes the dependency directly, keeping the class unaware of where it comes from.
Do I need a DI framework for every project?
Not necessarily. Small codebases often benefit from simple constructor injection without a container. Frameworks become useful when the object graph grows large and you need lifecycle management.
How does DI affect performance?
The runtime cost is usually negligible; the biggest impact is compile‑time and memory overhead from additional abstractions. In performance‑critical loops, you can still use DI but keep the hot path free of indirection.
Can DI be used with legacy code that wasn’t designed for it?
Yes, you can refactor gradually by extracting interfaces and adding constructors that accept those interfaces, then wiring the old implementation via a simple factory.
#concept#dependency injection#interview#software design#testing