One of the patterns I am personally partial to that I haven't really seen get traction or attention in Java is to do DI manually.
Imagine a hypothetical web framework.
interface Handler<Context> {
Response handle(Context context, Request request)
}
...
final class Router<Context> {
...
Router(Context context) {
...
}
void addHandler(String route, Handler<Context> handler) {
...
}
}
We can make a router that carries through some context to the different handlers
List<String> ips = Collections.synchronizedList(new ArrayList<>());
<List<String>> router = new Router<>(ips);
Router.addHandler("/hello", (ips, req) -> {
router.add(req.ip());
ipsreturn Response.of(ips.toString());
});
And then all our route handlers will get the context. Then the trick is to make an interface for each stateful "thing" a handler might want access to, like a database connection or a redis connection.
interface HasDB {
db();
DB }
interface HasRedis {
redis();
Redis }
And similarly for anything that you might have that is "derivative" of those root stateful components
interface UserService {
findById(int id);
User }
final class UserServiceImpl implements UserService {
...
UserServiceImpl(DB db) {
...
}
findById(int id) {
User ...
}
}
interface HasUserService {
userService();
UserService }
And make the "Context" be a type that implements all of those interfaces
System(DB db, Redis redis) implements HasDB, HasRedis, HasUserService {
record @Override
public UserService userService() {
return new UserServiceImpl(db);
}
}
Then a handler just needs to "declare its dependencies" by saying which stateful components it wants to use. For example if we have a handler that just wants to lookup a user and write things into redis
public static <Components extends HasRedis & HasUserService> handleRequest(
, Request request
Components components) {
= components.redis();
var redis = components.userService();
var userService
...
}
...
System system = new System(...);
<System> router = new Router<>(system);
Router.addHandler("/hello", Handlers::handleRequest); router
And by virtue of System
being HasDB
, HasRedis
and a HasUserService
it will fulfill HasRedis & HasUserService
.
Replace "route handler" with whatever other entry points your app has and boom, dependency injection without reflection or magics.
There are downsides - System
might get fairly large depending on your preferences, it doesn't solve the problem of starting everything in the right order, and there is a decent amount of boilerplate - but I just wish more people knew about this "System pattern."