Static Dependency Injection with Intersection Types

by: Ethan McCue

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<>());
Router<List<String>> router = new Router<>(ips);
router.addHandler("/hello", (ips, req) -> {
    ips.add(req.ip());
    return 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 {
    User findById(int id);
}

final class UserServiceImpl implements UserService {
    ...

    UserServiceImpl(DB db) {
        ...
    }

    User findById(int id) {
        ...
    }
}

interface HasUserService {
    UserService userService();
}

And make the "Context" be a type that implements all of those interfaces

record System(DB db, Redis redis) implements HasDB, HasRedis, HasUserService {
    @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(
    Components components, Request request
) {
   var redis = components.redis();
   var userService = components.userService();

   ...
}

...

System system = new System(...);
Router<System> router = new Router<>(system);
router.addHandler("/hello", Handlers::handleRequest);

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."


<- Index