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:
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.