Demystifyingย
Dependency Injection

๐จ๐ปโ๐ป Jens Knipper
- Software Engineer
- OpenValue Dรผsseldorf
- curious about new technologies
- likes to share his knowledge


๐ Agenda
- What is Dependency Injection (DI)?
- Why should you use DI?
- Core concepts
- Ways to inject
- Constructor Injection, ...
- Wiring
- manual vs frameworks
- Frameworks


A
B
uses methods of
- A creates B
- A uses methods of B
- B is dependency of A
creates
๐ฉ๏ธ Problem
๐ฉ๏ธ Problem

class UserService {
UserRepository userRepo = new UserRepository(...);
List<User> getActiveUsers() {
return userRepo.findAll().stream()
.filter(User::isActive)
.toList();
}
}- violates Single Responsibility Principle
- UserService responsible for Lifecycle of userRepo
- tight coupling
- cannot run UserService independently
- difficult to test in isolation
- changes in UserRepo might break UserService tests
โ๏ธ Solution

class UserService {
UserRepository userRepo;
UserSerivce (UserRepository userRepo) {
this.userRepo = userRepo;
}
List<User> getActiveUsers() {
return userRepo.findAll().stream()
.filter(User::isActive)
.toList();
}
}- UserRepo injected into UserService
- lifecycle outside of UserService
- independent testing possible

A
B
uses methods of
- X is injector
- A is client
- B is dependency
X
create
create and inject B
โ๏ธ Solution
๐ Dependency Injection (DI)
- separation of concerns
- separate construction and usage of dependencies
- dependencies are provided (injected) from outside
- test unit independent of its dependencies
- many ways to do injection
- constructor
- field
- ...
- different paradigms
- manual
- compile time
- runtime


A
uses methods of
- often used with Dependency Injection
- A does not know which implementation of B is used
X
create
create and inject B
๐ย Dependency Inversion Principle
interface
B
implements
impl.
B
๐ Inversion of Control (IoC)
- control flow of a program is inverted
- e.g. framework controls program flow
- e.g. Spring: RequestFilter, RestController
- loose coupling of classes
- easy testing
- main principle behind Dependency Injection
- Dependency Injection is a subset of IoC
- managing lifecycle of dependencies

๐ฃ๏ธ Ways to inject
- Lot's of different names
- Constructor injection
- Method injection
- Setter injection
- Interface Injection
- Parameter injection
- Field injection
- Property injection
- often mean the same or slightly different usage


Constructor
๐ฃ๏ธ Ways to inject
Method
Field /Property
Setter
Interface
Parameter
๐ ๏ธ Constructor Injection

class UserService {
UserRepository userRepo;
UserService (UserRepository userRepo) {
this.userRepo = userRepo;
}
List<User> getUsers() {
return userRepo.findAll();
}
}Setup
UserRepository userRepository = new UserRepository();
UserService userService = new UserService(userRepository);๐ ๏ธ Constructor Injection

- easy to use and understand
- for mandatory dependencies
- dependencies are always initialised
-
ย constructor can become bloated with dependencies
-
known as constructor over-injection
-
class might be doing more than one thing
-
refactoring!
-
-
๐ฒ Cyclic Dependencies

- not solvable with Constructor Injection
- refactoring!
- use another injection type
- framework specific solutions
UserRepository userRepository = new UserRepository(userSerivce);
UserService userService = new UserService(userRepository);๐ Setter Injection

class UserService {
UserRepository userRepo;
void setUserRepo (UserRepository userRepo) {
this.userRepo = userRepo;
}
List<User> getUsers() {
return userRepo.findAll();
}
}Setup
UserService userService = new UserService();
UserRepository userRepository = new UserRepository(userService);
userService.setUserRepo(userRepository);๐ Setter Injection

- might not be initialised when called
- runtime errors!
- mutability needed
- dependencies optional
- changeable at runtime
- useful for lazy initialisation
- only created when needed
- useful when expensive to create
- resolve cyclic dependencies
- only created when needed
โ๏ธ Interface Injection

-
similar to setter injection
-
dependency provided by injector method
-
defined in interface
-
-
dependent class (client) implements interface
-
plugin-based architecture
interface InjectUserRepo {
public void injectUserRepo(UserRepository userRepo);
}โ๏ธ Parameter Injection

class UserService {
List<User> getUsers(UserRepository userRepo) {
return userRepo.findAll();
}
}- method injection
- stateless
- dependency mandatory
- changeable
- only used when needed
- resolve cyclic dependencies
๐ผ๏ธ Field Injection

class UserService {
UserRepository userRepo;
List<User> getUsers() {
return userRepo.findAll();
}
}Setup
UserRepository userRepo = new UserRepository();
UserService userService = new UserService();
userService.userRepo = userRepo;๐ผ๏ธ Field Injection (Reflection)

class UserService {
private UserRepository userRepo;
List<User> getUsers() {
return userRepo.findAll();
}
}Setup
UserRepository userRepo = new UserRepository();
UserService userService = new UserService();
Field userRepoField = userService.getClass()
.getDeclaredField("userRepo");
userRepoField.setAccessible(true);
userRepoField.set(userService, userRepo);
๐ผ๏ธ Field Injection

- also called property injection
- simple, avoids boilerplate code
- difficult to test
- dependencies optional
- dependency changeable
- might not be initialised when called
- runtime errors!
๐ก Making a decision

- use Constructor Injection whenever possible
- refactor when class gets bloated
- refactor on cyclic dependencies
- use Setter or Parameter Injection otherwise
- avoid Field Injection
๐Wiring

- dependency lifecycle outside of our class
-
BUT: who manages it?
- you: manual DI
- someone else: a framework
class UserService {
UserRepository userRepo;
UserSerivce (UserRepository userRepo) {
this.userRepo = userRepo;
}
}๐ช Manual DI

public class CliTool {
public static void main (String[] args) {
String endpoint = System.getenv("ENDPOINT");
RestClient restClient = new RestClient(endpoint);
UserService userService = new UserService(restClient);
userService.doSomething();
restClient.close();
}
}
- small applications
- < few thousand LOC
- CLI tools, Serverless Functions (AWS Lambda), ...
- easy to lose overview
- switch to a framework
๐๏ธ Framework

- often annotation based
- compile time or runtime wiring
- different philosophies
- works differently internally
- IoC Container
- manages dependencies
- lazy or eager dependency creation
- define creation order of dependencies
- create dependencies when requested or at startup
๐ทโโ๏ธ Compile Time DI

- more recent frameworks mostly
- wiring during compilation
- annotation processing
- establish ordering using dependency graph
- code generation
- errors found during compilation: missing or cyclic dependencies
- fast, no reflection runtime overhead
- less flexible
- often cannot change dependency at runtime
๐ทโโ๏ธ Compile Time DI

-
- ย
-
- annotation processing
- establish ordering using dependency graph
- code generation
๐โโ๏ธ Runtime DI

- wiring during execution
- classpath scanning
- class inspection using reflection
- establish ordering
- object creation and wiring
- errors might only show up at runtime
- reflection might cause performance overhead
- very flexible
- easily switch dependencies using properties, profiles, ...
๐ฟ Popular Frameworks

- compile time
- Dagger
- Quarkus ArC
- Avaje Inject
- runtime
- Spring
- Guice
- Jakarta CDI - e.g. MicroProfile
- Koin
๐ก Making a decision

- use the one provided by the (web) framework you use
- well integrated
- manual if feasible
- compile time otherwise
- my preference: Avaje Inject
๐ฑ Spring

- TODO
๐ชย Avaje Inject

- TODO
๐ฏ Wrapping Up

- Dependency Injection helps you with testing
- combine with Dependency Inversion Principle for fakes and mocks
- DI != IOC
- lots of ways to inject
- use constructor injection
- refactor if not possible
- avoid field injection
- there is no need for a framework
- if you have it, use it
- prefer compile time
Resources

Thank you!
Please ask questions.
โ๏ธย jens@openvalue.de
ย ย ย @jensknipper.de
๐ openvalue.eu


Dependency Injection - A deep dive
By Jens Knipper
Dependency Injection - A deep dive
- 99