I have come up with a quick and simple IoC container to enable minimal dependency injection support in one of my Java projects.

The container supports services with:

  • A transient lifetime, meaning, a new instance of the service will be created each time they are requested from the container.
  • A singleton lifetime, which only creates an instance only once and returns this same instance upon subsequent requests.

Services are registered using the addService() method which takes in the types representing the abstraction and the corresponding implementation and the service lifetime.

A service can be requested using the getService() method which returns the corresponding service implementation for an abstraction.

Finally, I have an injectAndConstruct() method that takes in a client that depends on one or more of these services. It extracts the public constructor of the client and resolves all the dependencies and creates a new instance using this constructor through reflection.


public final class Container {
    private final Map<Class<?>, ServiceDescriptor<?, ?>> services = new HashMap<>();

    // holds singleton instances after initial creation
    private final Map<Class<?>, Object> singletonInstances = new HashMap<>();

    public <T> void addService(
          Class<T> abstraction, 
          Class<? extends T> implementation, 
          Lifetime lifetime) {
        services.put(abstraction, new ServiceDescriptor<>(abstraction, implementation, lifetime));

    public <T> T getService(Class<T> abstraction) {
        var descriptor = (ServiceDescriptor<T, ? extends T>)services.get(abstraction);
        if (descriptor == null) {
            throw new ServiceNotFoundException(abstraction); // custom exception, definition omitted for brevity

        Class<? extends T> implType = descriptor.implementationType();

        Constructor<? extends T> publicConstructor = (Constructor<? extends T>) implType.getConstructors()[0];

        return switch(descriptor.lifetime()) {
            case TRANSIENT -> newInstanceOf(publicConstructor);
            case SINGLETON -> {
                // retrieve any existing instance
                T instance = (T) singletonInstances.getOrDefault(abstraction, null);

                // first request: create singleton instance and save to map 
                if (instance == null) {
                    instance = newInstanceOf(publicConstructor);
                    singletonInstances.put(abstraction, instance);
                yield instance;

    public <T> T injectAndConstruct(Class<T> clazz) {
        Constructor<? extends T> publicConstructor = (Constructor<? extends T>) clazz.getConstructors()[0];
        return newInstanceOf(publicConstructor);

    private <T> T newInstanceOf(Constructor<T> constructor) {
        var parameterTypes = constructor.getParameterTypes();
        Object[] parameters = new Object[parameterTypes.length];
        for (int i = 0; i < parameterTypes.length; i++) {
            parameters[i] = getService(parameterTypes[i]);

        try {
            return constructor.newInstance(parameters);
        } catch (ReflectiveOperationException exception) {
            throw new RuntimeException(exception);

    private void validateService(Class<?> implementation) {
        if (implementation.getConstructors().length != 1) {
            throw new IllegalArgumentException("The service implementation must have exactly one public constructor");

    private void validateAbstraction(Class<?> abstraction) {
        if (!Modifier.isAbstract(abstraction.getModifiers()) && !abstraction.isInterface()) {
            throw new IllegalArgumentException("The parameter \"abstraction\" must represent an abstract class or an interface");

The ServiceDescriptor holds information about a service:

record ServiceDescriptor<A, I extends A>(
        Class<A> abstractionType,
        Class<I> implementationType,
        Lifetime lifetime) {

The Lifetime enum is to specify the service lifetime:

enum Lifetime {


Container container = new Container();
container.addService(Service1.class, Service1Impl.class, Lifetime.TRANSIENT);
container.addService(Service2.class, Service2Impl.class, Lifetime.SINGLETON);
Client client = container.injectAndConstruct(Client.class);

class Client {
    public Client(Service1, s1, Service2 s2) { .. }

interface Service1 { ... }
interface Service2 { ... }
class Service1Impl implements Service1 { ... }
class Service2Impl implements Service2 { ... }

This seems more of a service locator than injection.

I plan to use this in my JavaFX projects for dependency injection in my FXML controllers. The JavaFX FXML loader allows setting a controller factory that helps control creation of controller instances. This factory is essentially a javafx.util.Callback<Class<?>, Object>.

The above container allows me to do this:

Container container = new Container();
// add services
FXMLLoader fxmlLoader = new FXMLLoader(...);
fxmlLoader.setControllerFactory(x -> container.injectAndConstruct(MyController.class));

And my controller can declare dependencies in the constructor:

public class MyController {

    // Injected by factory
    public MyController(Service1 s1, Service2 s2) {..}

Let me know if there is something I can refactor or improve or if I'm missing something.

  • 4
    \$\begingroup\$ Why reinvent the wheel? There are already robust dependency injection frameworks, like Dagger (compile time injection) and Spring (runtime injection) \$\endgroup\$ Commented Jul 18, 2022 at 7:11
  • 2
    \$\begingroup\$ Drop the unchecked annotation: instead (T) ... do abstraction.cast(...). \$\endgroup\$
    – Joop Eggen
    Commented Jul 22, 2022 at 8:37
  • 2
    \$\begingroup\$ Seems like a fun exercise. My only thought was instead of exposing the Lifetime enum, encapsulate that and instead expose addSingleton and addTransient instead of addService. \$\endgroup\$
    – Xtros
    Commented Jul 22, 2022 at 14:57


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.