Skip to content

Events, Caching, and Scheduling

Spring's reactive support for application events, the caching abstraction, and task scheduling extends naturally to Kotlin coroutines. This page collects the coroutine-friendly patterns from the samples.

Application events

An @EventListener supports suspend functions directly: since Spring Framework 6.1 the listener method can be declared suspend (or return Flow), and Spring bridges the coroutine completion into the event multicaster:

@Component
class GreetingListener {

    @EventListener
    suspend fun onGreetingEvent(event: Greeting) {
        log.debug("received event: {}", event)
        // ... suspend calls inside the listener body
    }
}

This mirrors the Java pattern in Application Events — publish with ApplicationEventPublisher, observe with @EventListener — with the handler body written in coroutine style.

When the surrounding contract requires a blocking method — Spring Boot lifecycle hooks such as CommandLineRunner, for example — wrap the coroutine work in runBlocking { ... }, as the boot-bean-registrar-kotlin-dsl sample does for data initialization:

registerBean {
    CommandLineRunner {
        runBlocking { bean<PostRepository>().initializeSampleData() }
    }
}

When the hook is an event listener, you can skip runBlocking entirely — the boot-kotlin-co sample declares its ApplicationReadyEvent listener as a suspend function, like the GreetingListener above.

Scheduling

A @Scheduled method may be a suspend function that awaits a reactive pipeline with awaitSingle:

@Component
class KotlinCounter {

    private val count = AtomicInteger(0)

    @Scheduled(fixedDelay = 5)
    suspend fun scheduled(): Void {
        return Mono.defer { Mono.just(count.incrementAndGet()) }
            .doOnNext { item: Int? -> log.debug("current count:{}", item) }
            .then()
            .awaitSingle()
    }

    suspend fun getInvocationCount(): Int {
        return count.get()
    }
}

The scheduler subscribes to the coroutine completion and waits for it before scheduling the next execution — the same semantics as the Java Scheduling example, with awaitSingle() (from kotlinx.coroutines.reactor) bridging the Mono into the coroutine world.

The test asserts the counter advanced using runTest:

@SpringJUnitConfig(value = [TestConfig::class])
class KotlinCounterTest {

    @Autowired
    lateinit var counter: KotlinCounter

    @Test
    fun givenSleepBy100ms_whenGetInvocationCount_thenIsGreaterThanZero() = runTest {
        assertThat(counter.getInvocationCount()).isGreaterThan(0)
    }
}

Caching

The reactive caching support added in Spring 6.1 covers Kotlin coroutines as well: @Cacheable/@CachePut/@CacheEvict apply to suspend functions and functions returning Flow, and the cache stores the completion rather than the publisher. Configure the cache manager exactly as in the Java chapter — ConcurrentMapCacheManager, async-mode CaffeineCacheManager, or RedisCacheManager — and annotate the repository or service methods:

@Cacheable(value = "posts", key = "#id")
suspend fun findById(id: UUID): Post? = ...

JSON with kotlinx.serialization

Beyond Jackson, Spring Boot 4 adds a first-class starter for kotlinx.serialization: spring-boot-starter-kotlinx-serialization-json (plus its -test variant). A customizer tunes the Json instance:

@OptIn(ExperimentalSerializationApi::class)
@Configuration(proxyBeanMethods = false)
class CustomConfig {

    @Bean
    fun kotlinxSerializationJsonBuilderCustomizer(): KotlinxSerializationJsonBuilderCustomizer {
        return KotlinxSerializationJsonBuilderCustomizer { builder ->
            builder.apply {
                namingStrategy = JsonNamingStrategy.SnakeCase
                prettyPrint = true
                explicitNulls = false
                encodeDefaults = true
                ignoreUnknownKeys = false
            }
        }
    }
}

Annotate the DTOs with @Serializable and Spring encodes/decodes them on the HTTP boundary.