Manual Dependency Injection Container
Build a simple DI container with constructor injection, singleton scope, and interface binding.
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.
Builder Pattern — Fluent Object Construction
Implement the Builder pattern for complex objects with validation, immutability, and method chaining.
Best for: Constructing complex objects with many optional parameters
Singleton Pattern — Thread-Safe Approaches
Implement thread-safe singletons in Java: enum, holder class, double-checked locking, and eager init.
Best for: Application-wide configuration managers
Strategy Pattern with Lambdas
Implement the Strategy pattern using interfaces and Java lambdas for flexible algorithm selection.
Best for: Swappable pricing or discount algorithms
Factory Method Pattern with Registry
Implement the Factory pattern using a registry map for extensible object creation without switch statements.
Best for: Extensible object creation in plugin architectures