Claude Code for Kotlin and Spring Boot: Coroutines, WebFlux, and Idiomatic Kotlin — Claude Skills 360 Blog
Blog / Backend / Claude Code for Kotlin and Spring Boot: Coroutines, WebFlux, and Idiomatic Kotlin
Backend

Claude Code for Kotlin and Spring Boot: Coroutines, WebFlux, and Idiomatic Kotlin

Published: October 18, 2026
Read time: 9 min read
By: Claude Skills 360

Kotlin improves on Java for Spring Boot development: data classes replace POJOs, sealed classes model error states explicitly, extension functions add readability, and coroutines provide structured concurrency that’s cleaner than reactive chains. Claude Code writes idiomatic Kotlin — not Java transliterated to Kotlin — with proper use of null safety, coroutines, and the standard library.

CLAUDE.md for Kotlin/Spring Projects

## Stack
- Kotlin 2.x, Spring Boot 3.x
- Reactive: Spring WebFlux with Kotlin coroutines (suspend fun in controllers)
- Database: Spring Data R2DBC (reactive) or Spring Data JPA (blocking)
- Error handling: sealed classes + Result type, not exceptions for business logic
- Null safety: no !! operator in production code — use safe calls and elvis
- Data classes: prefer immutable (val), use copy() for updates
- Coroutines: structured concurrency, always propagate CoroutineScope, never GlobalScope
- Testing: kotest for assertions, MockK for mocking, @SpringBootTest for integration

Application Setup with Coroutines

// Main application
@SpringBootApplication
class OrderApiApplication

fun main(args: Array<String>) {
    runApplication<OrderApiApplication>(*args)
}

// application.yml
// spring:
//   r2dbc:
//     url: r2dbc:postgresql://localhost:5432/orders
//   webflux:
//     base-path: /api

@Configuration
class AppConfig {
    @Bean
    fun coroutineExceptionHandler() = CoroutineExceptionHandler { _, exception ->
        LoggerFactory.getLogger("CoroutineException").error("Uncaught exception", exception)
    }
}

Data Classes and Sealed Classes

// Domain models: immutable data classes
data class Order(
    val id: String = UUID.randomUUID().toString(),
    val customerId: String,
    val status: OrderStatus = OrderStatus.PENDING,
    val totalCents: Long,
    val items: List<OrderItem> = emptyList(),
    val createdAt: Instant = Instant.now(),
)

data class OrderItem(
    val productId: String,
    val quantity: Int,
    val unitPriceCents: Long,
)

enum class OrderStatus { PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED }

// Sealed classes for error modeling — exhaustive when expressions
sealed class OrderError {
    data class NotFound(val orderId: String) : OrderError()
    data class InvalidStatus(val current: OrderStatus, val attempted: OrderStatus) : OrderError()
    data class InsufficientStock(val productId: String, val requested: Int, val available: Int) : OrderError()
    object PaymentDeclined : OrderError()
}

// Result type using Kotlin's built-in Result or custom
typealias OrderResult<T> = Result<T>  // Result.success() or Result.failure()

// Or a custom Either
sealed class Either<out L, out R> {
    data class Left<L>(val value: L) : Either<L, Nothing>()
    data class Right<R>(val value: R) : Either<Nothing, R>()
}

WebFlux Controller with Coroutines

// Coroutine-style: suspend functions + CoRouter (or @RestController)
@RestController
@RequestMapping("/api/orders")
class OrderController(
    private val orderService: OrderService,
) {
    // suspend fun = coroutine, Spring handles the thread switching
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    suspend fun createOrder(
        @Validated @RequestBody request: CreateOrderRequest,
        @AuthenticationPrincipal principal: UserPrincipal,
    ): OrderResponse {
        return when (val result = orderService.placeOrder(principal.userId, request)) {
            is Either.Right -> OrderResponse.from(result.value)
            is Either.Left -> when (val err = result.value) {
                is OrderError.InsufficientStock ->
                    throw ResponseStatusException(HttpStatus.CONFLICT, "Insufficient stock for ${err.productId}")
                is OrderError.PaymentDeclined ->
                    throw ResponseStatusException(HttpStatus.PAYMENT_REQUIRED, "Payment declined")
                else ->
                    throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Order failed")
            }
        }
    }
    
    @GetMapping("/{id}")
    suspend fun getOrder(
        @PathVariable id: String,
        @AuthenticationPrincipal principal: UserPrincipal,
    ): OrderResponse {
        val order = orderService.findByIdForCustomer(id, principal.userId)
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Order $id not found")
        return OrderResponse.from(order)
    }
    
    // Streaming endpoint using Flow
    @GetMapping("/stream", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
    fun orderStatusStream(@AuthenticationPrincipal principal: UserPrincipal): Flow<ServerSentEvent<OrderStatusUpdate>> {
        return orderService.watchOrders(principal.userId)
            .map { update -> ServerSentEvent.builder(update).build() }
    }
}

// DSL-style routing alternative (for functional style)
@Configuration
class RouterConfig(private val orderHandler: OrderHandler) {
    @Bean
    fun routerFunction() = coRouter {
        POST("/api/orders", orderHandler::createOrder)
        GET("/api/orders/{id}", orderHandler::getOrder)
    }
}

Service with Coroutines and R2DBC

@Service
class OrderService(
    private val orderRepo: OrderRepository,
    private val inventoryClient: InventoryClient,
    private val paymentClient: PaymentClient,
) {
    suspend fun placeOrder(customerId: String, request: CreateOrderRequest): Either<OrderError, Order> {
        // Parallel calls with coroutines
        val (inventoryCheck, customerCheck) = coroutineScope {
            val inv = async { inventoryClient.checkAvailability(request.items) }
            val cust = async { orderRepo.findCustomer(customerId) }
            inv.await() to cust.await()
        }
        
        // Check availability
        inventoryCheck.unavailableItems.firstOrNull()?.let { item ->
            return Either.Left(OrderError.InsufficientStock(item.productId, item.requested, item.available))
        }
        
        // Create order
        val order = Order(
            customerId = customerId,
            totalCents = request.items.sumOf { it.quantity.toLong() * it.unitPriceCents },
            items = request.items.map { OrderItem(it.productId, it.quantity, it.unitPriceCents) },
        )
        
        // Process payment
        val payment = paymentClient.charge(customerId, order.totalCents, request.paymentMethodId)
        if (!payment.success) return Either.Left(OrderError.PaymentDeclined)
        
        val savedOrder = orderRepo.save(order.copy(status = OrderStatus.CONFIRMED))
        return Either.Right(savedOrder)
    }
    
    fun watchOrders(customerId: String): Flow<OrderStatusUpdate> =
        orderRepo.watchByCustomer(customerId)
            .map { OrderStatusUpdate(it.id, it.status) }
            .catch { e -> emit(OrderStatusUpdate.error(e.message ?: "Unknown error")) }
}

R2DBC Repository

@Repository
interface OrderRepository : CoroutineCrudRepository<Order, String> {
    // Spring Data generates the query from the method name
    suspend fun findByCustomerIdAndStatus(customerId: String, status: OrderStatus): List<Order>
    
    // Custom query
    @Query("SELECT * FROM orders WHERE customer_id = :customerId ORDER BY created_at DESC LIMIT :limit")
    fun findRecentByCustomer(customerId: String, limit: Int): Flow<Order>
    
    // Reactive change stream (database-specific)
    @Query("SELECT * FROM orders WHERE customer_id = :customerId")
    fun watchByCustomer(customerId: String): Flow<Order>
}

Extension Functions for Readability

// Extension functions: add methods to existing types
fun Order.toResponse() = OrderResponse(
    id = id,
    customerId = customerId,
    status = status.name,
    totalDollars = totalCents / 100.0,
    createdAt = createdAt.toString(),
)

// Extension on String for parsing
fun String.toOrderStatus(): OrderStatus? = runCatching {
    OrderStatus.valueOf(uppercase())
}.getOrNull()

// Scope functions: let, apply, also, run, with
val enrichedOrder = order.copy(
    items = order.items.map { item ->
        item.also { log.debug("Processing item: ${it.productId}") }
    }
)

// Inline lambda arguments (trailing lambda syntax)
val totalRevenue = orders.sumOf { it.totalCents }
val confirmedOrders = orders.filter { it.status == OrderStatus.CONFIRMED }
val orderMap = orders.associateBy { it.id }

Testing with Kotest and MockK

// OrderServiceTest.kt
class OrderServiceTest : DescribeSpec({
    val orderRepo = mockk<OrderRepository>()
    val inventoryClient = mockk<InventoryClient>()
    val paymentClient = mockk<PaymentClient>()
    
    val service = OrderService(orderRepo, inventoryClient, paymentClient)
    
    describe("placeOrder") {
        context("when payment succeeds") {
            it("saves the order with CONFIRMED status") {
                coEvery { inventoryClient.checkAvailability(any()) } returns InventoryResult(emptyList())
                coEvery { paymentClient.charge(any(), any(), any()) } returns PaymentResult(success = true)
                coEvery { orderRepo.save(any()) } answers { firstArg() }
                
                val result = service.placeOrder("cust-1", createOrderRequest())
                
                result.shouldBeInstanceOf<Either.Right<Order>>()
                result.value.status shouldBe OrderStatus.CONFIRMED
                
                coVerify { orderRepo.save(match { it.customerId == "cust-1" }) }
            }
        }
        
        context("when inventory is insufficient") {
            it("returns InsufficientStock error without charging") {
                coEvery { inventoryClient.checkAvailability(any()) } returns
                    InventoryResult(listOf(UnavailableItem("prod-1", 5, 2)))
                
                val result = service.placeOrder("cust-1", createOrderRequest())
                
                result.shouldBeInstanceOf<Either.Left<OrderError>>()
                result.value.shouldBeInstanceOf<OrderError.InsufficientStock>()
                coVerify(exactly = 0) { paymentClient.charge(any(), any(), any()) }
            }
        }
    }
})

For the gRPC service patterns that Kotlin Spring apps can expose alongside REST, see the Protocol Buffers guide. For reactive stream processing that complements Kotlin Flow, the RxJS reactive guide covers the same concepts in the browser context. The Claude Skills 360 bundle includes Kotlin skill sets covering coroutines, sealed classes, WebFlux patterns, and Kotest. Start with the free tier to try Kotlin Spring Boot generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free