Tiny, fast, zero-boilerplate runtime Dependency Injection (DI) framework for Java.
- Why Dimension-DI?
- Core Philosophy
- Requirements
- Installation
- Usage
- How It Works
- Design Notes: DI vs Service Locator
- Limitations
- API Cheatsheet
- Comparison Tables
- Documentation
- DI Containers Comparison
- Notice
- Contact
This framework provides dependency injection (DI) based on JSR-330 (jakarta.inject.*) annotations. It automatically discovers and wires your application's components through constructor injection, leveraging classpath scanning near a zero-configuration setup. Designed for simplicity and fast startup, it's perfect for smaller applications, microservices, and tools that need the benefits of DI without the overhead associated with larger frameworks like Spring, Guice, and Dagger 2.
Mermaid schema
graph TB
subgraph config["⚙️ Configuration JSR-330"]
direction LR
CA["@Inject<br/>Constructor"] --> CB["@Singleton<br/>Scope"] --> CC["@Named<br/>Qualifier"]
end
subgraph container["📦 DI Container"]
direction LR
ContainerDesc["<large>API Scans bytecode <br/>via JDK Class-File API</large>"] --> D1["DependencyScanner"] --> D2["Provider Registry"] --> D3["ServiceLocator"]
end
subgraph injection["💉 Dependency Resolution"]
direction LR
InjectionDesc["<large>Implements the Service<br/>Locator design pattern</large>"] --> I1["Request Type"] --> I2["Check Cache"] --> I3["Create Instance"] --> I4["Return Object"]
end
config -->|is analyzed by| container
container -->|resolves dependencies via| injection
injection -->|provides entrypoint to| APP["🎯 Your App<br/><small>Composition Root</small>"]
%% Styles
classDef configClass fill:#e3f2fd,stroke:#90caf9,stroke-width:2px,color:#333
classDef containerClass fill:#fff3e0,stroke:#ffcc80,stroke-width:2px,color:#333
classDef injectionClass fill:#f3e5f5,stroke:#ce93d8,stroke-width:2px,color:#333
classDef appClass fill:#e8f5e9,stroke:#a5d6a7,stroke-width:2px,color:#333
classDef descClass fill:none,stroke:none,color:#555,font-size:12px,font-style:italic
class CA,CB,CC configClass
class D1,D2,D3 containerClass
class I1,I2,I3,I4 injectionClass
class APP appClass
class CHeader descClass
Dimension-DI is a lightweight, runtime-oriented dependency injection container designed for simplicity and performance. It delivers the essential features you need without the complexity:
- Standards-Based: Uses JSR-330 (@Inject, @Named) for clean, constructor-injected code.
- Powerful & Safe: Features @Singleton scoping, circular dependency detection, and explicit binding for interfaces.
- Fast & Efficient: Employs classpath scanning via the JDK Class-File API (without loading classes) for rapid startup, working seamlessly from both directories and JARs.
- Minimal Overhead: No proxies, bytecode generation, or runtime magic—just a simple, thread-safe service locator under the hood that you'll never need to touch in your business logic.
Dimension-DI follows a two-phase approach:
- Build-Time Configuration: A fluent
BuilderAPI is used to configure the DI container. This phase involves scanning the classpath for components marked with@Inject, analyzing their dependencies, and registering providers (recipes for creating objects). This is where the "DI" part shines. - Runtime Resolution: At runtime, dependencies are resolved using an internal, globally accessible
ServiceLocator. While the implementation uses a Service Locator, the design encourages you to write your application code using pure Constructor Injection, decoupling your components from the DI framework itself.
- Java 25+ with JDK Class-File API.
To use Dimension-DI in your Maven project, add the following dependency to your pom.xml:
<dependency>
<groupId>ru.dimension</groupId>
<artifactId>di</artifactId>
<version>${revision}</version>
</dependency>Create your services and components using standard jakarta.inject.* annotations. Your classes should not have any knowledge of Dimension-DI.
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
@Singleton
class Config {
String url() { return "https://api.example.com"; }
}
class ApiClient {
private final Config config;
@Inject
ApiClient(Config config) {
this.config = config;
}
}
class App {
private final ApiClient api;
@Inject
App(ApiClient api) {
this.api = api;
}
void run() {}
}Scan your base packages and get entry point from ServiceLocator
import ru.dimension.di.DimensionDI;
import ru.dimension.di.ServiceLocator;
public class Main {
public static void main(String[] args) {
DimensionDI.builder()
.scanPackages("com.example")
.buildAndInit();
App app = ServiceLocator.get(App.class);
app.run();
}
}All dependencies are resolved via constructor injection. @Singleton classes are cached.
While constructor injection is highly recommended for mandatory dependencies, Dimension-DI also supports field and method injection. This is useful for optional dependencies or for integrating with frameworks that require it.
Field Injection
Annotate a non-final field with @Inject. The container will set its value after the object is constructed.
import jakarta.inject.Inject;
import jakarta.inject.Named;
class NotificationService {
@Inject private EmailSender emailSender;
@Inject @Named("sms")
private MessageSender smsSender;
public void notify(User user, String message) {
emailSender.send(user.getEmail(), message);
}
}Method Injection
Annotate a method with @Inject. The container will resolve its parameters and invoke it after construction.
class MyService {
private DependencyA depA;
// Must have an injectable constructor (e.g., public no-arg)
public MyService() {}
@Inject
public void initialize(DependencyA depA) {
// This method is called by the container after construction
this.depA = depA;
}
}Note: Members injection (both field and method) occurs after the constructor is called. The order of injection between fields and methods is not guaranteed. Final fields cannot be injected.
When injecting interfaces, add a binding so the container knows which implementation to use.
import jakarta.inject.Inject;
interface Transport { }
class HttpTransport implements Transport {
@Inject HttpTransport(Config cfg) { }
}
class Service {
@Inject Service(Transport transport) { }
}Bind interface to implementation
import ru.dimension.di.DimensionDI;
public class Main {
public static void main(String[] args) {
DimensionDI.builder()
.scanPackages("com.example")
.bind(Transport.class, HttpTransport.class)
.buildAndInit();
}
}import jakarta.inject.Inject;
import jakarta.inject.Named;
interface Cache {}
class RedisCache implements Cache { @Inject RedisCache(Config c) { } }
class InMemoryCache implements Cache { @Inject InMemoryCache() { } }
class Repository {
@Inject Repository(@Named("fast") Cache cache) { }
}import ru.dimension.di.DimensionDI;
public class Main {
public static void main(String[] args) {
DimensionDI.builder()
.scanPackages("com.example")
.bindNamed(Cache.class, "fast", InMemoryCache.class)
.bindNamed(Cache.class, "durable", RedisCache.class)
.buildAndInit();
}
}For objects that need custom construction logic (heavy init, load from file/env, etc)
import ru.dimension.di.DimensionDI;
public class Main {
public static void main(String[] args) {
DimensionDI.builder()
.scanPackages("com.example")
.provide(Config.class, ServiceLocator.singleton(() -> {
{ }
return new Config();
}))
.buildAndInit();
}
}Use ServiceLocator.singleton(supplier) to cache the result.
import ru.dimension.di.DimensionDI;
public class Main {
public static void main(String[] args) {
DimensionDI.builder()
.scanPackages("com.example")
.provideNamed(Cache.class, "fast", ServiceLocator.singleton(InMemoryCache::new))
.buildAndInit();
}
}If you cannot or do not want to use the Class-File API
import ru.dimension.di.DimensionDI;
import ru.dimension.di.ServiceLocator;
public class Main {
public static void main(String[] args) {
DimensionDI.builder()
.provide(Config.class, ServiceLocator.singleton(Config::new))
.provide(ApiClient.class, () -> new ApiClient(ServiceLocator.get(Config.class)))
.provide(App.class, () -> new App(ServiceLocator.get(ApiClient.class)))
.buildAndInit();
}
}Swap implementations in tests without changing source
import ru.dimension.di.DimensionDI;
import ru.dimension.di.ServiceLocator;
class FakeApiClient extends ApiClient { }
void setupTest() {
DimensionDI.builder()
.scanPackages("com.example")
.provide(ApiClient.class, FakeApiClient::new)
.buildAndInit();
}Override at runtime
@Test
void runTest() {
ServiceLocator.override(ServiceLocator.Key.of(ApiClient.class), FakeApiClient::new);
}Reset
@Test
void runTest() {
ServiceLocator.clear();
}- Scans configured packages for concrete classes with:
- an
@Injectconstructor, or - a public no-arg constructor
- an
- Reads
@Singletonand implemented interfaces - Uses the JDK Class-File API to inspect bytecode without loading classes
- Builds a provider map from scanned results
- Adds manual
bindandprovideentries (manual overrides win) - Initializes the ServiceLocator with providers
- Thread-safe registry of
Key -> Supplier<?> - Resolves constructor parameters on-demand (supports
@Named) - Performs members injection (fields and methods) after construction. Supports
@Injecton fields and methods, including@Namedqualifiers on fields and method parameters. - Detects circular dependencies and throws with a helpful stack
- Caches singletons via
SingletonSupplier
Note: Providers are keyed by concrete classes by default. Interfaces require explicit bind or bindNamed.
- You write normal constructor-injected code with
@Inject. This is DI-friendly. - Internally, the container uses a simple service locator
ServiceLocatorfor resolution. - Best practice: only call
ServiceLocator.get(...)at the composition root (for example, to get your top-levelApp).
- Only Jakarta Inject annotations are supported:
@Inject(on constructors and fields)@Singleton@Named(on constructor parameters and fields)
- Field injection works on
private,protected, andpublicnon-final fields, including inherited ones. - Not yet supported:
- Custom qualifiers beyond
@Named - Assisted injection,
Provider<T>, multi-bindings (collections), scopes beyond singleton
- Custom qualifiers beyond
- Scanning uses the JDK Class-File API.
DimensionDI.builder().scanPackages(...).bind(...).provide(...).buildAndInit();
ServiceLocator.get(MyRoot.class)
.provide(type, supplier).provideNamed(type, name, supplier).bind(interface, impl).bindNamed(interface, name, impl)
ServiceLocator.singleton(supplier)— Caches an instance.ServiceLocator.override(key, supplier)— Replaces a provider at runtime.ServiceLocator.alias(aliasKey, targetKey)— Creates an alias for a provider.ServiceLocator.clear()— Resets the entire registry.
| Feature | Dimension-DI | Spring IoC | Google Guice | Dagger 2 |
|---|---|---|---|---|
| Annotation Standard | JSR-330 (Jakarta) | Spring-specific + JSR-330 | JSR-330 | JSR-330 + custom |
| Dependency Injection | Constructor, field, method | Constructor, field, method | Constructor, field, method | Constructor-based |
| Learning Curve | ⭐ Minimal | ⭐⭐⭐⭐⭐ Steep | ⭐⭐⭐ Moderate | ⭐⭐⭐ Moderate |
| Performance | ⭐⭐⭐⭐⭐ Very High | ⭐⭐ Slow | ⭐⭐⭐ Medium | ⭐⭐⭐⭐⭐ Fastest |
| Startup Time | Ultra-fast | Slow | Fast | Instant (compile-time) |
| Runtime metadata | JDK Class-File API | Dynamic reflection | Dynamic reflection | None (compile-time) |
| Bytecode Generation | None | Extensive proxies | Extensive proxies | Compile-time only |
| Scoping | @Singleton | Request, Session, Singleton, Prototype | Singleton, custom | Singleton, custom |
| @Singleton Support | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| @Named Qualifiers | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| Custom Providers | ✅ provide() |
✅ @Bean |
✅ @Provides |
✅ @Provides |
| Field Injection | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes (members injection) |
| Method Injection | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes (members injection) |
| Collections/Multi-bind | ❌ No | ✅ Yes | ✅ Yes | ✅ Yes (@IntoSet/@IntoMap) |
| Circular Dependency Detection | ✅ Yes, explicit | ✅ Yes | ✅ Yes | ✅ Compile-time |
| Module/Config System | Fluent Builder | @Configuration + XML |
Module classes |
Component interface |
| Testing Support | ✅ Override, Clear | ✅ Profiles, Mocks | ✅ Binding override | ✅ Test components |
| JAR/Directory Scanning | ✅ Both | ✅ Both | Manual by default | N/A (compile-time) |
| Framework Size | ~19KB | ~10MB+ | ~782Kb | ~47Kb |
| Best For | Microservices, Tools, Minimal overhead | Enterprise apps, full web stack | Medium projects, modular | Android, compile-safety |
| Zero Configuration | ✅ Full classpath scan | Manual registration | Compile-time setup |
| Feature | Dimension-DI | PicoContainer | HK2 | Avaje Inject |
|---|---|---|---|---|
| Annotation Standard | JSR-330 | Custom only | JSR-330 | JSR-330 |
| Lightweight | ✅ Ultra-light | ✅ Very light | ✅ Light | |
| Classpath Scanning | ✅ Class-File API | ❌ Manual only | ✅ Yes | ✅ Yes |
| Constructor Injection | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| Field Injection | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| Method Injection | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| Scoping | @Singleton | Singleton | Singleton, request, custom | Singleton, custom |
| @Named Qualifiers | ✅ Yes | ❌ No | ✅ Yes | ✅ Yes |
| Custom Providers | ✅ provide() |
✅ Manual factories | ✅ @Factory |
✅ @Factory |
| Circular Dependency Detection | ✅ Explicit error | ❌ Runtime error | ✅ Yes | ✅ Yes |
| Performance | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Startup Time | Ultra-fast | Very fast | Fast | Fastest (compile-time) |
| Runtime Reflection | Minimal | Extensive | Moderate | None (compile-time) |
| Service Locator Pattern | ✅ Internal only | ✅ Primary model | ✅ HK2ServiceLocator | ✅ Internal only |
| Compilation Model | Runtime scan | Manual registration | Runtime scan | Compile-time (APT) |
| Maven Integration | ✅ Easy | ✅ Easy | ✅ Easy (Jersey) | ✅ Easy (APT) |
| Testing Support | ✅ Override, Clear | ✅ Rebind | ✅ Yes | ✅ Yes |
| Framework Size | ~19KB | ~327KB | ~131Kb | ~80KB |
| Active Development | ✅ Modern | ✅ Active | ✅ Active | |
| Jakarta Inject Ready | ✅ Full | ✅ Yes | ✅ Yes | |
| Best For | Microservices, fast startup | Embedded, custom, legacy | OSGi, modular systems | Compile-safe DI, GraalVM |
| Java Version | 25+ | 8+ | 8+ | 11+ |
Using source code of DI Containers Comparison project.
Processor: AMD Ryzen 5 5600H with Radeon Graphics, 3301 Mhz, 6 Core(s), 12 Logical Processor(s)
Memory: 16.0 GB
Disk: Generic Flash Disk USB Device<br>- SAMSUNG MZVL2512HCJQ-00B00 (476.94 GB)
OS: Microsoft Windows 11 (WSL2)
java version "25.0.1" 2025-10-21 LTS
Java(TM) SE Runtime Environment (build 25.0.1+8-LTS-27)
Java HotSpot(TM) 64-Bit Server VM (build 25.0.1+8-LTS-27, mixed mode, sharing)
| DI | Jar w/Deps Size, Mb | ⬇️ Wall, ms | User, ms | Sys, ms | Max RSS, Mb | Allocated, Mb | Alloc Count | LoC |
|---|---|---|---|---|---|---|---|---|
| jvm | 1.75 | 79.30 | 23.20 | 18.40 | 41.38 | 0.25 | 6 | 2 |
| DI | Jar w/Deps Size, Mb | ⬇️ Wall, ms | User, ms | Sys, ms | Max RSS, Mb | Allocated, Mb | Alloc Count | LoC |
|---|---|---|---|---|---|---|---|---|
| baseline | 1.75 | 100.30 | 47.00 | 22.10 | 43.54 | 0.25 | 6 | 24 |
| kotlin-lazy | 1.75 | 101.70 | 53.30 | 20.90 | 43.73 | 0.25 | 7 | 31 |
| dagger | 1.82 | 157.40 | 59.80 | 27.40 | 43.73 | 0.26 | 8 | 51 |
| cayennedi | 1.82 | 160.50 | 119.00 | 30.60 | 51.27 | 0.25 | 8 | 49 |
| dimension | 1.78 | 178.70 | 136.50 | 35.90 | 54.57 | 0.28 | 12 | 37 |
| kodein | 3.43 | 248.30 | 173.10 | 42.70 | 53.92 | 2.41 | 33 | 32 |
| koin | 2.22 | 283.90 | 180.90 | 49.80 | 58.18 | 2.15 | 38 | 31 |
| koin-reflect | 2.22 | 288.70 | 183.70 | 50.40 | 56.79 | 2.32 | 39 | 33 |
| komok-to-be-injected | 2.48 | 296.00 | 157.90 | 49.80 | 53.41 | 2.56 | 34 | 33 |
| bootique | 4.69 | 426.40 | 403.20 | 65.50 | 68.51 | 0.41 | 37 | 63 |
| spring-xml | 6.78 | 574.70 | 535.40 | 81.50 | 74.79 | 0.67 | 66 | 37 |
| guice | 5.62 | 620.00 | 489.70 | 88.70 | 69.18 | 0.62 | 60 | 47 |
| spring | 6.78 | 653.60 | 601.70 | 94.30 | 71.25 | 1.17 | 92 | 38 |
| spring-index | 6.78 | 669.70 | 652.60 | 90.20 | 74.07 | 0.95 | 100 | 34 |
| spring-scan | 6.78 | 704.20 | 666.50 | 115.10 | 73.60 | 1.05 | 96 | 34 |
| owb | 3.30 | 712.70 | 702.20 | 98.00 | 74.61 | 0.74 | 75 | 49 |
| springboot | 10.33 | 1,845.00 | 2,346.50 | 215.60 | 110.21 | 2.45 | 355 | 56 |
| springboot-index | 10.33 | 1,850.70 | 2,259.60 | 228.50 | 109.98 | 2.18 | 336 | 45 |
| DI | Jar w/Deps Size, Mb | ⬇️ Wall, ms | User, ms | Sys, ms | Max RSS, Mb | Allocated, Mb | Alloc Count | LoC |
|---|---|---|---|---|---|---|---|---|
| baseline-deep | 1.88 | 175.70 | 110.80 | 32.90 | 44.27 | 0.25 | 11 | 719 |
| kotlin-lazy-deep | 1.89 | 254.00 | 240.60 | 45.30 | 56.30 | 0.39 | 26 | 925 |
| dagger-deep | 2.05 | 278.90 | 180.40 | 44.40 | 47.96 | 0.26 | 14 | 1145 |
| cayennedi-deep | 2.00 | 305.00 | 308.80 | 53.10 | 58.54 | 0.27 | 19 | 1953 |
| dimension-deep | 1.91 | 376.60 | 464.80 | 53.00 | 67.68 | 0.63 | 46 | 831 |
| koin-deep | 2.37 | 418.20 | 371.00 | 65.10 | 67.83 | 2.68 | 57 | 725 |
| komok-to-be-injected-deep | 2.62 | 456.20 | 358.30 | 65.20 | 67.63 | 2.83 | 57 | 927 |
| koin-reflect-deep | 2.52 | 478.30 | 349.90 | 74.80 | 61.65 | 2.75 | 55 | 727 |
| kodein-deep | 3.86 | 480.30 | 424.40 | 75.30 | 69.57 | 1.92 | 59 | 726 |
| bootique-deep | 4.82 | 517.80 | 511.50 | 78.20 | 69.98 | 0.42 | 42 | 1057 |
| guice-deep | 5.75 | 689.80 | 628.50 | 104.40 | 74.25 | 0.64 | 74 | 1141 |
| spring-xml-deep | 6.90 | 868.60 | 843.30 | 118.60 | 75.45 | 1.24 | 94 | 931 |
| spring-deep | 6.91 | 1,066.80 | 1,189.30 | 140.30 | 75.31 | 0.98 | 123 | 1032 |
| owb-deep | 3.42 | 1,130.60 | 1,391.70 | 147.00 | 78.70 | 1.04 | 137 | 1143 |
| spring-index-deep | 6.91 | 1,187.30 | 1,521.40 | 141.90 | 81.28 | 1.67 | 213 | 729 |
| spring-scan-deep | 6.91 | 1,361.00 | 1,719.00 | 168.00 | 81.69 | 1.68 | 218 | 728 |
| springboot-index-deep | 10.46 | 2,234.70 | 3,027.00 | 247.60 | 112.44 | 2.99 | 451 | 739 |
| springboot-deep | 10.46 | 2,558.00 | 3,515.90 | 275.90 | 115.62 | 2.91 | 464 | 1050 |
| EN | RU |
|---|---|
| README in English | README на русском |
Section DI Containers Comparison was created using:
- Project: DI Containers Comparison
- Author: Ruslan Ibrahimau (Ibragimov)
- License: Creative Commons Attribution 4.0 International (CC BY 4.0)
Created by @akardapolov
