Understanding Micronaut's Dependency Injection System

Compile-Time Dependency Injection

Unlike Spring, which uses reflection for DI, Micronaut compiles all bean metadata at build time. This offers major performance benefits but introduces tight coupling between code structure and bean visibility. For a bean to be discoverable, it must be in a classpath-visible module, annotated correctly, and not removed by code minimization or obfuscation.

Common Bean Declaration Styles

  • Using @Singleton on class definitions
  • Defining @Bean methods inside @Factory classes
  • Creating conditional beans via @Requires annotations

Problems arise when these definitions reside in separate modules without proper indexing or when they are unintentionally excluded from build-time analysis.

Root Cause: Bean Metadata Not Compiled or Discovered

Symptom Patterns

  • Application starts but fails with NullPointerException during injection
  • Beans defined in libraries are never injected even when @Factory is present
  • Native image fails to resolve certain beans or class proxies

Technical Root Causes

  • Missing micronaut-inject-java or incorrect annotation processors in dependent modules
  • Factory class not compiled due to wrong mainClassName configuration
  • Shadow plugin shading the factory class into an inaccessible package
  • GraalVM native-image rejecting classes lacking explicit configuration in reflect-config.json
// Example: Factory bean in library module never discovered
@Factory
public class ExternalBeanFactory {
    @Singleton
    public ExternalService externalService() {
        return new ExternalService();
    }
}

How to Diagnose the Issue

Step-by-Step Diagnostic Process

  1. Run the application with --trace log level to identify failed injections
  2. Use micronaut-cli or javap to inspect generated beanDefinition metadata
  3. Verify that micronaut-inject-java is listed in the library's annotationProcessorPaths
  4. Check GraalVM native-image logs for missing reflect configuration warnings

Useful CLI Command

./gradlew classes --info | grep micronaut.processing

This confirms whether the Micronaut annotation processors are being invoked during compilation.

Common Pitfalls in Multi-Module Micronaut Projects

1. Missing Annotation Processing

If a submodule does not explicitly apply Micronaut's annotation processor, beans in that module will not be visible at runtime.

2. Shadowed Beans in Fat JARs

Shaded JARs (via Gradle Shadow plugin) can move beans to unexpected packages, breaking Micronaut's AOT assumptions.

3. GraalVM Native Image Limitations

Without reflection configuration, GraalVM will exclude factory methods or proxies not declared in reflect-config.json.

How to Fix the Problem

Short-Term Fixes

  • Ensure all modules declare annotationProcessor 'io.micronaut:micronaut-inject-java'
  • Avoid shading factory or configuration classes unless explicitly re-indexed
  • Use @Context to force bean initialization if needed

Long-Term Solutions

  • Modularize Micronaut projects with micronaut-library plugin applied in all reusable modules
  • Use micronaut-cli to generate factory stubs with correct annotations
  • Enable strict CI checks for bean injection validation using test containers
  • When using GraalVM, maintain and version-control reflect-config.json and resource-config.json
// build.gradle snippet for correct annotation processing
dependencies {
    annotationProcessor "io.micronaut:micronaut-inject-java"
    implementation "io.micronaut:micronaut-runtime"
}

apply plugin: "io.micronaut.application"

Best Practices

  • Always build with --stacktrace and --info during troubleshooting
  • Run tests with native-image enabled to simulate real-world deployments
  • Use a unified version catalog for Micronaut dependencies across modules
  • Centralize @Factory and @Configuration classes in a dedicated DI module

Conclusion

Micronaut offers powerful DI and startup performance, but its compile-time injection strategy makes it sensitive to project structure, annotation processing, and module boundaries. By enforcing consistent build configurations, applying the correct annotation processors, and treating DI visibility as a first-class architectural concern, teams can ensure reliability even at scale. Especially in multi-module or GraalVM-native projects, early validation and structure-aware testing are key to avoiding injection-time surprises.

FAQs

1. Why are my @Factory beans not being injected?

They likely reside in a module that lacks annotation processing. Ensure the module applies the Micronaut plugin and annotation processor.

2. Can I use shaded JARs with Micronaut safely?

Only if you take care to preserve bean metadata and avoid relocating packages that contain @Factory or @Configuration classes.

3. Why does GraalVM native-image fail to resolve Micronaut beans?

Micronaut relies on AOT metadata. If GraalVM excludes reflection proxies or resources, the beans won't be initialized. Explicit reflect-config is needed.

4. How do I debug Micronaut bean discovery at runtime?

Enable TRACE logging and inspect the startup logs for bean registration messages. You can also manually inspect the build output for generated bean metadata.

5. What's the best way to structure a multi-module Micronaut project?

Use a dedicated module for DI-related classes, apply the Micronaut library plugin in all modules, and centralize shared configurations and beans.