Micronaut Scope Types

Learn about the available scopes: Singleton, Prototype, Request, Refreshable, Context…​

Authors: Sergio del Amo

Micronaut Version: 4.4.2

1. Introduction

Micronaut features an extensible bean scoping mechanism based on JSR-330.

2. What you will need

To complete this guide, you will need the following:

3. Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

4. Writing the Application

Create an application using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app example.micronaut.micronautguide \
    --features=junit-params,management,validation \
    --build=gradle \
    --lang=kotlin \
    --test=junit
If you don’t specify the --build argument, Gradle is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.
If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.

The previous command creates a Micronaut application with the default package example.micronaut in a directory named micronautguide.

If you use Micronaut Launch, select Micronaut Application as application type and add junit-params, management, and validation features.

If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features and apply those changes to your application.

5. Scenario

We use the following scenario to talk about the different types of scopes.

The following @Controller injects two collaborators.

src/main/kotlin/example/micronaut/singleton/RobotController.kt
package example.micronaut.singleton

import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/singleton") (1)

class RobotController(
    private val robotFather: RobotFather, (2)
    private val robotMother: RobotMother (3)
) {

    @Get (4)
    fun children(): List<String> {
        return listOf(robotMother.child().getSerialNumber(), robotFather.child().getSerialNumber())
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /.
2 Use constructor injection to inject a bean of type RobotFather.
3 Use constructor injection to inject a bean of type RobotMother.
4 The @Get annotation maps the children method to an HTTP GET request on /.

Each collaborator has an injection point for a bean of type Robot.

src/main/kotlin/example/micronaut/singleton/RobotFather.kt
package example.micronaut.singleton

import jakarta.inject.Singleton

@Singleton (1)
class RobotFather(private val robot: Robot) { (2)
    fun child() = robot
}
1 Use jakarta.inject.Singleton to designate a class as a singleton.
2 Use constructor injection to inject a bean of type Robot.
src/main/kotlin/example/micronaut/singleton/RobotMother.kt
package example.micronaut.singleton

import jakarta.inject.Singleton

@Singleton (1)
class RobotMother(private val robot: Robot) { (2)
    fun child() = robot
}
1 Use jakarta.inject.Singleton to designate a class as a singleton.
2 Use constructor injection to inject a bean of type Robot.

Let’s discuss how the application behaves depending on the scope used for the bean of type Robot.

6. Singleton

Singleton scope indicates only one instance of the bean will exist.

To define a singleton, annotate a class with jakarta.inject.Singleton at the class level.

The following class creates a unique identifier in the constructor. This identifier allows us to identify how many Robot instances are used.

src/main/kotlin/example/micronaut/singleton/Robot.kt
package example.micronaut.singleton

import jakarta.inject.Singleton
import java.util.UUID

@Singleton (1)
class Robot {
    private val serialNumber: String = UUID.randomUUID().toString()

    fun getSerialNumber(): String {
        return serialNumber
    }
}
1 Use jakarta.inject.Singleton to designate a class as a singleton.

6.1. Singleton Test

To use the testing features described in this section, add the following dependency to your build file:

build.gradle
testImplementation("org.junit.jupiter:junit-jupiter-params")

The following test verifies @Singleton behavior.

src/test/kotlin/example/micronaut/SingletonScopeTest.kt
package example.micronaut
*/

import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource


@MicronautTest (1)
class SingletonScopeTest(@Client("/") val httpClient: HttpClient) { (2)

    @ParameterizedTest
    @ValueSource(strings = ["/singleton"])
    fun onlyOneInstanceOfTheBeanExistsForSingletonBeans(path: String) {
        val responses = executeRequest(httpClient, path).toMutableSet()
        assertEquals(1, responses.size) (3)
        responses.addAll(executeRequest(httpClient, path))
        assertEquals(1, responses.size) (4)
    }

    private fun executeRequest(client: HttpClient, path: String): List<String> {
        return client.toBlocking().retrieve(
            HttpRequest.GET<Any>(path),
            Argument.listOf(String::class.java)
        )
    }

}


import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource


@MicronautTest (1)
class SingletonScopeTest(@Client("/") val httpClient: HttpClient) { (2)

    @ParameterizedTest
    @ValueSource(strings = ["/singleton"])

    fun onlyOneInstanceOfTheBeanExistsForSingletonBeans(path: String) {
        val responses = executeRequest(httpClient, path).toMutableSet()
        assertEquals(1, responses.size) (3)
        responses.addAll(executeRequest(httpClient, path))
        assertEquals(1, responses.size) (4)
    }

    private fun executeRequest(client: HttpClient, path: String): List<String> {
        return client.toBlocking().retrieve(
            HttpRequest.GET<Any>(path),
            Argument.listOf(String::class.java)
        )
    }

}
1 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
2 Inject the HttpClient bean and point it to the embedded server.
3 The same instance fulfills both injection points at RobotFather and RobotMother.
4 Same instance is used upon subsequent requests.

7. Prototype

Prototype scope indicates that a new instance of the bean is created each time it is injected

Let’s use @Prototype instead of @Singleton.

src/main/kotlin/example/micronaut/prototype/Robot.kt
package example.micronaut.prototype

import io.micronaut.context.annotation.Prototype
import java.util.UUID

@Prototype (1)
class Robot {
    private val serialNumber: String = UUID.randomUUID().toString()

    fun getSerialNumber(): String {
        return serialNumber
    }
}
1 Use io.micronaut.context.annotation.Prototype to designate the scope of bean as Prototype - a non-singleton scope that creates a new bean for every injection point.

7.1. Prototype Tests

The following test verifies the behavior of Prototype scope.

src/test/kotlin/example/micronaut/PrototypeScopeTest.kt
package example.micronaut
*/

import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource

@MicronautTest (1)
class PrototypeScopeTest(@Client("/") val httpClient: HttpClient) { (2)

    @ParameterizedTest
    @ValueSource(strings = ["/prototype"])

    fun prototypeScopeIndicatesThatANewInstanceOfTheBeanIsCreatedEachTimeItIsInjected(path: String) {
        val responses = executeRequest(httpClient, path).toMutableSet()
        assertEquals(2, responses.size) (3)
        responses.addAll(executeRequest(httpClient, path))
        assertEquals(2, responses.size) (4)
    }

    private fun executeRequest(client: HttpClient, path: String): List<String> {
        return client.toBlocking().retrieve(
            HttpRequest.GET<Any>(path),
            Argument.listOf(String::class.java)
        )
    }

}


import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource

@MicronautTest (1)
class PrototypeScopeTest(@Client("/") val httpClient: HttpClient) { (2)

    @ParameterizedTest
    @ValueSource(strings = ["/prototype"])

    fun prototypeScopeIndicatesThatANewInstanceOfTheBeanIsCreatedEachTimeItIsInjected(path: String) {
        val responses = executeRequest(httpClient, path).toMutableSet()
        assertEquals(2, responses.size) (3)
        responses.addAll(executeRequest(httpClient, path))
        assertEquals(2, responses.size) (4)
    }

    private fun executeRequest(client: HttpClient, path: String): List<String> {
        return client.toBlocking().retrieve(
            HttpRequest.GET<Any>(path),
            Argument.listOf(String::class.java)
        )
    }
1 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
2 Inject the HttpClient bean and point it to the embedded server.
3 A new instance is created to fulfill each injection point. Two instances - one for RobotFather and another for RobotMother.
4 Instances remain upon subsequent requests.

8. Request

@RequestScope scope is a custom scope that indicates a new instance of the bean is created and associated with each HTTP request

src/main/kotlin/example/micronaut/request/Robot.kt
package example.micronaut.request

import io.micronaut.http.HttpRequest
import io.micronaut.runtime.http.scope.RequestAware
import io.micronaut.runtime.http.scope.RequestScope

@RequestScope (1)
class Robot : RequestAware { (2)
    private var serialNumber: String? = null

    override fun setRequest(request: HttpRequest<*>?) {
        this.serialNumber = request!!.headers.get("UUID")
    }

    fun getSerialNumber(): String {
        return serialNumber!!
    }
}
1 Use io.micronaut.runtime.http.scope.RequestScope to designate the scope of bean as Request - a new instance of the bean is created and associated with each HTTP request.
2 RequestAware API allows @RequestScope beans to access to the current request.

8.1. Request Tests

The following test verifies the behavior of @RequestScope scope.

src/test/kotlin/example/micronaut/RequestScopeTest.kt
package example.micronaut
*/

import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import jakarta.inject.Inject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import java.util.UUID

@MicronautTest (1)
class RequestScopeTest(@Client("/") val httpClient: HttpClient) { (2)

    @ParameterizedTest
    @ValueSource(strings = ["/request"])
    fun requestScopeScopeIsACustomScopeThatIndicatesANewInstanceOfTheBeanIsCreatedAndAssociatedWithEachHTTPRequest(path: String) {
        val responses = executeRequest(httpClient, createRequest(path)).toMutableSet()
        assertEquals(1, responses.size) (3)
        responses.addAll(executeRequest(httpClient, createRequest(path)))
        assertEquals(2, responses.size) (4)
    }

    private fun executeRequest(client: HttpClient, request: HttpRequest<Any>): List<String> {
        return client.toBlocking().retrieve(
            request,
            Argument.listOf(String::class.java)
        )
    }

    private fun createRequest(path: String): HttpRequest<Any> {
        return HttpRequest.GET<Any>(path).header("UUID", UUID.randomUUID().toString())
    }
}


import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import jakarta.inject.Inject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import java.util.UUID

@MicronautTest (1)
class RequestScopeTest(@Client("/") val httpClient: HttpClient) { (2)

    @ParameterizedTest
    @ValueSource(strings = ["/request"])
    fun requestScopeScopeIsACustomScopeThatIndicatesANewInstanceOfTheBeanIsCreatedAndAssociatedWithEachHTTPRequest(path: String) {
        val responses = executeRequest(httpClient, createRequest(path)).toMutableSet()
        assertEquals(1, responses.size) (3)
        responses.addAll(executeRequest(httpClient, createRequest(path)))
        assertEquals(2, responses.size) (4)
    }

    private fun executeRequest(client: HttpClient, request: HttpRequest<Any>): List<String> {
        return client.toBlocking().retrieve(
            request,
            Argument.listOf(String::class.java)
        )
    }

    private fun createRequest(path: String): HttpRequest<Any> {
        return HttpRequest.GET<Any>(path).header("UUID", UUID.randomUUID().toString())
    }
}
1 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
2 Inject the HttpClient bean and point it to the embedded server.
3 Both injection points, RobotFather and RobotMother, are fulfilled with the same instance of Robot. An instance associated with the HTTP Request.
4 Both injection points are fulfilled with the new instance of Robot.

9. Refreshable

Refreshable scope is a custom scope that allows a bean’s state to be refreshed via the /refresh endpoint.

src/main/kotlin/example/micronaut/refreshable/Robot.kt
package example.micronaut.refreshable

import io.micronaut.runtime.context.scope.Refreshable
import java.util.UUID

@Refreshable (1)
class Robot {
    private val serialNumber: String = UUID.randomUUID().toString()

    fun getSerialNumber(): String {
        return serialNumber
    }
}
1 @Refreshable scope is a custom scope that allows a bean’s state to be refreshed via the /refresh endpoint.

Your application needs the management dependency to enable the refresh endpoint.

build.gradle
implementation("io.micronaut:micronaut-management")

9.1. Refreshable Tests

The following test enables the refresh endpoint and verifies the behavior of @Refreshable

src/test/kotlin/example/micronaut/RefreshableScopeTest.kt
package example.micronaut
*/

import io.micronaut.context.annotation.Property
import io.micronaut.core.type.Argument
import io.micronaut.core.util.StringUtils
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource

@Property(name = "endpoints.refresh.enabled", value = StringUtils.TRUE) (1)
@Property(name = "endpoints.refresh.sensitive", value = StringUtils.FALSE) (2)
@MicronautTest (3)
class RefreshableScopeTest(@Client("/") val httpClient: HttpClient) { (4)

    @ParameterizedTest
    @ValueSource(strings = ["/refreshable"])
    fun refreshableScopeIsACustomScopeThatAllowsABeansStateToBeRefreshedViaTheRefreshEndpoint(path: String) {
        val responses = executeRequest(httpClient, path).toMutableSet()
        assertEquals(1, responses.size) (5)
        responses.addAll(executeRequest(httpClient, path))
        assertEquals(1, responses.size) (6)
        refresh() (7)
        responses.addAll(executeRequest(httpClient, path))
        assertEquals(2, responses.size) (8)
    }

    private fun executeRequest(client: HttpClient, path: String): List<String> {
        return client.toBlocking().retrieve(
            HttpRequest.GET<Any>(path),
            Argument.listOf(String::class.java)
        )
    }

    private fun refresh() {
        httpClient.toBlocking().exchange<Any, Any>(
            HttpRequest.POST(
                "/refresh",
                mapOf("force" to true)
            )
        )
    }

}


import io.micronaut.context.annotation.Property
import io.micronaut.core.type.Argument
import io.micronaut.core.util.StringUtils
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource

@Property(name = "endpoints.refresh.enabled", value = StringUtils.TRUE) (1)
@Property(name = "endpoints.refresh.sensitive", value = StringUtils.FALSE) (2)
@MicronautTest (3)
class RefreshableScopeTest(@Client("/") val httpClient: HttpClient) { (4)

    @ParameterizedTest
    @ValueSource(strings = ["/refreshable"])
    fun refreshableScopeIsACustomScopeThatAllowsABeansStateToBeRefreshedViaTheRefreshEndpoint(path: String) {
        val responses = executeRequest(httpClient, path).toMutableSet()
        assertEquals(1, responses.size) (5)
        responses.addAll(executeRequest(httpClient, path))
        assertEquals(1, responses.size) (6)
        refresh() (7)
        responses.addAll(executeRequest(httpClient, path))
        assertEquals(2, responses.size) (8)
    }

    private fun executeRequest(client: HttpClient, path: String): List<String> {
        return client.toBlocking().retrieve(
            HttpRequest.GET<Any>(path),
            Argument.listOf(String::class.java)
        )
    }

    private fun refresh() {
        httpClient.toBlocking().exchange<Any, Any>(
            HttpRequest.POST(
                "/refresh",
                mapOf("force" to true)
            )
        )
    }

}
1 Annotate the class with @Property to supply configuration to the test.
2 The refresh endpoint is sensitive by default. To invoke it in the test, we set endpoints.refresh.sensitive to false.
3 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
4 Inject the HttpClient bean and point it to the embedded server.
5 The same instance fulfills both injection points at RobotFather and RobotMother.
6 The same instance serves a new request.
7 Hitting the refresh endpoint, publishes a RefreshEvent, which invalidates the instance of Robot.
8 A new instance of Robot is created the next time the object is requested.

10. @Context

Context scope indicates that the bean will be created at the same time as the ApplicationContext (eager initialization)

The following example uses @Context in combination with @ConfigurationProperties.

src/main/kotlin/example/micronaut/context/MicronautConfiguration.kt
package example.micronaut.context

import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.context.annotation.Context
import jakarta.validation.constraints.Pattern

@Context (1)
@ConfigurationProperties("micronaut") (2)
class MicronautConfiguration {

    @field:Pattern(regexp = "groovy|java|kotlin") (3)
    var language: String? = null

}
1 The life cycle of classes annotated with io.micronaut.context.annotation.Context is bound to that of the bean context.
2 The @ConfigurationProperties annotation takes the configuration prefix.
3 Use jakarta.validation.constraints Constraints to ensure the data matches your expectations.

The result is validation being performed on the Application Context start-up.

src/test/kotlin/example/micronaut/ContextTest.kt
package example.micronaut
/*
//tag::package[]
package example.micronaut
//tag::package[]
*/
//tag::imports[]

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.BeanInstantiationException
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

class ContextTest {

    @Test
    fun lifeCycleOfClassesAnnotatedWithAtContextIsBoundToThatOfTheBeanContext() {

        val thrown =
            assertThrows<BeanInstantiationException> { ApplicationContext.run(mapOf("micronaut.language" to "scala")) }
        assertTrue(thrown.message!!.contains("must match \"groovy|java|kotlin\""))
    }
}

11. Other scopes

Micronaut Framework ships with other built-in Scopes:

11.1. @Infrastructure

@Infrastructure scope represents a bean that cannot be overridden or replaced using @Replaces because it is critical to the functioning of the system.

11.2. @ThreadLocal

@ThreadLocal scope is a custom scope that associates a bean per thread via a ThreadLocal

11.3. Next Steps

Read more about Scopes in the Micronaut Framework.

12. Help with the Micronaut Framework

The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.

13. License

All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…​).