Spring Boot with Kotlin Explained
Spring Boot + Kotlin is the golden combo for backend microservices — Charlie uses data class for request/response bodies, suspend controller methods for non-blocking APIs, and extension functions to make code more Kotlin-idiomatic.
1. What You'll Learn
- Spring Boot + Kotlin configuration
- Controllers:
@RestController+ data class request/response bodies - Coroutine support:
suspendcontroller methods - JPA + Kotlin:
kotlin-jpacompiler plugin - Charlie's hands-on: OrderProcessor microservice
2. An Architect's Real Story
(1) Pain Point: Java Spring Boot Boilerplate
Charlie's Java Spring Boot controllers needed 30+ lines of POJO + getters/setters for every request/response. Async APIs using CompletableFuture nesting were hard to maintain.
(2) Kotlin Spring Boot Solution
KOTLIN
// Java: 30+ lines for request DTO
public class CreateOrderRequest {
private String customerId;
private Double total;
// getter/setter x 2 = 8 lines
}
// Kotlin: 1 line for request DTO
data class CreateOrderRequest(val customerId: String, val total: Double)
// Suspend controller method
@PostMapping
suspend fun createOrder(@RequestBody req: CreateOrderRequest): OrderResponse {
return orderService.createOrder(req) // Non-blocking!
}
data class + suspend reduces Spring Boot code by 60%, and async APIs read as cleanly as synchronous code.
3. Spring Boot + Kotlin Configuration
(1) build.gradle.kts
KOTLIN
plugins {
kotlin("jvm") version "1.9.22"
kotlin("plugin.spring") version "1.9.22" // Spring support
kotlin("plugin.jpa") version "1.9.22" // JPA no-arg
kotlin("plugin.serialization") version "1.9.22"
id("org.springframework.boot") version "3.2.1"
id("io.spring.dependency-management") version "1.1.4"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux") // For coroutine support
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
(2) Kotlin Spring Plugin Overview
| Plugin | Purpose |
|---|---|
kotlin-spring |
Auto-opens classes annotated with @Component, @Transactional, etc. |
kotlin-jpa |
Generates no-arg constructors for @Entity classes |
kotlin-serialization |
Enables data class JSON serialization |
4. REST Controllers
(1) Basic Controller
KOTLIN
@RestController
@RequestMapping("/api/orders")
class OrderController(private val orderService: OrderService) {
@GetMapping
fun getAllOrders(): List<OrderResponse> = orderService.findAll()
@GetMapping("/{id}")
fun getOrder(@PathVariable id: String): OrderResponse =
orderService.findById(id) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@PostMapping
fun createOrder(@RequestBody request: CreateOrderRequest): OrderResponse =
orderService.create(request)
}
(2) Request/Response data class
KOTLIN
data class CreateOrderRequest(
val customerId: String,
val total: Double,
val items: List<OrderItemRequest> = emptyList()
)
data class OrderItemRequest(
val sku: String,
val quantity: Int,
val unitPrice: Double
)
data class OrderResponse(
val id: String,
val total: Double,
val status: String,
val customer: String
)
(3) Java DTO vs Kotlin data class
| Dimension | Java DTO | Kotlin data class |
|---|---|---|
| Lines of code | 30+ lines | 3 lines |
| equals/hashCode | Manual/Lombok | Auto-generated |
| Immutability | Manual effort | Default val |
| Default values | Method overloading | Parameter defaults |
| JSON mapping | Jackson annotations | jackson-module-kotlin auto |
5. Coroutine Support
(1) Suspend Controller Methods
KOTLIN
// Add webflux dependency for coroutine support
@RestController
@RequestMapping("/api/orders")
class OrderController(private val orderService: OrderService) {
// Suspend controller: non-blocking, runs on Netty event loop
@GetMapping("/{id}")
suspend fun getOrder(@PathVariable id: String): OrderResponse {
return orderService.findByIdAsync(id)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@PostMapping
suspend fun createOrder(@RequestBody request: CreateOrderRequest): OrderResponse {
return orderService.createAsync(request)
}
// Flow endpoint: streaming response
@GetMapping("/stream")
fun orderStream(): Flow<OrderResponse> = orderService.orderStream()
}
(2) Spring Boot Request Processing Chain
flowchart TD
A[HTTP Request] --> B[DispatcherServlet<br/>or Netty]
B --> C[Controller<br/>@RestController]
C --> D{suspend?}
D -->|Yes| E[Coroutine Context<br/>Non-blocking]
D -->|No| F[Servlet Thread<br/>Blocking]
E --> G[Service Layer<br/>suspend functions]
F --> G
G --> H[Repository<br/>R2DBC / JPA]
H --> I[Database]
(3) Blocking vs Non-Blocking Comparison
| Dimension | Blocking (MVC) | Non-Blocking (WebFlux + coroutines) |
|---|---|---|
| Thread model | One thread per request | Event loop |
| Concurrency limit | ~200 (thread pool) | ~100,000+ (coroutines) |
| Code style | Synchronous | suspend (reads like sync) |
| Database | JPA (blocking) | R2DBC (reactive) |
| Throughput | Medium | High |
6. JPA + Kotlin
(1) Entity Definition
KOTLIN
@Entity
@Table(name = "orders")
class OrderEntity(
@Id @GeneratedValue(strategy = GenerationType.UUID)
val id: String = "",
val total: Double = 0.0,
val status: String = "PENDING",
val customerId: String = "",
@CreationTimestamp
val createdAt: LocalDateTime = LocalDateTime.now()
)
// Repository
interface OrderJpaRepository : JpaRepository<OrderEntity, String> {
fun findByCustomerId(customerId: String): List<OrderEntity>
fun countByStatus(status: String): Long
}
(2) no-arg Plugin
KOTLIN
// kotlin-jpa plugin auto-generates no-arg constructor for @Entity classes
// Without it, JPA cannot instantiate Kotlin classes (all have constructor params)
7. Complete Example: OrderProcessor Microservice
KOTLIN
// ============================================
// OrderProcessor - Spring Boot Microservice
// Feature: REST API + suspend + data class
// ============================================
// --- Domain ---
data class Order(val id: String, val total: Double, var status: String, val customerId: String)
// --- Request/Response ---
data class CreateOrderRequest(val customerId: String, val total: Double)
data class OrderResponse(val id: String, val total: Double, val status: String, val customerId: String)
data class UpdateStatusRequest(val status: String)
// --- Repository (simulated) ---
class OrderRepository {
private val storage = mutableMapOf<String, Order>()
fun save(order: Order): Order {
storage[order.id] = order
return order
}
fun findById(id: String): Order? = storage[id]
fun findAll(): List<Order> = storage.values.toList()
fun deleteById(id: String) { storage.remove(id) }
}
// --- Service ---
class OrderService(private val repo: OrderRepository) {
private var idCounter = 0L
fun create(request: CreateOrderRequest): Order {
val order = Order("ORD-${++idCounter}", request.total, "PENDING", request.customerId)
return repo.save(order)
}
fun findById(id: String): Order? = repo.findById(id)
fun findAll(): List<Order> = repo.findAll()
fun updateStatus(id: String, newStatus: String): Order {
val order = repo.findById(id) ?: throw NoSuchElementException("Order $id not found")
order.status = newStatus
return repo.save(order)
}
fun delete(id: String) = repo.deleteById(id)
}
// --- Controller (simulated, requires Spring Boot runtime) ---
// @RestController
// @RequestMapping("/api/orders")
// class OrderController(private val orderService: OrderService) {
// @GetMapping fun getAll() = orderService.findAll().map { it.toResponse() }
// @GetMapping("/{id}") fun getOne(@PathVariable id: String) = orderService.findById(id)?.toResponse()
// @PostMapping fun create(@RequestBody req: CreateOrderRequest) = orderService.create(req).toResponse()
// }
// --- Extension for mapping ---
fun Order.toResponse() = OrderResponse(id, total, status, customerId)
// --- Demo ---
fun main() {
val repo = OrderRepository()
val service = OrderService(repo)
println("=== Spring Boot OrderProcessor API Demo ===\n")
// POST /api/orders
val order1 = service.create(CreateOrderRequest("CUST-001", 299.99))
println("POST /api/orders -> ${order1.toResponse()}")
val order2 = service.create(CreateOrderRequest("CUST-002", 15_000.00))
println("POST /api/orders -> ${order2.toResponse()}")
// GET /api/orders
println("\nGET /api/orders -> ${service.findAll().map { it.toResponse() }}")
// GET /api/orders/{id}
println("GET /api/orders/ORD-1 -> ${service.findById("ORD-1")?.toResponse()}")
// PATCH /api/orders/{id}/status
val updated = service.updateStatus("ORD-1", "CONFIRMED")
println("PATCH /api/orders/ORD-1/status -> ${updated.toResponse()}")
// Summary
val revenue = service.findAll().filter { it.status != "CANCELLED" }.sumOf { it.total }
println("\nTotal Revenue: \$$revenue USD across ${service.findAll().size} orders")
}
Output:
TEXT
=== Spring Boot OrderProcessor API Demo ===
POST /api/orders -> OrderResponse(id=ORD-1, total=299.99, status=PENDING, customerId=CUST-001)
POST /api/orders -> OrderResponse(id=ORD-2, total=15000.0, status=PENDING, customerId=CUST-002)
GET /api/orders -> [OrderResponse(id=ORD-1, total=299.99, status=PENDING, customerId=CUST-001), OrderResponse(id=ORD-2, total=15000.0, status=PENDING, customerId=CUST-002)]
GET /api/orders/ORD-1 -> OrderResponse(id=ORD-1, total=299.99, status=PENDING, customerId=CUST-001)
PATCH /api/orders/ORD-1/status -> OrderResponse(id=ORD-1, total=299.99, status=CONFIRMED, customerId=CUST-001)
Total Revenue: $15299.99 USD across 2 orders
❓ FAQ
Q Should I use MVC or WebFlux for Spring Boot?
A WebFlux + coroutines is recommended for new projects —
suspend reads like synchronous code but performs non-blocking. If your team is more familiar with JPA and doesn't need extreme concurrency, MVC + coroutines works too.Q Can Kotlin data class be used as a JPA Entity?
A Possible but has limitations — data classes are immutable by nature, while JPA needs mutable entities for dirty checking. Recommended: use plain
class + var properties for entities, data class for DTOs.Q What does the
kotlin-spring plugin do?A It automatically marks classes with
@Component, @Transactional, etc. annotations as open, since Spring AOP needs to create proxies (requiring inheritable classes).Q Do suspend controller methods require WebFlux?
A Yes. Spring MVC does not natively support
suspend — WebFlux dependencies are required. Spring 6.1+ has improved support, but WebFlux is still recommended.Q How do I use Kotlin coroutines in Spring Boot?
A Add
spring-boot-starter-webflux dependency, mark controller methods as suspend, use suspend functions in the Service layer, and use R2DBC or coroutine extensions for the Repository.Q Is
jackson-module-kotlin mandatory?A Strongly recommended. Without it, Jackson cannot correctly deserialize Kotlin data classes (constructor parameters, nullability, and default value handling all fail).
📖 Summary
- Spring Boot + Kotlin configuration requires
kotlin-springandkotlin-jpaplugins - data class for request/response DTOs: 1 line replaces 30+ lines of Java
suspendcontroller methods + WebFlux enable non-blocking APIskotlin-jpaplugin generates no-arg constructors for entities- Extension functions like
toResponse()separate domain models from API models - WebFlux + coroutines throughput far exceeds MVC + thread pools
📝 Exercises
- Beginner (⭐): Define
CreateUserRequestandUserResponseusing data class, and write a Spring Boot controller. Hint:@RestController+@PostMapping - Intermediate (⭐⭐): Implement a
suspendcontroller method that calls asuspendService function to fetch an order. Hint:suspend fun getOrder(@PathVariable id: String) - Advanced (⭐⭐⭐): Implement a complete CRUD REST API + coroutines + R2DBC (or simulated Repository), including exception handling and validation. Hint:
@RestControllerAdvice+@Valid



