A lightweight, annotation-based dependency injection framework for Java
Features β’ Installation β’ Quick Start β’ Documentation β’ Contributing
Released January 8, 2026
- Method Injection - Inject dependencies via setter methods with
@Inject - @Primary Beans - Mark default implementation when multiple candidates exist
- @PreDestroy Lifecycle - Cleanup callbacks on container shutdown
- Conditional Registration -
@ConditionalOnProperty,@ConditionalOnBean,@ConditionalOnMissingBean
See CHANGELOG.md for full details.
LightDI was created to provide a simple, lightweight, and educational dependency injection solution for Java applications. Unlike heavyweight frameworks like Spring or Guice, LightDI focuses on:
- πͺΆ Minimal footprint - No external dependencies, pure Java implementation
- π Educational value - Clean, readable source code perfect for learning DI internals
- β‘ Quick setup - Get started in minutes, not hours
- π― Focused functionality - Does one thing well: dependency injection
| Feature | Description |
|---|---|
| π§ Constructor Injection | Automatic dependency resolution via constructors |
| π Field Injection | Inject dependencies directly into fields with @Inject |
| π Method Injection | Inject dependencies via setter methods with @Inject |
| π Singleton Scope | Share single instance across all injection points |
| π Prototype Scope | Create new instance for each injection |
| π·οΈ Named Qualifiers | Support multiple implementations of same interface |
| β Primary Beans | Mark default bean with @Primary for ambiguous types |
| π΄ Lazy Loading | Delay bean creation until first use |
| π Circular Detection | Fail-fast with clear error messages |
| π¦ Package Scanning | Auto-discover injectable classes |
| π PostConstruct | Lifecycle callbacks after injection |
| π PreDestroy | Cleanup callbacks on container shutdown |
| β‘ Conditional Registration | Register beans based on properties or other beans |
| π οΈ Fluent Builder | Clean, readable container configuration |
Add JitPack repository and dependency to your pom.xml:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.abolpv</groupId>
<artifactId>lightdi</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>Add to your build.gradle:
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
implementation 'com.github.abolpv:lightdi:1.1.0'
}repositories {
maven("https://jitpack.io")
}
dependencies {
implementation("com.github.abolpv:lightdi:1.1.0")
}Clone and build from source:
git clone https://github.com/abolpv/lightdi.git
cd lightdi
mvn clean installimport io.github.abolpv.lightdi.annotation.*;
@Injectable
public class UserRepository {
public User findById(Long id) {
// Database logic here
return new User(id, "John Doe");
}
}
@Injectable
public class UserService {
private final UserRepository repository;
@Inject
public UserService(UserRepository repository) {
this.repository = repository;
}
public User getUser(Long id) {
return repository.findById(id);
}
}import io.github.abolpv.lightdi.container.Container;
public class Application {
public static void main(String[] args) {
// Create container and register classes
Container container = new Container();
container.register(UserRepository.class);
container.register(UserService.class);
// Get instance with dependencies injected
UserService service = container.get(UserService.class);
User user = service.getUser(1L);
System.out.println("Hello, " + user.getName());
}
}Container container = Container.builder()
.scan("com.example.services")
.scan("com.example.repositories")
.register(AppConfig.class)
.build();
UserService service = container.get(UserService.class);| Annotation | Target | Description |
|---|---|---|
@Injectable |
Class | Marks class as managed by the container |
@Inject |
Constructor, Field, Method | Marks injection point for dependencies |
@Singleton |
Class | Creates single shared instance |
@Primary |
Class | Marks bean as preferred when multiple candidates exist |
@Lazy |
Class, Field | Delays instantiation until first use |
@Named |
Class, Field, Parameter | Qualifies beans for disambiguation |
@PostConstruct |
Method | Invoked after all dependencies injected |
@PreDestroy |
Method | Invoked when container shuts down |
@ConditionalOnProperty |
Class | Register only when property matches |
@ConditionalOnBean |
Class | Register only when specified beans exist |
@ConditionalOnMissingBean |
Class | Register only when specified beans are absent |
@ComponentScan |
Class | Specifies packages to scan |
New instance created for each request:
@Injectable
public class RequestHandler {
// New instance every time container.get() is called
}Single instance shared across all injection points:
@Injectable
@Singleton
public class DatabaseConnection {
// Same instance always returned
private final Connection connection;
public DatabaseConnection() {
this.connection = createConnection();
}
}When you have multiple implementations of an interface:
// Define interface
public interface MessageSender {
void send(String message);
}
// Implementation 1
@Injectable
@Named("email")
public class EmailSender implements MessageSender {
@Override
public void send(String message) {
// Send via email
}
}
// Implementation 2
@Injectable
@Named("sms")
public class SmsSender implements MessageSender {
@Override
public void send(String message) {
// Send via SMS
}
}Inject by qualifier:
@Injectable
public class NotificationService {
@Inject
@Named("email")
private MessageSender emailSender;
@Inject
@Named("sms")
private MessageSender smsSender;
public void notifyAll(String message) {
emailSender.send(message);
smsSender.send(message);
}
}Or via constructor:
@Injectable
public class AlertService {
private final MessageSender sender;
@Inject
public AlertService(@Named("email") MessageSender sender) {
this.sender = sender;
}
}Or programmatically:
Container container = Container.builder()
.bind(MessageSender.class, EmailSender.class).named("email")
.bind(MessageSender.class, SmsSender.class).named("sms")
.build();
MessageSender email = container.get(MessageSender.class, "email");
MessageSender sms = container.get(MessageSender.class, "sms");Delay expensive initialization until first use:
@Injectable
@Lazy
public class ExpensiveService implements ServiceInterface {
public ExpensiveService() {
// Heavy initialization - only runs when first method called
loadLargeDataset();
}
}On specific injection points:
@Injectable
public class MyService {
@Inject
@Lazy
private ExpensiveService expensive;
public void doWork() {
// ExpensiveService created here on first access
expensive.process();
}
}
β οΈ Note: Lazy loading requires an interface type for proxy creation.
Execute initialization logic after all dependencies are injected:
@Injectable
@Singleton
public class CacheService {
@Inject
private DatabaseService database;
private Map<String, Object> cache;
@PostConstruct
public void initialize() {
// Called after database is injected
cache = new HashMap<>();
loadInitialData();
}
private void loadInitialData() {
// Load from database into cache
}
}Alternative to constructor injection:
@Injectable
public class OrderService {
@Inject
private UserService userService;
@Inject
private PaymentService paymentService;
@Inject
private InventoryService inventoryService;
public Order createOrder(Long userId, List<Item> items) {
User user = userService.getUser(userId);
// ... order logic
}
}π‘ Best Practice: Prefer constructor injection for required dependencies. Use field injection for optional dependencies or to avoid constructor bloat.
Inject dependencies via setter methods:
@Injectable
public class NotificationService {
private EmailSender emailSender;
private SmsSender smsSender;
@Inject
public void setEmailSender(EmailSender emailSender) {
this.emailSender = emailSender;
}
@Inject
public void setSmsSender(SmsSender smsSender) {
this.smsSender = smsSender;
}
// Or inject multiple dependencies in one method
@Inject
public void setDependencies(LogService log, MetricsService metrics) {
this.log = log;
this.metrics = metrics;
}
}Method injection works with @Named qualifiers on parameters:
@Injectable
public class AlertService {
private MessageSender sender;
@Inject
public void setSender(@Named("email") MessageSender sender) {
this.sender = sender;
}
}When multiple implementations of an interface exist, use @Primary to designate the default:
interface CacheService {
void put(String key, Object value);
}
@Injectable
@Primary // This will be injected by default
public class RedisCacheService implements CacheService {
@Override
public void put(String key, Object value) {
// Redis implementation
}
}
@Injectable
public class InMemoryCacheService implements CacheService {
@Override
public void put(String key, Object value) {
// In-memory implementation
}
}
@Injectable
public class DataService {
@Inject
private CacheService cache; // RedisCacheService will be injected
}
β οΈ Note: If multiple@Primarybeans exist for the same type, anAmbiguousBeanExceptionis thrown.
Execute cleanup logic when the container shuts down:
@Injectable
@Singleton
public class DatabaseConnection {
private Connection connection;
@PostConstruct
public void connect() {
connection = DriverManager.getConnection(url);
}
@PreDestroy
public void disconnect() {
// Called when container.shutdown() is invoked
if (connection != null) {
connection.close();
}
}
}
// Application code
Container container = Container.builder()
.register(DatabaseConnection.class)
.build();
// ... use container ...
// On shutdown - invokes @PreDestroy in reverse creation order
container.shutdown();π‘ Note:
@PreDestroymethods are called in reverse order of bean creation (LIFO), ensuring dependencies are cleaned up properly.
Register beans conditionally based on properties or other beans:
// Register only when cache.enabled=true
@Injectable
@ConditionalOnProperty("cache.enabled")
public class RedisCacheService implements CacheService { }
// Match specific value
@Injectable
@ConditionalOnProperty(value = "cache.type", havingValue = "memcached")
public class MemcachedCacheService implements CacheService { }
// Register when property is missing (fallback)
@Injectable
@ConditionalOnProperty(value = "feature.new", matchIfMissing = true)
public class DefaultFeatureService implements FeatureService { }// Register only when UserRepository exists
@Injectable
@ConditionalOnBean(UserRepository.class)
public class UserService {
@Inject
private UserRepository repository;
}// Register only when no CacheService exists (fallback pattern)
@Injectable
@ConditionalOnMissingBean(CacheService.class)
public class InMemoryCacheService implements CacheService { }Container container = Container.builder()
.property("cache.enabled", "true")
.property("db.type", "postgresql")
.register(RedisCacheService.class) // Registered (cache.enabled=true)
.register(InMemoryCacheService.class) // Skipped (CacheService exists)
.build();// =============== Registration ===============
// Register a single class
container.register(MyService.class);
// Register interface with implementation
container.register(UserRepository.class, JpaUserRepository.class);
// Register with qualifier name
container.register(MessageSender.class, EmailSender.class, "email");
// Register pre-created instance
container.registerInstance(Config.class, loadedConfig);
// Scan package for @Injectable classes
container.scan("com.example.services");
// =============== Retrieval ===============
// Get instance (dependencies auto-injected)
MyService service = container.get(MyService.class);
// Get named instance
MessageSender sender = container.get(MessageSender.class, "email");
// Get optional (no exception if not found)
Optional<MyService> optional = container.getOptional(MyService.class);
// Get all implementations of interface
List<MessageSender> allSenders = container.getAll(MessageSender.class);
// =============== Query ===============
// Check if registered
boolean exists = container.contains(MyService.class);
boolean namedExists = container.contains(MessageSender.class, "email");
// Get scope
Scope scope = container.getScope(MyService.class);
// Count registered beans
int count = container.size();
// Get all registered types
Set<Class<?>> types = container.getRegisteredTypes();
// =============== Properties ===============
// Set a property (for conditional registration)
container.setProperty("cache.enabled", "true");
// Get a property
String value = container.getProperty("cache.enabled");
String withDefault = container.getProperty("cache.type", "inmemory");
// Check if property exists
boolean hasProperty = container.hasProperty("cache.enabled");
// =============== Lifecycle ===============
// Shutdown container (invokes @PreDestroy)
container.shutdown();
// Check if shutdown was called
boolean isShutdown = container.isShutdown();
// Clear singleton cache (definitions remain)
container.clearSingletons();
// Clear everything
container.clear();Container container = Container.builder()
// Scan packages
.scan("com.example.services")
.scan("com.example.repositories", "com.example.controllers")
// Set properties (for conditional registration)
.property("cache.enabled", "true")
.property("db.type", "postgresql")
// Register individual classes
.register(AppConfig.class)
.register(SecurityService.class)
// Bind interfaces to implementations
.bind(UserRepository.class, JpaUserRepository.class)
.bind(CacheService.class, RedisCacheService.class)
// Named bindings
.bind(MessageSender.class, EmailSender.class).named("email")
.bind(MessageSender.class, SmsSender.class).named("sms")
// Pre-created instances
.instance(Configuration.class, loadConfig())
// Build the container
.build();LightDI provides clear, descriptive exceptions:
| Exception | Cause |
|---|---|
BeanNotFoundException |
Requested bean not registered in container |
CircularDependencyException |
Circular dependency detected (A β B β A) |
AmbiguousBeanException |
Multiple candidates found without qualifier |
ContainerException |
General container errors (instantiation, etc.) |
Example handling:
try {
UserService service = container.get(UserService.class);
} catch (BeanNotFoundException e) {
System.err.println("Bean not found: " + e.getRequestedType());
} catch (CircularDependencyException e) {
System.err.println("Circular dependency: " + e.getDependencyChain());
} catch (ContainerException e) {
System.err.println("Container error: " + e.getMessage());
}lightdi/
βββ src/
β βββ main/java/io/github/abolpv/lightdi/
β β βββ annotation/ # @Injectable, @Inject, @Singleton, etc.
β β βββ container/ # Container, ContainerBuilder, BeanDefinition
β β βββ resolver/ # Dependency resolution, circular detection
β β βββ proxy/ # Lazy loading proxy implementation
β β βββ scanner/ # Classpath scanning
β β βββ exception/ # Custom exceptions
β β βββ util/ # Reflection utilities
β βββ test/java/ # Unit tests
βββ assets/ # Logo and images
βββ pom.xml # Maven configuration
βββ LICENSE # Apache 2.0 License
βββ README.md # This file
-
Prefer Constructor Injection
- Makes dependencies explicit and immutable
- Easier to test with mock objects
- Fails fast if dependencies missing
-
Use Interfaces for Flexibility
- Enables lazy loading (proxy-based)
- Easier to swap implementations
- Better for testing
-
Keep Singletons Stateless
- Or ensure proper synchronization
- Avoid mutable shared state
-
Avoid Circular Dependencies
- Redesign if detected
- Consider using
@Lazyas temporary fix
-
Use @Named for Multiple Implementations
- Clear disambiguation
- Self-documenting code
- Java 17 or higher
- No additional dependencies
Contributions are welcome! Here's how you can help:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
# Clone the repository
git clone https://github.com/abolpv/lightdi.git
cd lightdi
# Build the project
mvn clean compile
# Run tests
mvn test
# Install to local repository
mvn installThis project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Copyright 2025-2026 Abolfazl Azizi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Abolfazl Azizi
GitHub
Made with β€οΈ for the Java community
β Star this repo if you find it useful!