SkillAgentSearch skills...

DependencyInjector

Lightweight, simple, and modern dependency injection framework for Java featuring constructor-based injection and automatic post-construction method invocation

Install / Use

/learn @neziw/DependencyInjector

README

<div align="center">

🚀 DependencyInjector

License: MIT Java Maven Central

</div>

Lightweight, simple, and modern dependency injection framework for Java featuring constructor-based injection and automatic post-construction method invocation


📋 Table of Contents


✨ Features

  • 🔌 Constructor Injection - Simple and intuitive constructor-based dependency injection
  • 🎯 Annotation-Driven - Clean and declarative @Inject and @PostConstruct annotations
  • 🚀 Lightweight - Minimal dependencies, zero runtime overhead
  • 🔧 Easy Integration - Simple API, easy to integrate into any Java project
  • 🛡️ Type-Safe - Compile-time type safety with Java generics
  • 📝 Post-Construction - Automatic invocation of @PostConstruct methods after object creation
  • 🎨 Clean Code - Encourages clean, testable, and maintainable code architecture
  • Fast - Reflection-based implementation with minimal overhead

📦 Requirements

  • Java: 17 or higher
  • Build Tool: Maven or Gradle (for dependency management)

❌ Why Field-Based Injection is Bad?

This framework intentionally supports only constructor-based injection and does not provide field-based injection. This is a deliberate design decision based on best practices and software engineering principles. Here's why field-based injection is problematic and why constructor injection is the superior approach:

🔴 Problems with Field-Based Injection

1. Immutability and Final Fields

Field-based injection requires non-final fields, making your objects mutable and potentially leaving them in an inconsistent state:

// ❌ BAD: Field injection
public class UserService {
    @Inject  // Field must be non-final
    private DatabaseService databaseService;  // Can be null, can be changed

    public void saveUser(User user) {
        // What if databaseService is null? No way to enforce it at compile time
        this.databaseService.save(user);
    }
}

With constructor injection, you can use final fields, ensuring immutability and thread-safety:


// ✅ GOOD: Constructor injection
public class UserService {

    private final DatabaseService databaseService;  // Final, immutable, thread-safe
    @Inject
    public UserService(final DatabaseService databaseService) {
        this.databaseService = databaseService;  // Guaranteed to be non-null
    }
    
    public void saveUser(User user) {
        this.databaseService.save(user);  // Always available
    }
}

2. Testability Issues

Field-based injection makes unit testing significantly more difficult. You must use reflection or rely on the dependency injection framework even in tests:

// ❌ BAD: Testing with field injection
public class UserServiceTest {
    @Test
    void testSaveUser() {
        UserService userService = new UserService();  // databaseService is null!
        // Must use reflection or a mock framework to inject
        // Reflection.setField(userService, "databaseService", mockDatabase);
        // This is error-prone and fragile
    }
}

Constructor injection makes testing straightforward and explicit:

// ✅ GOOD: Testing with constructor injection
public class UserServiceTest {

    @Test
    void testSaveUser() {
        DatabaseService mockDatabase = mock(DatabaseService.class);
        UserService userService = new UserService(mockDatabase);  // Clean and explicit
        // Test implementation
    }
}

3. Hidden Dependencies

Field-based injection hides dependencies. When you look at a class, you cannot immediately see what dependencies it requires without examining annotations and fields. This makes code harder to understand and maintain:

// ❌ BAD: Hidden dependencies
public class OrderService {
    @Inject
    private PaymentService paymentService;  // Hidden dependency
    @Inject
    private ShippingService shippingService;  // Hidden dependency
    @Inject

    private EmailService emailService;  // Hidden dependency
    // Looking at this class, it's not immediately clear what dependencies are needed
    // Must scan all fields to understand the class dependencies
}

Constructor injection makes dependencies explicit and visible:

// ✅ GOOD: Explicit dependencies
public class OrderService {
    private final PaymentService paymentService;
    private final ShippingService shippingService;
    private final EmailService emailService;

    @Inject
    public OrderService(
        final PaymentService paymentService,
        final ShippingService shippingService,
        final EmailService emailService
    ) {
        this.paymentService = paymentService;
        this.shippingService = shippingService;
        this.emailService = emailService;
    }
    // Dependencies are immediately visible in the constructor signature
    // Easy to understand what this class needs
}

4. Circular Dependency Detection

Field-based injection can hide circular dependencies until runtime, making them harder to detect and debug. Constructor injection exposes circular dependencies immediately, forcing you to resolve them during design:

// ❌ BAD: Circular dependency hidden with field injection
public class ServiceA {

    @Inject
    private ServiceB serviceB;  // Circular dependency not obvious

}

public class ServiceB {

    @Inject
    private ServiceA serviceA;  // Circular dependency not obvious

}
// This might work at runtime but creates tight coupling and design issues

Constructor injection makes circular dependencies impossible, encouraging better design:

// ✅ GOOD: Circular dependency impossible with constructor injection
// If you try to create ServiceA, you need ServiceB
// If you try to create ServiceB, you need ServiceA
// This immediately reveals the design problem and forces you to refactor

5. Null Safety

Field-based injection can leave objects in an invalid state where required dependencies are null. There's no compile-time guarantee that dependencies are injected. Constructor injection ensures that all required dependencies are provided before the object is created:

// ❌ BAD: Null safety issues
public class UserService {

    @Inject
    private DatabaseService databaseService;  // Could be null!

    public void saveUser(User user) {
        // Runtime NullPointerException if injection failed
        this.databaseService.save(user);
    }
}

Constructor injection guarantees non-null dependencies:


// ✅ GOOD: Null safety guaranteed
public class UserService {

    private final DatabaseService databaseService;  // Final, guaranteed non-null

    @Inject
    public UserService(final DatabaseService databaseService) {
        // Compiler and framework ensure this is never null
        this.databaseService = Objects.requireNonNull(databaseService);
    }
}

6. Framework Coupling

Field-based injection tightly couples your code to the dependency injection framework. Your classes cannot be instantiated without the framework, making them harder to reuse and test. Constructor injection allows classes to be instantiated normally, with or without the framework:

// ❌ BAD: Tight framework coupling
public class UserService {
    @Inject
    private DatabaseService databaseService;
    // Cannot create UserService without the DI framework
    // Must use reflection or framework-specific mechanisms
}

Constructor injection provides flexibility:

// ✅ GOOD: Framework-agnostic
public class UserService {

    private final DatabaseService databaseService;

    @Inject  // Optional: Framework can use this
    public UserService(final DatabaseService databaseService) {
        this.databaseService = databaseService;
    }
    // Can still be created normally: new UserService(databaseService)
    // Framework is optional, not required
}

7. Order of Initialization

Field-based injection makes the order of initialization unclear. Dependencies might be injected in an unpredictable order, leading to initialization issues. Constructor injection ensures a clear, predictable initialization order:

// ❌ BAD: Unclear initialization order
public class ServiceA {

    @Inject
    private ServiceB serviceB;

    @PostConstruct
    void init() {
        // Is serviceB injected before this runs? Unclear
        this.serviceB.doSomething();
    }
}

Constructor injection provides a clear sequence: constructor → field assignment → @PostConstruct methods:

// ✅ GOOD: Clear initialization order
public class ServiceA {

    private final ServiceB serviceB;

    @Inject
    public ServiceA(final ServiceB serviceB) {
        // 1. Constructor runs first
        this.serviceB = serviceB;  // 2. Fields assigned
    }

    @PostConstruct
    void init() {
        // 3. Post-construct runs last, all dependencies guaranteed available
        this.serviceB.doSomething();
    }
}

✅ Why Constructor Injection is Superior

Constructor injection provides numerous benefits that field injection cannot match:

  1. Immutability - Enables final fields, ensuring objects are immutable and thread-safe
  2. Explicit Dependencies - Dependencies are visible in the constructor signature, making code self-documenting
  3. Testability -
View on GitHub
GitHub Stars4
CategoryDevelopment
Updated6d ago
Forks0

Languages

Java

Security Score

90/100

Audited on Mar 24, 2026

No findings