Software Development

Understanding Dependency Injection in Micronaut

Micronaut is a lightweight framework for microservices and serverless applications, offering fast startup times and low memory usage. One of its core features is Dependency Injection (DI), which allows components to be injected efficiently at runtime, reducing boilerplate code and enhancing modularity. This article will explore different ways to implement Dependency Injection in Micronaut.

1. Setting Up a Micronaut Project

Before diving into DI, let’s create a new Micronaut project using the CLI:

1
2
mn create-app com.jcg.micronautdi --build=maven --lang=java
cd micronautdi

This command generates a Micronaut project with Maven as the build tool. You can also use Gradle if preferred.

Start the application by running:

1
./mvnw mn:run

2. Understanding Dependency Injection

Dependency Injection (DI) is a design pattern that promotes loose coupling by injecting dependencies into a class rather than creating them internally. This approach improves modularity, testability, and maintainability, making applications more scalable and easier to manage. While many Java frameworks implement DI at runtime, Micronaut takes a different approach by compiling DI at build time, significantly improving performance.

Traditional frameworks use runtime DI, which relies on reflection to scan and manage dependencies. This process may slow startup time due to annotation scanning and proxy generation while also increasing memory consumption due to runtime metadata storage. In contrast, Micronaut eliminates runtime reflection by processing DI at compile time, resulting in faster startup times, lower memory usage, and better GraalVM compatibility. These advantages make Micronaut an excellent choice for cloud-native and serverless applications.

Now, let’s explore different ways to inject dependencies in Micronaut.

2. Constructor Injection in Micronaut

Constructor-based Dependency Injection is the most recommended approach in Micronaut. Let’s create a simple service and inject it into a controller.

Define the Service

Before injecting a service, we need to define a class annotated with @Singleton, which tells Micronaut that this class should have a single instance throughout the application.

1
2
3
4
5
6
@Singleton
public class GreetingService {
    public String greet(String name) {
        return "Hello, " + name + "!";
    }
}

The @Singleton annotation ensures that GreetingService is managed by Micronaut’s DI container, while the greet method generates a greeting message.

Inject the Service into a Controller

To use GreetingService, we inject it into a controller using constructor injection.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@Controller("/greet")
public class GreetingController {
 
    private final GreetingService greetingService;
 
    public GreetingController(GreetingService greetingService) {
        this.greetingService = greetingService;
    }
 
    @Get("/{name}")
    public String greet(String name) {
        return greetingService.greet(name);
    }
}

The @Singleton annotation ensures GreetingService is a singleton, meaning Micronaut creates only one instance of it. The GreetingController receives an instance of GreetingService through constructor injection, ensuring that the service is available when the controller is instantiated.

Now, accessing http://localhost:8080/greet/John will return:

1
Hello, John!

3. Field Injection in Micronaut

Field Injection allows dependencies to be injected directly into fields using @Inject.

Inject a Service Using Field Injection

01
02
03
04
05
06
07
08
09
10
11
@Controller("/greet2")
public class GreetingFieldController {
 
    @Inject
    private GreetingService greetingService;
 
    @Get("/{name}")
    public String greet(String name) {
        return greetingService.greet(name);
    }
}

The @Inject annotation injects GreetingService directly into the private field greetingService. While field injection reduces boilerplate code, it has drawbacks:

  • Makes dependencies less explicit, reducing clarity.
  • Harder to test, as dependencies cannot be injected via the constructor.

Because of these drawbacks, constructor injection is preferred.

4. Method Injection in Micronaut

Method Injection allows dependencies to be injected into a method, making it useful for optional dependencies or when dependencies are needed only in specific cases.

Inject a Service Using Method Injection

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Controller("/greet3")
public class GreetingMethodController {
 
    private GreetingService greetingService;
 
    @Inject
    public void setGreetingService(GreetingService greetingService) {
        this.greetingService = greetingService;
    }
 
    @Get("/{name}")
    public String greet(String name) {
        return greetingService.greet(name);
    }
}

The @Inject annotation on the setGreetingService method instructs Micronaut to inject the dependency at runtime. While this approach is useful for optional dependencies, it introduces mutability, which can make testing and debugging more challenging. For better maintainability and testability, constructor injection is generally preferred.

5. Using Annotations for Multiple Implementations

If multiple implementations of an interface exist, Micronaut needs to know which one to inject. This is handled using @Named("beanName") or @Primary. In many cases, different implementations of the same interface are needed to handle varying business logic. Micronaut allows defining multiple beans for the same interface and selecting the appropriate one when injecting dependencies.

Let’s consider a PaymentService interface with different implementations for processing payments.

Define an Interface

1
2
3
public interface PaymentService {
    String processPayment(double amount);
}

First Implementation: Credit Card Payment

1
2
3
4
5
6
7
@Singleton
public class CreditCardPaymentService implements PaymentService {
    @Override
    public String processPayment(double amount) {
        return "Processed credit card payment of $" + amount;
    }
}

Second Implementation: PayPal Payment

1
2
3
4
5
6
7
@Singleton
public class PayPalPaymentService implements PaymentService {
    @Override
    public String processPayment(double amount) {
        return "Processed PayPal payment of $" + amount;
    }
}

Here, we define a PaymentService interface and provide two implementations:

  • CreditCardPaymentService, which processes payments using credit cards.
  • PayPalPaymentService, which handles PayPal transactions.

Since both implementations are marked with @Singleton, Micronaut will recognize them as beans. However, without additional configuration, Micronaut wouldn’t know which implementation to inject. Next, we will see how to specify a particular implementation.

Inject a Specific Implementation

When multiple beans implement the same interface, Micronaut allows us to select the correct one using the @Named("beanName") or @Primary annotation. This ensures that the desired implementation is injected at runtime.

Update the Implementation Classes

1
2
3
4
5
6
7
8
@Singleton
@Named("creditCardPaymentService")
public class CreditCardPaymentService implements PaymentService {
    @Override
    public String processPayment(double amount) {
        return "Processed credit card payment of $" + amount;
    }
}
1
2
3
4
5
6
7
8
@Singleton
@Named("payPalPaymentService")
public class PayPalPaymentService implements PaymentService {
    @Override
    public String processPayment(double amount) {
        return "Processed PayPal payment of $" + amount;
    }
}

Inject a Specific Implementation Using @Named

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import jakarta.inject.Inject;
import jakarta.inject.Named;
 
@Controller("/payment")
public class PaymentController {
 
    private final PaymentService paymentService;
 
    @Inject
    public PaymentController(@Named("creditCardPaymentService") PaymentService paymentService) {
        this.paymentService = paymentService;
    }
 
    @Get("/{amount}")
    public String processPayment(double amount) {
        return paymentService.processPayment(amount);
    }
}

The @Named("creditCardPaymentService") annotation tells Micronaut to inject the CreditCardPaymentService specifically. This approach is useful when multiple implementations exist and a specific one needs to be selected. If no qualifier is provided, Micronaut will throw an error due to ambiguity in selecting the correct bean.

Test the Injected CreditCardPaymentService

Once the application is running, open a terminal and send a request using curl to test the PaymentController.

Expected Response

1
Processed credit card payment of $100.0

Modify the Injected Implementation

To change the injected service from CreditCardPaymentService to PayPalPaymentService, update the constructor in PaymentController like this:

1
2
3
public PaymentController(@Named("payPalPaymentService") PaymentService paymentService) {
    this.paymentService = paymentService;
}

Then, when the application restarts, test again:

Expected Response

1
Processed PayPal payment of $50.0

6. Injecting Multiple Dependencies

In Micronaut, a class can depend on multiple services to perform its tasks. Instead of injecting a single dependency, we can inject multiple different types of services into a class. This makes the application more modular, reusable, and testable.

Let’s consider an example where we have:

  • A WeatherService that provides the current weather.
  • A NotificationService that returns the current time.

We will inject both services into a controller and call them together.

Define the WeatherService Interface and Implementation

WeatherService Interface

1
2
3
public interface WeatherService {
    String getWeather();
}

WeatherService Implementation

1
2
3
4
5
6
7
@Singleton
public class DefaultWeatherService implements WeatherService {
    @Override
    public String getWeather() {
        return "It's a sunny day!";
    }
}

Define the NotificationService

This service will simply return the current time.

1
2
3
4
5
6
@Singleton
public class NotificationService {
    public String getCurrentTime() {
        return LocalTime.now().toString();
    }
}

Inject Multiple Dependencies in the Controller

Now, let’s inject both WeatherService and NotificationService into WeatherController.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Controller("/weather")
public class WeatherController {
 
    private final WeatherService weatherService;
    private final NotificationService notificationService;
 
    public WeatherController(WeatherService weatherService, NotificationService notificationService) {
        this.weatherService = weatherService;
        this.notificationService = notificationService;
    }
 
    @Get
    public String getWeatherUpdate() {
        return weatherService.getWeather() + " | Current time: " + notificationService.getCurrentTime();
    }
}

The WeatherController injects two dependencies: WeatherService, which provides the current weather, and NotificationService, which returns the current time. The getWeatherUpdate() method calls getWeather() from WeatherService and getCurrentTime() from NotificationService, then combines both results into a single response.

To test the /weather endpoint, run:

Expected Response

1
It's a sunny day! | Current time: 19:17:03.452424

Note: The time will change with each request.

7. Conclusion

In this article, we demonstrated how to use dependency injection in Micronaut, showcasing how services can be injected efficiently at compile time. Through our example, we saw how to inject both single and multiple dependencies in a controller, making the application more modular. With its lightweight architecture and fast execution, Micronaut is an excellent choice for high-performance applications requiring efficient dependency management.

8. Download the Source Code

This article explored the use of dependency injection in Micronaut.

Download
You can download the full source code of this example here: how to use dependency injection in micronaut

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest


This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button