404 Not Found

404 Not Found


nginx

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


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

100%
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


📝 Exercises

  1. Beginner (⭐): Define CreateUserRequest and UserResponse using data class, and write a Spring Boot controller. Hint: @RestController + @PostMapping
  2. Intermediate (⭐⭐): Implement a suspend controller method that calls a suspend Service function to fetch an order. Hint: suspend fun getOrder(@PathVariable id: String)
  3. Advanced (⭐⭐⭐): Implement a complete CRUD REST API + coroutines + R2DBC (or simulated Repository), including exception handling and validation. Hint: @RestControllerAdvice + @Valid

← Previous | Next →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏