javaadvanced

Manual Dependency Injection Container

Build a simple DI container with constructor injection, singleton scope, and interface binding.

java
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class DIContainer {
    private final Map<Class<?>, Class<?>> bindings = new HashMap<>();
    private final Map<Class<?>, Object> singletons = new ConcurrentHashMap<>();
    private final Set<Class<?>> singletonTypes = new HashSet<>();

    // Bind interface to implementation
    public <T> DIContainer bind(Class<T> iface, Class<? extends T> impl) {
        bindings.put(iface, impl);
        return this;
    }

    // Register as singleton
    public <T> DIContainer singleton(Class<T> iface, Class<? extends T> impl) {
        bindings.put(iface, impl);
        singletonTypes.add(iface);
        return this;
    }

    // Register instance
    public <T> DIContainer instance(Class<T> type, T instance) {
        singletons.put(type, instance);
        singletonTypes.add(type);
        return this;
    }

    // Resolve dependency
    @SuppressWarnings("unchecked")
    public <T> T resolve(Class<T> type) {
        // Check for cached singleton
        if (singletons.containsKey(type)) {
            return (T) singletons.get(type);
        }

        // Find implementation class
        Class<?> implClass = bindings.getOrDefault(type, type);

        // Get constructor with most parameters (greedy)
        Constructor<?> constructor = Arrays.stream(implClass.getConstructors())
            .max(Comparator.comparingInt(Constructor::getParameterCount))
            .orElseThrow(() -> new RuntimeException("No public constructor: " + implClass));

        // Resolve constructor parameters recursively
        Object[] args = Arrays.stream(constructor.getParameterTypes())
            .map(this::resolve)
            .toArray();

        try {
            T instance = (T) constructor.newInstance(args);
            if (singletonTypes.contains(type)) {
                singletons.put(type, instance);
            }
            return instance;
        } catch (Exception e) {
            throw new RuntimeException("Failed to create: " + implClass, e);
        }
    }

    // --- Example usage ---
    interface Logger { void log(String msg); }
    interface UserRepo { String findUser(String id); }

    static class ConsoleLogger implements Logger {
        public void log(String msg) { System.out.println("[LOG] " + msg); }
    }

    static class InMemoryUserRepo implements UserRepo {
        private final Logger logger;
        public InMemoryUserRepo(Logger logger) { this.logger = logger; }
        public String findUser(String id) {
            logger.log("Finding user: " + id);
            return "User-" + id;
        }
    }

    static class UserService {
        private final UserRepo repo;
        private final Logger logger;
        public UserService(UserRepo repo, Logger logger) {
            this.repo = repo;
            this.logger = logger;
        }
        public String getUser(String id) {
            logger.log("Getting user");
            return repo.findUser(id);
        }
    }

    public static void main(String[] args) {
        DIContainer container = new DIContainer()
            .singleton(Logger.class, ConsoleLogger.class)
            .bind(UserRepo.class, InMemoryUserRepo.class);

        UserService service = container.resolve(UserService.class);
        System.out.println(service.getUser("42"));
    }
}

Use Cases

  • Understanding dependency injection internals
  • Lightweight IoC for small projects
  • Testing and mocking with manual DI

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.