Back to skills
SKILL.md
Kotlin Spring
ASecurityKotlin Coroutines with Spring Boot/WebFlux performance optimization and best practices guidelines
- 2 stars
- 0 votes
- 0 copies
- 0 views
- Added September 27, 2026
Works with
Security analysis
100/100Pro scans all 9 files and shows the line behind each finding
npx -y skills add David-Li0406/meta-skill-evloving --skill kotlin-spring --agent claude-codeAre you the author of Kotlin Spring?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/david-li0406-kotlin-spring)---
name: best-practices
description: Kotlin Coroutines with Spring Boot/WebFlux performance optimization and best practices guidelines
license: MIT
metadata:
author: devnogari
project_type: kotlin-spring
rule_count: 48
categories: 8
---
# Kotlin Coroutines + Spring Boot Best Practices
Comprehensive performance optimization and best practices for Spring Boot applications with Kotlin Coroutines. **48 rules across 8 categories** based on Spring Framework 7.0, Spring Boot 4.0, Kotlin 2.x coroutines, and R2DBC async patterns.
## Activation Triggers
This skill activates when:
- Writing new controllers, services, or repositories with coroutines
- Implementing Flow patterns and reactive streams
- Optimizing async performance with WebFlux
- Reviewing or refactoring Kotlin coroutine code
- Debugging scope, cancellation, or flow issues
- Implementing authentication/authorization with reactive security
- Writing tests for coroutine-based applications
## Rule Categories
| # | Category | Priority | Prefix | Rule Count | Focus |
|---|----------|----------|--------|------------|-------|
| 1 | Coroutine Basics | CRITICAL | `coroutine-` | 6 | suspend functions, dispatchers |
| 2 | Scope Management | CRITICAL | `scope-` | 6 | GlobalScope avoidance, structured concurrency |
| 3 | Flow Patterns | HIGH | `flow-` | 6 | Flow, StateFlow, SharedFlow |
| 4 | R2DBC Database | HIGH | `db-` | 6 | Reactive repositories, transactions |
| 5 | Error Handling | HIGH | `err-` | 6 | SupervisorJob, exception handling |
| 6 | Spring WebFlux | MEDIUM | `webflux-` | 6 | Reactive endpoints, context propagation |
| 7 | Security | MEDIUM | `sec-` | 6 | Reactive security, JWT, context |
| 8 | Testing | MEDIUM | `test-` | 6 | runTest, MockK, Turbine |
## Quick Reference - All 48 Rules
### Coroutine Basics (CRITICAL) - `coroutine-*`
| Rule ID | Rule Name | Impact |
|---------|-----------|--------|
| `coroutine-suspend` | Use suspend functions correctly | Non-blocking execution |
| `coroutine-dispatcher` | Choose correct dispatcher | CPU/IO optimization |
| `coroutine-blocking` | Never block in coroutines | Prevents thread starvation |
| `coroutine-context` | Understand CoroutineContext | Proper context propagation |
| `coroutine-async` | Use async/await for parallel | Concurrent execution |
| `coroutine-withContext` | Use withContext for dispatcher switch | Clean context switching |
### Scope Management (CRITICAL) - `scope-*`
| Rule ID | Rule Name | Impact |
|---------|-----------|--------|
| `scope-no-global` | Never use GlobalScope | Prevents memory leaks |
| `scope-structured` | Use structured concurrency | Automatic cleanup |
| `scope-coroutineScope` | Use coroutineScope for grouping | Child coroutine management |
| `scope-supervisorScope` | Use supervisorScope for isolation | Fault tolerance |
| `scope-lifecycle` | Tie scope to lifecycle | Resource management |
| `scope-injection` | Inject CoroutineScope | Testability |
### Flow Patterns (HIGH) - `flow-*`
| Rule ID | Rule Name | Impact |
|---------|-----------|--------|
| `flow-cold` | Understand Flow is cold | Proper subscription |
| `flow-operators` | Use flow operators correctly | Efficient transformations |
| `flow-stateflow` | Use StateFlow for state | UI state management |
| `flow-sharedflow` | Use SharedFlow for events | Event broadcasting |
| `flow-flowOn` | Use flowOn for context | Single context switch |
| `flow-collect` | Collect safely in scope | Prevent leaks |
### R2DBC Database (HIGH) - `db-*`
| Rule ID | Rule Name | Impact |
|---------|-----------|--------|
| `db-r2dbc` | Use R2DBC not JDBC | Non-blocking DB |
| `db-repository` | Use CoroutineCrudRepository | Coroutine-native API |
| `db-transaction` | Handle transactions correctly | Data consistency |
| `db-connection-pool` | Configure connection pool | Resource efficiency |
| `db-n-plus-one` | Avoid N+1 queries | Query optimization |
| `db-migration` | Use Flyway/Liquibase async | Schema management |
### Error Handling (HIGH) - `err-*`
| Rule ID | Rule Name | Impact |
|---------|-----------|--------|
| `err-supervisor` | Use SupervisorJob correctly | Fault isolation |
| `err-handler` | Use CoroutineExceptionHandler | Centralized error handling |
| `err-try-catch` | Catch exceptions in coroutines | Proper error recovery |
| `err-cancellation` | Handle CancellationException | Correct cancellation |
| `err-flow-catch` | Use catch operator in Flow | Flow error handling |
| `err-retry` | Implement retry with backoff | Resilience |
### Spring WebFlux (MEDIUM) - `webflux-*`
| Rule ID | Rule Name | Impact |
|---------|-----------|--------|
| `webflux-controller` | Use suspend in controllers | Non-blocking endpoints |
| `webflux-coRouter` | Use coRouter DSL | Coroutine-friendly routing |
| `webflux-webclient` | Use WebClient with awaitBody | Async HTTP calls |
| `webflux-context` | Propagate ReactorContext | Observability, tracing |
| `webflux-response` | Return proper types | Efficient serialization |
| `webflux-filter` | Use CoWebFilter | Coroutine-aware filters |
### Security (MEDIUM) - `sec-*`
| Rule ID | Rule Name | Impact |
|---------|-----------|--------|
| `sec-reactive` | Use ReactiveSecurityContextHolder | Reactive auth context |
| `sec-method` | Use @PreAuthorize with suspend | Method-level security |
| `sec-context-propagation` | Propagate SecurityContext | Cross-thread auth |
| `sec-jwt` | Implement JWT properly | Stateless auth |
| `sec-cors` | Configure CORS correctly | Cross-origin security |
| `sec-csrf` | Handle CSRF in WebFlux | Request forgery protection |
### Testing (MEDIUM) - `test-*`
| Rule ID | Rule Name | Impact |
|---------|-----------|--------|
| `test-runTest` | Use runTest for coroutines | Proper test execution |
| `test-mockk` | Use MockK coEvery/coVerify | Suspend function mocking |
| `test-turbine` | Use Turbine for Flow testing | Flow assertions |
| `test-dispatcher` | Use TestDispatcher | Deterministic tests |
| `test-webclient` | Test with WebTestClient | Integration testing |
| `test-container` | Use Testcontainers | Database testing |
---
## Detailed Rules by Category
### 1. Coroutine Basics (CRITICAL)
#### `coroutine-suspend` - Use Suspend Functions Correctly
**Impact**: Non-blocking execution, proper coroutine integration
```kotlin
// ❌ INCORRECT - blocking call in suspend function
suspend fun getUser(id: Long): User {
Thread.sleep(1000) // BLOCKS the thread!
return repository.findById(id) // blocking JDBC
}
// ✅ CORRECT - proper suspend function
suspend fun getUser(id: Long): User {
delay(1000) // non-blocking delay
return repository.findByIdOrNull(id) // R2DBC suspend
?: throw UserNotFoundException(id)
}
// ✅ CORRECT - marking function as suspend when it calls other suspend functions
suspend fun getUserWithOrders(userId: Long): UserWithOrders {
val user = userRepository.findById(userId) // suspend call
val orders = orderRepository.findByUserId(userId) // suspend call
return UserWithOrders(user, orders)
}
```
#### `coroutine-dispatcher` - Choose Correct Dispatcher
**Impact**: Optimized thread utilization for CPU vs IO operations
```kotlin
// ❌ INCORRECT - CPU-intensive work on Default/IO dispatcher
suspend fun processImage(data: ByteArray): ByteArray {
return withContext(Dispatchers.IO) { // Wrong! IO is for blocking I/O
heavyImageProcessing(data) // CPU-bound work
}
}
// ✅ CORRECT - use appropriate dispatchers
suspend fun processImage(data: ByteArray): ByteArray {
return withContext(Dispatchers.Default) { // CPU-bound work
heavyImageProcessing(data)
}
}
suspend fun readFile(path: String): String {
return withContext(Dispatchers.IO) { // Blocking I/O
File(path).readText()
}
}
// ✅ CORRECT - no dispatcher needed for suspend-native operations
suspend fun fetchFromApi(url: String): Response {
// WebClient is already non-blocking, no withContext needed
return webClient.get()
.uri(url)
.awaitExchange { it.awaitBody() }
}
```
#### `coroutine-blocking` - Never Block in Coroutines
**Impact**: Prevents thread starvation, maintains concurrency
```kotlin
// ❌ INCORRECT - blocking operations in coroutine
@GetMapping("/data")
suspend fun getData(): Data {
val result = blockingHttpClient.get(url) // BLOCKS!
Thread.sleep(100) // BLOCKS!
return result
}
// ✅ CORRECT - use non-blocking alternatives
@GetMapping("/data")
suspend fun getData(): Data {
val result = webClient.get()
.uri(url)
.awaitBody<Data>() // non-blocking
delay(100) // non-blocking
return result
}
// ✅ CORRECT - if blocking is unavoidable, use withContext(Dispatchers.IO)
suspend fun legacyOperation(): Result {
return withContext(Dispatchers.IO) {
legacyBlockingClient.execute() // isolated on IO dispatcher
}
}
```
#### `coroutine-context` - Understand CoroutineContext
**Impact**: Proper context propagation for logging, tracing, auth
```kotlin
// ❌ INCORRECT - losing context elements
suspend fun processWithLogging() {
// MDC context lost when switching dispatchers
withContext(Dispatchers.Default) {
logger.info("Processing") // MDC context may be empty!
}
}
// ✅ CORRECT - propagate context elements
suspend fun processWithLogging() {
val mdcContext = MDCContext()
withContext(Dispatchers.Default + mdcContext) {
logger.info("Processing") // MDC context preserved
}
}
// ✅ CORRECT - Spring Boot 4.0 automatic context propagation
// application.properties: spring.reactor.context-propagation=auto
@GetMapping("/trace")
suspend fun tracedEndpoint(): Response {
// Tracing context automatically propagated
return service.process()
}
```
#### `coroutine-async` - Use async/await for Parallel Execution
**Impact**: Concurrent execution, reduced latency
```kotlin
// ❌ INCORRECT - sequential execution (3x latency)
suspend fun getDashboard(userId: Long): Dashboard {
val user = userService.getUser(userId) // 100ms
val orders = orderService.getOrders(userId) // 100ms
val notifications = notificationService.get() // 100ms
return Dashboard(user, orders, notifications) // Total: 300ms
}
// ✅ CORRECT - parallel execution with coroutineScope
suspend fun getDashboard(userId: Long): Dashboard = coroutineScope {
val user = async { userService.getUser(userId) }
val orders = async { orderService.getOrders(userId) }
val notifications = async { notificationService.get() }
Dashboard(
user = user.await(),
orders = orders.await(),
notifications = notifications.await()
) // Total: ~100ms (max of all)
}
// ✅ CORRECT - with structured error handling
suspend fun getDashboardSafe(userId: Long): Dashboard = supervisorScope {
val user = async { userService.getUser(userId) }
val orders = async {
runCatching { orderService.getOrders(userId) }
.getOrDefault(emptyList())
}
val notifications = async {
runCatching { notificationService.get() }
.getOrDefault(emptyList())
}
Dashboard(user.await(), orders.await(), notifications.await())
}
```
#### `coroutine-withContext` - Use withContext for Dispatcher Switch
**Impact**: Clean context switching without nested scopes
```kotlin
// ❌ INCORRECT - unnecessary scope creation
suspend fun processData(data: Data): Result {
return CoroutineScope(Dispatchers.Default).async {
heavyProcessing(data)
}.await() // Creates new scope, loses parent context!
}
// ✅ CORRECT - use withContext
suspend fun processData(data: Data): Result {
return withContext(Dispatchers.Default) {
heavyProcessing(data)
} // Maintains parent scope, proper cancellation
}
// ✅ CORRECT - combine context elements
suspend fun processWithTimeout(data: Data): Result {
return withContext(Dispatchers.Default + CoroutineName("processor")) {
withTimeout(5000) {
heavyProcessing(data)
}
}
}
```
---
### 2. Scope Management (CRITICAL)
#### `scope-no-global` - Never Use GlobalScope
**Impact**: Prevents memory leaks, enables proper cancellation
```kotlin
// ❌ INCORRECT - GlobalScope leaks and can't be cancelled
@Service
class NotificationService {
fun sendAsync(notification: Notification) {
GlobalScope.launch { // NEVER CANCELLED!
sendNotification(notification)
}
}
}
// ✅ CORRECT - inject scope tied to service lifecycle
@Service
class NotificationService(
private val scope: CoroutineScope // injected
) {
fun sendAsync(notification: Notification) {
scope.launch { // cancelled when service destroyed
sendNotification(notification)
}
}
}
// ✅ CORRECT - define scope as Spring bean
@Configuration
class CoroutineConfig {
@Bean
fun applicationScope(): CoroutineScope {
return CoroutineScope(
SupervisorJob() +
Dispatchers.Default +
CoroutineName("app-scope")
)
}
@Bean
fun cleanupScope(scope: CoroutineScope): DisposableBean {
return DisposableBean { scope.cancel() }
}
}
```
#### `scope-structured` - Use Structured Concurrency
**Impact**: Automatic cleanup, proper parent-child relationships
```kotlin
// ❌ INCORRECT - fire-and-forget without structure
suspend fun processOrders(orders: List<Order>) {
orders.forEach { order ->
CoroutineScope(Dispatchers.Default).launch {
processOrder(order) // orphaned coroutines!
}
}
// returns immediately, children still running
}
// ✅ CORRECT - structured concurrency waits for children
suspend fun processOrders(orders: List<Order>) = coroutineScope {
orders.forEach { order ->
launch {
processOrder(order)
}
}
// waits for all children to complete
}
// ✅ CORRECT - parallel with result collection
suspend fun processOrders(orders: List<Order>): List<Result> = coroutineScope {
orders.map { order ->
async {
processOrder(order)
}
}.awaitAll()
}
```
#### `scope-coroutineScope` - Use coroutineScope for Grouping
**Impact**: Child coroutine management, exception propagation
```kotlin
// ✅ CORRECT - group related operations
suspend fun updateUserProfile(userId: Long, profile: Profile): User = coroutineScope {
// Validate in parallel
val validationDeferred = async { validateProfile(profile) }
val existingUser = async { userRepository.findById(userId) }
validationDeferred.await() // throws if invalid
val user = existingUser.await() ?: throw UserNotFoundException(userId)
// Update with validated data
val updatedUser = user.copy(
name = profile.name,
email = profile.email
)
// Parallel side effects
launch { auditService.logUpdate(userId) }
launch { cacheService.invalidate(userId) }
userRepository.save(updatedUser)
}
```
#### `scope-supervisorScope` - Use supervisorScope for Isolation
**Impact**: Fault tolerance, independent child failures
```kotlin
// ❌ INCORRECT - one failure cancels all
suspend fun sendNotifications(users: List<User>) = coroutineScope {
users.forEach { user ->
launch {
sendNotification(user) // one failure cancels ALL
}
}
}
// ✅ CORRECT - isolated failures with supervisorScope
suspend fun sendNotifications(users: List<User>) = supervisorScope {
users.forEach { user ->
launch {
try {
sendNotification(user)
} catch (e: Exception) {
logger.error("Failed to notify ${user.id}", e)
// continues with other users
}
}
}
}
// ✅ CORRECT - collect results with partial failure handling
suspend fun fetchAllData(): AggregatedData = supervisorScope {
val users = async {
runCatching { userService.getAll() }.getOrDefault(emptyList())
}
val products = async {
runCatching { productService.getAll() }.getOrDefault(emptyList())
}
val orders = async {
runCatching { orderService.getAll() }.getOrDefault(emptyList())
}
AggregatedData(users.await(), products.await(), orders.await())
}
```
#### `scope-lifecycle` - Tie Scope to Lifecycle
**Impact**: Proper resource management, no orphaned coroutines
```kotlin
// ✅ CORRECT - scope tied to component lifecycle
@Component
class BackgroundProcessor : DisposableBean {
private val scope = CoroutineScope(
SupervisorJob() + Dispatchers.Default
)
fun startProcessing() {
scope.launch {
while (isActive) {
processNextItem()
delay(1000)
}
}
}
override fun destroy() {
scope.cancel() // cleanup on shutdown
}
}
// ✅ CORRECT - request-scoped with Spring
@RestController
class UserController {
@GetMapping("/users/{id}")
suspend fun getUser(@PathVariable id: Long): User {
// Spring creates request-scoped coroutine context
return userService.getUser(id)
}
}
```
#### `scope-injection` - Inject CoroutineScope for Testability
**Impact**: Easy testing, flexible dispatcher configuration
```kotlin
// ✅ CORRECT - inject scope for testability
@Service
class OrderProcessor(
private val orderRepository: OrderRepository,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) {
fun processAsync(orderId: Long): Job {
return scope.launch {
val order = orderRepository.findById(orderId)
processOrder(order)
}
}
}
// In tests
@Test
fun `test order processing`() = runTest {
val testScope = this // TestScope from runTest
val processor = OrderProcessor(mockRepository, testScope)
processor.processAsync(1L)
advanceUntilIdle()
coVerify { mockRepository.findById(1L) }
}
```
---
### 3. Flow Patterns (HIGH)
#### `flow-cold` - Understand Flow is Cold
**Impact**: Proper subscription behavior, no unexpected side effects
```kotlin
// ❌ INCORRECT - expecting Flow to run immediately
fun getUpdates(): Flow<Update> = flow {
println("Starting flow") // Not printed until collected!
emit(fetchUpdate())
}
// This doesn't start the flow:
val updates = getUpdates() // Nothing happens yet
// ✅ CORRECT - collect to start the flow
suspend fun processUpdates() {
getUpdates().collect { update ->
println("Received: $update")
}
}
// ✅ CORRECT - use SharedFlow for hot streams
@Service
class EventService {
private val _events = MutableSharedFlow<Event>(
replay = 1,
extraBufferCapacity = 64
)
val events: SharedFlow<Event> = _events.asSharedFlow()
suspend fun emit(event: Event) {
_events.emit(event)
}
}
```
#### `flow-operators` - Use Flow Operators Correctly
**Impact**: Efficient transformations, proper backpressure handling
```kotlin
// ✅ CORRECT - chain operators efficiently
fun getProcessedOrders(): Flow<ProcessedOrder> {
return orderRepository.findAllAsFlow()
.filter { it.status == OrderStatus.PENDING }
.map { order -> processOrder(order) }
.catch { e ->
logger.error("Processing failed", e)
emit(ProcessedOrder.failed())
}
.onCompletion { logger.info("Processing complete") }
}
// ✅ CORRECT - use buffer for slow collectors
fun getNotifications(): Flow<Notification> {
return notificationSource.asFlow()
.buffer(capacity = 64) // buffer if collector is slow
.map { enrichNotification(it) }
}
// ✅ CORRECT - use conflate for latest-only semantics
fun getPriceUpdates(): Flow<Price> {
return priceSource.asFlow()
.conflate() // keep only latest if collector is slow
}
// ✅ CORRECT - debounce for rate limiting
fun getSearchResults(queries: Flow<String>): Flow<List<Result>> {
return queries
.debounce(300) // wait 300ms of inactivity
.distinctUntilChanged()
.flatMapLatest { query ->
searchService.search(query)
}
}
```
#### `flow-stateflow` - Use StateFlow for State
**Impact**: Proper state management, UI updates
```kotlin
// ✅ CORRECT - StateFlow for observable state
@Service
class ConnectionService {
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
suspend fun connect() {
_connectionState.value = ConnectionState.CONNECTING
try {
establishConnection()
_connectionState.value = ConnectionState.CONNECTED
} catch (e: Exception) {
_connectionState.value = ConnectionState.ERROR
}
}
}
// ✅ CORRECT - expose immutable StateFlow
class ViewModel {
private val _uiState = MutableStateFlow(UiState.Initial)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// Never expose MutableStateFlow directly
}
```
#### `flow-sharedflow` - Use SharedFlow for Events
**Impact**: Event broadcasting, multiple subscribers
```kotlin
// ✅ CORRECT - SharedFlow for one-time events
@Service
class EventBus {
private val _events = MutableSharedFlow<AppEvent>(
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<AppEvent> = _events.asSharedFlow()
suspend fun publish(event: AppEvent) {
_events.emit(event)
}
// For fire-and-forget without suspend
fun tryPublish(event: AppEvent): Boolean {
return _events.tryEmit(event)
}
}
// ✅ CORRECT - replay for late subscribers
@Service
class ConfigService {
private val _config = MutableSharedFlow<Config>(
replay = 1 // late subscribers get last value
)
val config: SharedFlow<Config> = _config.asSharedFlow()
}
```
#### `flow-flowOn` - Use flowOn for Context Switching
**Impact**: Single context switch point, efficient execution
```kotlin
// ❌ INCORRECT - multiple unnecessary context switches
fun processItems(): Flow<Item> = flow {
items.forEach { item ->
val processed = withContext(Dispatchers.Default) { // switch per item!
process(item)
}
emit(processed)
}
}
// ✅ CORRECT - single flowOn for upstream
fun processItems(): Flow<Item> = flow {
items.forEach { item ->
emit(process(item))
}
}.flowOn(Dispatchers.Default) // all upstream on Default
// ✅ CORRECT - multiple flowOn for different stages
fun loadAndProcessData(): Flow<Result> {
return loadFromDb()
.flowOn(Dispatchers.IO) // DB operations on IO
.map { process(it) }
.flowOn(Dispatchers.Default) // processing on Default
}
```
#### `flow-collect` - Collect Safely in Scope
**Impact**: Prevent leaks, proper cancellation
```kotlin
// ❌ INCORRECT - collecting in GlobalScope
@Service
class EventListener {
fun startListening() {
GlobalScope.launch { // leaks!
events.collect { handleEvent(it) }
}
}
}
// ✅ CORRECT - collect in lifecycle-bound scope
@Service
class EventListener(
private val scope: CoroutineScope
) : DisposableBean {
private var job: Job? = null
fun startListening() {
job = scope.launch {
events.collect { handleEvent(it) }
}
}
override fun destroy() {
job?.cancel()
}
}
// ✅ CORRECT - use launchIn for cleaner syntax
@Service
class EventListener(
private val scope: CoroutineScope
) {
init {
events
.onEach { handleEvent(it) }
.launchIn(scope)
}
}
```
---
### 4. R2DBC Database (HIGH)
#### `db-r2dbc` - Use R2DBC Not JDBC
**Impact**: Non-blocking database operations
```kotlin
// ❌ INCORRECT - blocking JDBC
@Repository
class UserRepository(
private val jdbcTemplate: JdbcTemplate
) {
fun findById(id: Long): User? {
return jdbcTemplate.queryForObject(...) // BLOCKS!
}
}
// ✅ CORRECT - R2DBC with coroutines
@Repository
interface UserRepository : CoroutineCrudRepository<User, Long> {
suspend fun findByEmail(email: String): User?
@Query("SELECT * FROM users WHERE status = :status")
fun findByStatus(status: String): Flow<User>
}
// ✅ CORRECT - configuration
@Configuration
@EnableR2dbcRepositories
class DatabaseConfig : AbstractR2dbcConfiguration() {
@Bean
override fun connectionFactory(): ConnectionFactory {
return ConnectionFactories.get(
ConnectionFactoryOptions.builder()
.option(DRIVER, "postgresql")
.option(HOST, "localhost")
.option(DATABASE, "mydb")
.option(USER, "user")
.option(PASSWORD, "pass")
.build()
)
}
}
```
#### `db-repository` - Use CoroutineCrudRepository
**Impact**: Native coroutine API, clean suspend functions
```kotlin
// ✅ CORRECT - CoroutineCrudRepository for suspend functions
@Repository
interface OrderRepository : CoroutineCrudRepository<Order, Long> {
// Suspend function for single result
suspend fun findByOrderNumber(orderNumber: String): Order?
// Flow for multiple results
fun findByUserId(userId: Long): Flow<Order>
// Custom query with suspend
@Query("SELECT * FROM orders WHERE status = :status LIMIT :limit")
suspend fun findTopByStatus(status: String, limit: Int): List<Order>
}
// ✅ CORRECT - use in service
@Service
class OrderService(
private val orderRepository: OrderRepository
) {
suspend fun getOrder(id: Long): Order {
return orderRepository.findById(id)
?: throw OrderNotFoundException(id)
}
fun getUserOrders(userId: Long): Flow<Order> {
return orderRepository.findByUserId(userId)
}
}
```
#### `db-transaction` - Handle Transactions Correctly
**Impact**: Data consistency, ACID compliance
```kotlin
// ❌ INCORRECT - no transaction for multiple operations
suspend fun transferMoney(from: Long, to: Long, amount: BigDecimal) {
val fromAccount = accountRepository.findById(from)!!
fromAccount.balance -= amount
accountRepository.save(fromAccount) // committed
val toAccount = accountRepository.findById(to)!! // if this fails, money lost!
toAccount.balance += amount
accountRepository.save(toAccount)
}
// ✅ CORRECT - programmatic transaction with coroutines
@Service
class TransferService(
private val transactionalOperator: TransactionalOperator,
private val accountRepository: AccountRepository
) {
suspend fun transferMoney(from: Long, to: Long, amount: BigDecimal) {
transactionalOperator.executeAndAwait {
val fromAccount = accountRepository.findById(from)
?: throw AccountNotFoundException(from)
val toAccount = accountRepository.findById(to)
?: throw AccountNotFoundException(to)
if (fromAccount.balance < amount) {
throw InsufficientFundsException()
}
accountRepository.save(fromAccount.copy(balance = fromAccount.balance - amount))
accountRepository.save(toAccount.copy(balance = toAccount.balance + amount))
}
}
}
// ✅ CORRECT - @Transactional with suspend
@Service
class OrderService(
private val orderRepository: OrderRepository,
private val inventoryRepository: InventoryRepository
) {
@Transactional
suspend fun createOrder(order: Order): Order {
val savedOrder = orderRepository.save(order)
inventoryRepository.decrementStock(order.productId, order.quantity)
return savedOrder
}
}
```
#### `db-connection-pool` - Configure Connection Pool
**Impact**: Resource efficiency, connection reuse
```kotlin
// ✅ CORRECT - R2DBC pool configuration
@Configuration
class R2dbcConfig {
@Bean
fun connectionFactory(): ConnectionFactory {
val connectionFactory = ConnectionFactories.get(
ConnectionFactoryOptions.builder()
.option(DRIVER, "pool")
.option(PROTOCOL, "postgresql")
.option(HOST, "localhost")
.option(DATABASE, "mydb")
.option(USER, System.getenv("DB_USER"))
.option(PASSWORD, System.getenv("DB_PASSWORD"))
.build()
)
val poolConfig = ConnectionPoolConfiguration.builder(connectionFactory)
.maxIdleTime(Duration.ofMinutes(10))
.maxSize(20)
.initialSize(5)
.maxCreateConnectionTime(Duration.ofSeconds(5))
.validationQuery("SELECT 1")
.build()
return ConnectionPool(poolConfig)
}
}
// application.yml alternative
# spring:
# r2dbc:
# url: r2dbc:pool:postgresql://localhost/mydb
# pool:
# initial-size: 5
# max-size: 20
# max-idle-time: 10m
```
#### `db-n-plus-one` - Avoid N+1 Queries
**Impact**: Query optimization, reduced database load
```kotlin
// ❌ INCORRECT - N+1 queries
suspend fun getUsersWithOrders(): List<UserWithOrders> {
val users = userRepository.findAll().toList()
return users.map { user ->
val orders = orderRepository.findByUserId(user.id).toList() // N queries!
UserWithOrders(user, orders)
}
}
// ✅ CORRECT - batch fetch with join or single query
@Repository
interface UserRepository : CoroutineCrudRepository<User, Long> {
@Query("""
SELECT u.*, o.* FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'ACTIVE'
""")
fun findActiveUsersWithOrders(): Flow<UserWithOrders>
}
// ✅ CORRECT - manual batching
suspend fun getUsersWithOrders(): List<UserWithOrders> {
val users = userRepository.findAll().toList()
val userIds = users.map { it.id }
// Single batch query for all orders
val ordersByUser = orderRepository.findByUserIdIn(userIds)
.toList()
.groupBy { it.userId }
return users.map { user ->
UserWithOrders(user, ordersByUser[user.id] ?: emptyList())
}
}
```
#### `db-migration` - Use Flyway/Liquibase
**Impact**: Schema version control, safe migrations
```kotlin
// ✅ CORRECT - Flyway configuration for R2DBC
// build.gradle.kts
dependencies {
implementation("org.flywaydb:flyway-core")
runtimeOnly("org.postgresql:postgresql") // for migrations
}
// application.yml
# spring:
# flyway:
# enabled: true
# locations: classpath:db/migration
# url: jdbc:postgresql://localhost/mydb # JDBC for Flyway
# user: ${DB_USER}
# password: ${DB_PASSWORD}
# r2dbc:
# url: r2dbc:postgresql://localhost/mydb # R2DBC for app
// ✅ CORRECT - migration script V1__create_users.sql
/*
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
*/
```
---
### 5. Error Handling (HIGH)
#### `err-supervisor` - Use SupervisorJob Correctly
**Impact**: Fault isolation, independent failure handling
```kotlin
// ❌ INCORRECT - SupervisorJob without exception handler
val scope = CoroutineScope(SupervisorJob())
scope.launch {
throw Exception("Unhandled!") // crashes the app on Android, logged on server
}
// ✅ CORRECT - SupervisorJob with CoroutineExceptionHandler
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
logger.error("Coroutine failed", exception)
errorReporter.report(exception)
}
val scope = CoroutineScope(
SupervisorJob() +
Dispatchers.Default +
exceptionHandler
)
// ✅ CORRECT - handle exceptions in each child
scope.launch {
try {
riskyOperation()
} catch (e: Exception) {
logger.error("Operation failed", e)
// handle gracefully
}
}
```
#### `err-handler` - Use CoroutineExceptionHandler
**Impact**: Centralized error handling for uncaught exceptions
```kotlin
// ✅ CORRECT - global exception handler
@Configuration
class CoroutineExceptionConfig {
@Bean
fun coroutineExceptionHandler(): CoroutineExceptionHandler {
return CoroutineExceptionHandler { context, exception ->
val name = context[CoroutineName]?.name ?: "unknown"
logger.error("Coroutine '$name' failed", exception)
when (exception) {
is CancellationException -> {
// Normal cancellation, don't report
}
is NetworkException -> {
// Retry logic or user notification
retryQueue.add(context)
}
else -> {
errorReporter.report(exception)
}
}
}
}
@Bean
fun applicationScope(handler: CoroutineExceptionHandler): CoroutineScope {
return CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)
}
}
```
#### `err-try-catch` - Catch Exceptions in Coroutines
**Impact**: Proper error recovery, graceful degradation
```kotlin
// ❌ INCORRECT - unhandled exception in async
suspend fun fetchData(): Data = coroutineScope {
val result = async {
riskyNetworkCall() // exception not caught by try-catch below
}
try {
result.await()
} catch (e: Exception) {
// This catches, but coroutineScope already cancelled!
defaultData()
}
}
// ✅ CORRECT - catch at the source
suspend fun fetchData(): Data = coroutineScope {
val result = async {
try {
riskyNetworkCall()
} catch (e: Exception) {
logger.warn("Network call failed, using default", e)
defaultData()
}
}
result.await()
}
// ✅ CORRECT - use supervisorScope for independent failures
suspend fun fetchData(): Data = supervisorScope {
val result = async {
riskyNetworkCall()
}
try {
result.await()
} catch (e: Exception) {
defaultData()
}
}
// ✅ CORRECT - use runCatching
suspend fun fetchDataSafe(): Result<Data> {
return runCatching {
riskyNetworkCall()
}
}
```
#### `err-cancellation` - Handle CancellationException Properly
**Impact**: Correct cancellation propagation, resource cleanup
```kotlin
// ❌ INCORRECT - swallowing CancellationException
suspend fun processItems(items: List<Item>) {
items.forEach { item ->
try {
processItem(item)
} catch (e: Exception) { // catches CancellationException!
logger.error("Failed", e)
// Cancellation is swallowed, coroutine continues!
}
}
}
// ✅ CORRECT - rethrow CancellationException
suspend fun processItems(items: List<Item>) {
items.forEach { item ->
try {
processItem(item)
} catch (e: CancellationException) {
throw e // always rethrow!
} catch (e: Exception) {
logger.error("Failed to process ${item.id}", e)
}
}
}
// ✅ CORRECT - use runCatching which handles this
suspend fun processItems(items: List<Item>) {
items.forEach { item ->
runCatching {
processItem(item)
}.onFailure { e ->
if (e is CancellationException) throw e
logger.error("Failed to process ${item.id}", e)
}
}
}
// ✅ CORRECT - cleanup with try-finally
suspend fun processWithResource() {
val resource = acquireResource()
try {
process(resource)
} finally {
withContext(NonCancellable) {
resource.close() // cleanup even on cancellation
}
}
}
```
#### `err-flow-catch` - Use catch Operator in Flow
**Impact**: Flow error handling without breaking collection
```kotlin
// ❌ INCORRECT - exception breaks entire flow
fun getUpdates(): Flow<Update> = flow {
while (true) {
val update = fetchUpdate() // throws on network error
emit(update)
delay(1000)
}
}
// ✅ CORRECT - catch and recover
fun getUpdates(): Flow<Update> = flow {
while (true) {
val update = fetchUpdate()
emit(update)
delay(1000)
}
}.catch { e ->
logger.error("Update fetch failed", e)
emit(Update.error(e.message)) // emit error state
}
// ✅ CORRECT - retry on error
fun getUpdates(): Flow<Update> = flow {
val update = fetchUpdate()
emit(update)
}.retry(3) { e ->
e is NetworkException // retry only network errors
}.catch { e ->
emit(Update.error("Failed after retries"))
}
// ✅ CORRECT - onEach for side effects, catch for errors
fun processNotifications(): Flow<Notification> {
return notificationSource
.onEach { notification ->
analyticsService.track(notification)
}
.catch { e ->
logger.error("Notification processing failed", e)
// don't emit, just log
}
}
```
#### `err-retry` - Implement Retry with Backoff
**Impact**: Resilience for transient failures
```kotlin
// ✅ CORRECT - retry with exponential backoff
suspend fun <T> retryWithBackoff(
maxRetries: Int = 3,
initialDelay: Long = 100,
maxDelay: Long = 1000,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(maxRetries - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
if (e is CancellationException) throw e
logger.warn("Attempt ${attempt + 1} failed, retrying in ${currentDelay}ms", e)
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
return block() // last attempt, let it throw
}
// Usage
suspend fun fetchDataWithRetry(): Data {
return retryWithBackoff(maxRetries = 3) {
apiClient.fetchData()
}
}
// ✅ CORRECT - retry in Flow
fun getDataStream(): Flow<Data> = flow {
while (currentCoroutineContext().isActive) {
emit(fetchData())
delay(5000)
}
}.retryWhen { cause, attempt ->
if (cause is NetworkException && attempt < 3) {
delay(1000L * (attempt + 1))
true // retry
} else {
false // give up
}
}
```
---
### 6. Spring WebFlux (MEDIUM)
#### `webflux-controller` - Use Suspend in Controllers
**Impact**: Non-blocking endpoints, efficient resource use
```kotlin
// ✅ CORRECT - suspend function in controller
@RestController
@RequestMapping("/api/users")
class UserController(
private val userService: UserService
) {
@GetMapping("/{id}")
suspend fun getUser(@PathVariable id: Long): User {
return userService.findById(id)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@GetMapping
fun getAllUsers(): Flow<User> { // Flow for streaming
return userService.findAll()
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
suspend fun createUser(@RequestBody user: User): User {
return userService.save(user)
}
}
```
#### `webflux-coRouter` - Use coRouter DSL
**Impact**: Coroutine-friendly functional routing
```kotlin
// ✅ CORRECT - coRouter for functional endpoints
@Configuration
class RouterConfig(
private val userHandler: UserHandler
) {
@Bean
fun userRoutes() = coRouter {
"/api/users".nest {
GET("", userHandler::getAllUsers)
GET("/{id}", userHandler::getUser)
POST("", userHandler::createUser)
PUT("/{id}", userHandler::updateUser)
DELETE("/{id}", userHandler::deleteUser)
}
filter { request, next ->
// logging filter
val start = System.currentTimeMillis()
val response = next(request)
val duration = System.currentTimeMillis() - start
logger.info("${request.method()} ${request.path()} - ${duration}ms")
response
}
}
}
@Component
class UserHandler(
private val userService: UserService
) {
suspend fun getUser(request: ServerRequest): ServerResponse {
val id = request.pathVariable("id").toLong()
val user = userService.findById(id)
return if (user != null) {
ServerResponse.ok().bodyValueAndAwait(user)
} else {
ServerResponse.notFound().buildAndAwait()
}
}
suspend fun getAllUsers(request: ServerRequest): ServerResponse {
val users = userService.findAll()
return ServerResponse.ok().bodyAndAwait(users)
}
}
```
#### `webflux-webclient` - Use WebClient with awaitBody
**Impact**: Non-blocking HTTP client calls
```kotlin
// ✅ CORRECT - WebClient with coroutine extensions
@Service
class ExternalApiService(
private val webClient: WebClient
) {
suspend fun fetchUser(userId: String): ExternalUser {
return webClient.get()
.uri("/users/{id}", userId)
.retrieve()
.awaitBody()
}
suspend fun fetchUserOrNull(userId: String): ExternalUser? {
return webClient.get()
.uri("/users/{id}", userId)
.retrieve()
.awaitBodyOrNull()
}
suspend fun createUser(user: ExternalUser): ExternalUser {
return webClient.post()
.uri("/users")
.bodyValue(user)
.retrieve()
.awaitBody()
}
// Stream response
fun fetchUsers(): Flow<ExternalUser> {
return webClient.get()
.uri("/users")
.retrieve()
.bodyToFlow()
}
}
// ✅ CORRECT - WebClient configuration
@Configuration
class WebClientConfig {
@Bean
fun webClient(): WebClient {
return WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.filter(ExchangeFilterFunction.ofRequestProcessor { request ->
logger.debug("Request: ${request.method()} ${request.url()}")
Mono.just(request)
})
.build()
}
}
```
#### `webflux-context` - Propagate ReactorContext
**Impact**: Observability, tracing, MDC propagation
```kotlin
// ✅ CORRECT - context propagation in Spring Boot 4
// application.properties: spring.reactor.context-propagation=auto
// For earlier versions or custom context
@Configuration
class ContextPropagationConfig {
@Bean
fun contextPropagationHook(): Hooks.OnEachOperator {
return { publisher ->
if (publisher is Mono<*>) {
publisher.contextWrite { ctx ->
// Copy MDC to reactor context
val mdc = MDC.getCopyOfContextMap() ?: emptyMap()
ctx.putAll(mdc.mapKeys { (k, _) -> k })
}
} else {
publisher
}
}
}
}
// ✅ CORRECT - propagate context in coroutines
suspend fun <T> withMdcContext(block: suspend () -> T): T {
return withContext(MDCContext()) {
block()
}
}
@GetMapping("/traced")
suspend fun tracedEndpoint(): Response {
return withMdcContext {
MDC.put("requestId", UUID.randomUUID().toString())
logger.info("Processing request") // MDC available
service.process()
}
}
```
#### `webflux-response` - Return Proper Types
**Impact**: Efficient serialization, proper HTTP semantics
```kotlin
// ✅ CORRECT - return appropriate types
@RestController
class ApiController(
private val itemService: ItemService
) {
// Single item - suspend function
@GetMapping("/items/{id}")
suspend fun getItem(@PathVariable id: Long): ResponseEntity<Item> {
val item = itemService.findById(id)
return if (item != null) {
ResponseEntity.ok(item)
} else {
ResponseEntity.notFound().build()
}
}
// Multiple items - Flow for streaming
@GetMapping("/items", produces = [MediaType.APPLICATION_NDJSON_VALUE])
fun getItems(): Flow<Item> {
return itemService.findAll()
}
// Server-Sent Events
@GetMapping("/items/stream", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun streamItems(): Flow<ServerSentEvent<Item>> {
return itemService.findAll().map { item ->
ServerSentEvent.builder(item)
.id(item.id.toString())
.event("item")
.build()
}
}
// No content response
@DeleteMapping("/items/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
suspend fun deleteItem(@PathVariable id: Long) {
itemService.delete(id)
}
}
```
#### `webflux-filter` - Use CoWebFilter
**Impact**: Coroutine-aware request filtering
```kotlin
// ✅ CORRECT - CoWebFilter for coroutine-based filtering
@Component
class LoggingFilter : CoWebFilter() {
override suspend fun filter(exchange: ServerWebExchange, chain: CoWebFilterChain) {
val request = exchange.request
val start = System.currentTimeMillis()
val requestId = UUID.randomUUID().toString()
// Add request ID to context
exchange.attributes["requestId"] = requestId
try {
chain.filter(exchange)
} finally {
val duration = System.currentTimeMillis() - start
logger.info(
"[$requestId] ${request.method} ${request.path} - ${duration}ms"
)
}
}
}
// ✅ CORRECT - authentication filter
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
class AuthenticationFilter(
private val tokenService: TokenService
) : CoWebFilter() {
override suspend fun filter(exchange: ServerWebExchange, chain: CoWebFilterChain) {
val token = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)
?.removePrefix("Bearer ")
if (token != null) {
val user = tokenService.validateAndGetUser(token)
if (user != null) {
exchange.attributes["currentUser"] = user
}
}
chain.filter(exchange)
}
}
```
---
### 7. Security (MEDIUM)
#### `sec-reactive` - Use ReactiveSecurityContextHolder
**Impact**: Reactive-compatible security context access
```kotlin
// ❌ INCORRECT - blocking SecurityContextHolder
suspend fun getCurrentUser(): User {
val auth = SecurityContextHolder.getContext().authentication // May be empty!
return auth.principal as User
}
// ✅ CORRECT - ReactiveSecurityContextHolder with coroutines
suspend fun getCurrentUser(): User {
return ReactiveSecurityContextHolder.getContext()
.awaitSingleOrNull()
?.authentication
?.principal as? User
?: throw UnauthorizedException()
}
// ✅ CORRECT - as extension function
suspend fun <T : Principal> ReactiveSecurityContextHolder.currentUserOrNull(): T? {
return getContext()
.awaitSingleOrNull()
?.authentication
?.principal as? T
}
// Usage
@GetMapping("/me")
suspend fun getProfile(): UserProfile {
val user = ReactiveSecurityContextHolder.currentUserOrNull<User>()
?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
return userService.getProfile(user.id)
}
```
#### `sec-method` - Use @PreAuthorize with Suspend
**Impact**: Method-level security on suspend functions
```kotlin
// ✅ CORRECT - enable reactive method security
@Configuration
@EnableReactiveMethodSecurity
class SecurityConfig
// ✅ CORRECT - @PreAuthorize on suspend functions
@Service
class AdminService {
@PreAuthorize("hasRole('ADMIN')")
suspend fun deleteUser(userId: Long) {
userRepository.deleteById(userId)
}
@PreAuthorize("hasRole('ADMIN') or #userId == principal.id")
suspend fun updateUser(userId: Long, update: UserUpdate): User {
return userRepository.update(userId, update)
}
@PreAuthorize("@permissionService.canAccess(principal, #resourceId)")
suspend fun accessResource(resourceId: Long): Resource {
return resourceRepository.findById(resourceId)
}
}
```
#### `sec-context-propagation` - Propagate SecurityContext
**Impact**: Security context available across async boundaries
```kotlin
// ✅ CORRECT - propagate security context to coroutines
@Configuration
class SecurityContextConfig {
@Bean
fun securityContextCoroutineContext(): CoroutineContext {
return ReactorContext(
Context.of(SecurityContext::class.java,
ReactiveSecurityContextHolder.getContext())
)
}
}
// ✅ CORRECT - custom context element for security
class SecurityContextElement(
val securityContext: SecurityContext
) : AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<SecurityContextElement>
}
suspend fun withSecurityContext(block: suspend () -> Unit) {
val securityContext = ReactiveSecurityContextHolder.getContext()
.awaitSingleOrNull()
if (securityContext != null) {
withContext(SecurityContextElement(securityContext)) {
block()
}
} else {
block()
}
}
```
#### `sec-jwt` - Implement JWT Properly
**Impact**: Stateless authentication, token validation
```kotlin
// ✅ CORRECT - JWT configuration for WebFlux
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http
.csrf { it.disable() }
.httpBasic { it.disable() }
.formLogin { it.disable() }
.authorizeExchange { auth ->
auth.pathMatchers("/api/auth/**").permitAll()
auth.pathMatchers("/api/admin/**").hasRole("ADMIN")
auth.anyExchange().authenticated()
}
.oauth2ResourceServer { oauth2 ->
oauth2.jwt { jwt ->
jwt.jwtAuthenticationConverter(jwtAuthConverter())
}
}
.build()
}
@Bean
fun jwtAuthConverter(): ReactiveJwtAuthenticationConverter {
val converter = ReactiveJwtAuthenticationConverter()
converter.setJwtGrantedAuthoritiesConverter { jwt ->
val roles = jwt.getClaimAsStringList("roles") ?: emptyList()
Flux.fromIterable(roles.map { SimpleGrantedAuthority("ROLE_$it") })
}
return converter
}
}
// ✅ CORRECT - JWT token service
@Service
class JwtTokenService(
@Value("\${jwt.secret}") private val secret: String,
@Value("\${jwt.expiration}") private val expiration: Long
) {
private val key = Keys.hmacShaKeyFor(secret.toByteArray())
fun generateToken(user: User): String {
val now = Date()
val expiryDate = Date(now.time + expiration)
return Jwts.builder()
.setSubject(user.id.toString())
.claim("email", user.email)
.claim("roles", user.roles)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(key, SignatureAlgorithm.HS256)
.compact()
}
suspend fun validateToken(token: String): User? {
return try {
val claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.body
val userId = claims.subject.toLong()
userRepository.findById(userId)
} catch (e: Exception) {
null
}
}
}
```
#### `sec-cors` - Configure CORS Correctly
**Impact**: Cross-origin security for SPAs
```kotlin
// ✅ CORRECT - CORS configuration for WebFlux
@Configuration
class CorsConfig {
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration().apply {
allowedOrigins = listOf(
"https://myapp.com",
"https://staging.myapp.com"
)
allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
allowedHeaders = listOf(
HttpHeaders.AUTHORIZATION,
HttpHeaders.CONTENT_TYPE,
"X-Request-ID"
)
exposedHeaders = listOf("X-Total-Count", "X-Page-Count")
allowCredentials = true
maxAge = 3600
}
return UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/api/**", configuration)
}
}
}
// In SecurityConfig
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http
.cors { it.configurationSource(corsConfigurationSource()) }
// ... rest of config
.build()
}
```
#### `sec-csrf` - Handle CSRF in WebFlux
**Impact**: Request forgery protection
```kotlin
// ✅ CORRECT - CSRF for stateless APIs (typically disabled)
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http
// For stateless JWT APIs, CSRF is typically disabled
.csrf { it.disable() }
// ... rest of config
.build()
}
}
// ✅ CORRECT - CSRF for stateful apps with cookies
@Configuration
@EnableWebFluxSecurity
class StatefulSecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http
.csrf { csrf ->
csrf.csrfTokenRepository(
CookieServerCsrfTokenRepository.withHttpOnlyFalse()
)
}
// ... rest of config
.build()
}
}
```
---
### 8. Testing (MEDIUM)
#### `test-runTest` - Use runTest for Coroutines
**Impact**: Proper test execution, virtual time control
```kotlin
// ✅ CORRECT - use runTest for coroutine tests
class UserServiceTest {
private val userRepository = mockk<UserRepository>()
private val userService = UserService(userRepository)
@Test
fun `should return user by id`() = runTest {
// Given
val user = User(1L, "test@example.com")
coEvery { userRepository.findById(1L) } returns user
// When
val result = userService.getUser(1L)
// Then
assertEquals(user, result)
coVerify { userRepository.findById(1L) }
}
@Test
fun `should handle delay correctly`() = runTest {
// Given
coEvery { userRepository.findById(any()) } coAnswers {
delay(1000) // Virtual time - completes instantly
User(1L, "test@example.com")
}
// When
val result = userService.getUser(1L)
// Then
assertNotNull(result)
// Test completes instantly despite 1000ms delay
}
}
```
#### `test-mockk` - Use MockK coEvery/coVerify
**Impact**: Suspend function mocking, clean test syntax
```kotlin
// ✅ CORRECT - MockK for coroutine mocking
class OrderServiceTest {
private val orderRepository = mockk<OrderRepository>()
private val paymentService = mockk<PaymentService>()
private val orderService = OrderService(orderRepository, paymentService)
@Test
fun `should create order with payment`() = runTest {
// Given
val order = Order(productId = 1L, quantity = 2)
val savedOrder = order.copy(id = 1L)
coEvery { orderRepository.save(any()) } returns savedOrder
coEvery { paymentService.processPayment(any()) } returns PaymentResult.SUCCESS
// When
val result = orderService.createOrder(order)
// Then
assertEquals(1L, result.id)
coVerify(exactly = 1) { orderRepository.save(order) }
coVerify(exactly = 1) { paymentService.processPayment(savedOrder) }
}
@Test
fun `should handle payment failure`() = runTest {
// Given
coEvery { orderRepository.save(any()) } returns Order(id = 1L)
coEvery { paymentService.processPayment(any()) } throws PaymentException("Failed")
// When/Then
assertThrows<PaymentException> {
orderService.createOrder(Order())
}
}
}
```
#### `test-turbine` - Use Turbine for Flow Testing
**Impact**: Clean Flow assertions, timeout handling
```kotlin
// build.gradle.kts
// testImplementation("app.cash.turbine:turbine:1.0.0")
// ✅ CORRECT - Turbine for Flow testing
class EventServiceTest {
private val eventService = EventService()
@Test
fun `should emit events in order`() = runTest {
eventService.events.test {
// Trigger events
eventService.emit(Event("first"))
eventService.emit(Event("second"))
// Assert emissions
assertEquals(Event("first"), awaitItem())
assertEquals(Event("second"), awaitItem())
// Cancel and ensure no more events
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `should complete flow`() = runTest {
val flow = flowOf(1, 2, 3)
flow.test {
assertEquals(1, awaitItem())
assertEquals(2, awaitItem())
assertEquals(3, awaitItem())
awaitComplete()
}
}
@Test
fun `should handle flow errors`() = runTest {
val errorFlow = flow {
emit(1)
throw RuntimeException("Error")
}
errorFlow.test {
assertEquals(1, awaitItem())
val error = awaitError()
assertEquals("Error", error.message)
}
}
}
```
#### `test-dispatcher` - Use TestDispatcher
**Impact**: Deterministic tests, controlled execution
```kotlin
// ✅ CORRECT - TestDispatcher for controlled execution
class TimerServiceTest {
@Test
fun `should emit at intervals`() = runTest {
val testDispatcher = StandardTestDispatcher(testScheduler)
val timerService = TimerService(testDispatcher)
val emissions = mutableListOf<Long>()
val job = launch {
timerService.ticker(1000).take(3).collect { emissions.add(it) }
}
// Advance time manually
advanceTimeBy(3000)
runCurrent()
assertEquals(3, emissions.size)
job.cancel()
}
@Test
fun `should use UnconfinedTestDispatcher for immediate execution`() = runTest {
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
var executed = false
launch(testDispatcher) {
executed = true
}
assertTrue(executed) // Executed immediately
}
}
// ✅ CORRECT - inject dispatcher for testability
class DataRefresher(
private val dispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun refresh() = withContext(dispatcher) {
// ...
}
}
// In test
@Test
fun `should refresh data`() = runTest {
val refresher = DataRefresher(StandardTestDispatcher(testScheduler))
refresher.refresh()
advanceUntilIdle()
// assertions
}
```
#### `test-webclient` - Test with WebTestClient
**Impact**: Integration testing for WebFlux endpoints
```kotlin
// ✅ CORRECT - WebTestClient for integration tests
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
@Autowired
lateinit var webTestClient: WebTestClient
@Test
fun `should get user by id`() {
webTestClient.get()
.uri("/api/users/1")
.exchange()
.expectStatus().isOk
.expectBody<User>()
.consumeWith { result ->
val user = result.responseBody!!
assertEquals(1L, user.id)
}
}
@Test
fun `should create user`() {
val newUser = UserCreate("test@example.com", "Test User")
webTestClient.post()
.uri("/api/users")
.bodyValue(newUser)
.exchange()
.expectStatus().isCreated
.expectBody<User>()
.consumeWith { result ->
val user = result.responseBody!!
assertNotNull(user.id)
assertEquals("test@example.com", user.email)
}
}
@Test
fun `should return 404 for missing user`() {
webTestClient.get()
.uri("/api/users/999")
.exchange()
.expectStatus().isNotFound
}
// ✅ CORRECT - test Flow endpoints
@Test
fun `should stream users`() {
webTestClient.get()
.uri("/api/users/stream")
.accept(MediaType.APPLICATION_NDJSON)
.exchange()
.expectStatus().isOk
.returnResult<User>()
.responseBody
.test()
.expectNextCount(10)
.verifyComplete()
}
}
```
#### `test-container` - Use Testcontainers
**Impact**: Real database testing, production-like tests
```kotlin
// build.gradle.kts
// testImplementation("org.testcontainers:postgresql:1.19.0")
// testImplementation("org.testcontainers:r2dbc:1.19.0")
// ✅ CORRECT - Testcontainers with R2DBC
@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {
companion object {
@Container
val postgres = PostgreSQLContainer("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
@JvmStatic
@DynamicPropertySource
fun properties(registry: DynamicPropertyRegistry) {
registry.add("spring.r2dbc.url") {
"r2dbc:postgresql://${postgres.host}:${postgres.getMappedPort(5432)}/${postgres.databaseName}"
}
registry.add("spring.r2dbc.username") { postgres.username }
registry.add("spring.r2dbc.password") { postgres.password }
registry.add("spring.flyway.url") { postgres.jdbcUrl }
registry.add("spring.flyway.user") { postgres.username }
registry.add("spring.flyway.password") { postgres.password }
}
}
@Autowired
lateinit var userRepository: UserRepository
@Test
fun `should save and retrieve user`() = runTest {
// Given
val user = User(email = "test@example.com", name = "Test User")
// When
val saved = userRepository.save(user)
val retrieved = userRepository.findById(saved.id!!)
// Then
assertNotNull(retrieved)
assertEquals("test@example.com", retrieved?.email)
}
@Test
fun `should find users by email`() = runTest {
// Given
userRepository.save(User(email = "find@example.com", name = "Find Me"))
// When
val found = userRepository.findByEmail("find@example.com")
// Then
assertNotNull(found)
assertEquals("Find Me", found?.name)
}
}
```
---
## Project Structure
```
src/
├── main/kotlin/com/example/
│ ├── Application.kt # Main application
│ ├── config/
│ │ ├── CoroutineConfig.kt # Coroutine scope beans
│ │ ├── R2dbcConfig.kt # Database configuration
│ │ ├── SecurityConfig.kt # Security configuration
│ │ └── WebClientConfig.kt # HTTP client configuration
│ ├── controller/
│ │ └── UserController.kt # REST controllers
│ ├── domain/
│ │ └── User.kt # Domain entities
│ ├── repository/
│ │ └── UserRepository.kt # R2DBC repositories
│ ├── service/
│ │ └── UserService.kt # Business logic
│ └── exception/
│ └── Exceptions.kt # Custom exceptions
├── main/resources/
│ ├── application.yml # Configuration
│ └── db/migration/ # Flyway migrations
└── test/kotlin/com/example/
├── controller/
│ └── UserControllerTest.kt
├── service/
│ └── UserServiceTest.kt
└── repository/
└── UserRepositoryTest.kt
```
## Integration Workflow
When implementing Kotlin Coroutine + Spring features:
1. **Before implementation**: Review relevant rule categories
2. **During development**: Apply patterns from quick reference
3. **Code review**: Verify against rule checklist
4. **Performance issues**: Consult detailed rules for solutions
```
coroutine-* rules → Proper suspend functions, no blocking
↓
scope-* rules → Structured concurrency, no GlobalScope
↓
flow-* rules → Correct Flow patterns and operators
↓
db-* rules → R2DBC, transactions, N+1 prevention
↓
err-* rules → SupervisorJob, proper exception handling
↓
webflux-* rules → Non-blocking endpoints, context propagation
↓
sec-* rules → Reactive security, JWT, context
↓
test-* rules → runTest, MockK, Turbine
```
## Compliance Checklist
Before submitting Kotlin Coroutine + Spring code:
- [ ] No blocking calls in suspend functions (`coroutine-blocking`)
- [ ] Correct dispatcher for CPU vs IO (`coroutine-dispatcher`)
- [ ] No GlobalScope usage (`scope-no-global`)
- [ ] Structured concurrency with coroutineScope/supervisorScope (`scope-structured`)
- [ ] StateFlow/SharedFlow used appropriately (`flow-stateflow`, `flow-sharedflow`)
- [ ] R2DBC with CoroutineCrudRepository (`db-r2dbc`, `db-repository`)
- [ ] Transactions handled correctly (`db-transaction`)
- [ ] CancellationException rethrown (`err-cancellation`)
- [ ] SupervisorJob with exception handler (`err-supervisor`)
- [ ] Security context propagated (`sec-context-propagation`)
- [ ] Tests use runTest with MockK (`test-runTest`, `test-mockk`)
Files in this skill
- SKILL.md
- rules/coroutine-basics.md
- rules/database-patterns.md
- rules/error-handling.md
- rules/flow-patterns.md
- rules/scope-management.md
- rules/security-patterns.md
- rules/testing.md
- rules/webflux-patterns.md
Attribution
Comments
Loading comments…