Kotlin: Spring Boot com Kotlin Explicado

Última atualização: 2026-08-26

Spring Boot + Kotlin é a combinação dourada para microsserviços de backend — Charlie usa data class para corpos de requisição/resposta, métodos controller suspend para APIs não-bloqueantes, e funções de extensão para tornar o código mais idiomático em Kotlin.

1. O que Você Aprenderá


2. A História Real de um Arquiteto

(1) Problema: Boilerplate do Java Spring Boot

Os controllers Java Spring Boot do Charlie precisavam de 30+ linhas de POJO + getters/setters para cada requisição/resposta. APIs assíncronas usando aninhamento de CompletableFuture eram difíceis de manter.

(2) Solução Kotlin Spring Boot

KOTLIN
// Java: 30+ linhas para DTO de requisição
public class CreateOrderRequest {
    private String customerId;
    private Double total;
    // getter/setter x 2 = 8 linhas
}

// Kotlin: 1 linha para DTO de requisição
data class CreateOrderRequest(val customerId: String, val total: Double)

// Método controller suspend
@PostMapping
suspend fun createOrder(@RequestBody req: CreateOrderRequest): OrderResponse {
    return orderService.createOrder(req)  // Não-bloqueante!
}

data class + suspend reduz o código Spring Boot em 60%, e APIs assíncronas são tão legíveis quanto código síncrono.


3. Configuração Spring Boot + Kotlin

(1) build.gradle.kts

KOTLIN
plugins {
    kotlin("jvm") version "1.9.22"
    kotlin("plugin.spring") version "1.9.22"   // Suporte Spring
    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")  // Para suporte a corrotinas
    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) Visão Geral dos Plugins Kotlin Spring

Plugin Propósito
kotlin-spring Abre automaticamente classes anotadas com @Component, @Transactional, etc.
kotlin-jpa Gera construtores sem argumentos para classes @Entity
kotlin-serialization Habilita serialização JSON de data classes

4. Controllers REST

(1) Controller Básico

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) data class de Requisição/Resposta

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) DTO Java vs data class Kotlin

Dimensão DTO Java data class Kotlin
Linhas de código 30+ linhas 3 linhas
equals/hashCode Manual/Lombok Auto-gerado
Imutabilidade Esforço manual val por padrão
Valores padrão Sobrecarga de métodos Padrões de parâmetros
Mapeamento JSON Anotações Jackson jackson-module-kotlin automático

5. Suporte a Corrotinas

(1) Métodos Controller Suspend

KOTLIN
// Adicionar dependência webflux para suporte a corrotinas
@RestController
@RequestMapping("/api/orders")
class OrderController(private val orderService: OrderService) {

    // Controller suspend: não-bloqueante, roda no event loop do Netty
    @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)
    }

    // Endpoint Flow: resposta em streaming
    @GetMapping("/stream")
    fun orderStream(): Flow<OrderResponse> = orderService.orderStream()
}

(2) Cadeia de Processamento de Requisições do Spring Boot

100%
flowchart TD
    A[Requisição HTTP] --> B[DispatcherServlet<br/>ou Netty]
    B --> C[Controller<br/>@RestController]
    C --> D{suspend?}
    D -->|Sim| E[Contexto de Corrotina<br/>Não-bloqueante]
    D -->|Não| F[Thread Servlet<br/>Bloqueante]
    E --> G[Camada de Serviço<br/>Funções suspend]
    F --> G
    G --> H[Repositório<br/>R2DBC / JPA]
    H --> I[Banco de Dados]

(3) Comparação Bloqueante vs Não-Bloqueante

Dimensão Bloqueante (MVC) Não-Bloqueante (WebFlux + corrotinas)
Modelo de threads Uma thread por requisição Event loop
Limite de concorrência ~200 (pool de threads) ~100.000+ (corrotinas)
Estilo de código Síncrono suspend (lê como síncrono)
Banco de dados JPA (bloqueante) R2DBC (reativo)
Throughput Médio Alto

6. JPA + Kotlin

(1) Definição de Entity

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) Plugin no-arg

KOTLIN
// O plugin kotlin-jpa gera automaticamente construtor sem argumentos para classes @Entity
// Sem ele, JPA não consegue instanciar classes Kotlin (todas têm parâmetros no construtor)

7. Exemplo Completo: Microsserviço OrderProcessor

KOTLIN
// ============================================
// OrderProcessor - Microsserviço Spring Boot
// Funcionalidade: API REST + suspend + data class
// ============================================

// --- Domínio ---
data class Order(val id: String, val total: Double, var status: String, val customerId: String)

// --- Requisição/Resposta ---
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)

// --- Repositório (simulado) ---
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) }
}

// --- Serviço ---
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 (simulado, requer runtime Spring Boot) ---
// @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()
// }

// --- Extensão para mapeamento ---
fun Order.toResponse() = OrderResponse(id, total, status, customerId)

// --- Demo ---
fun main() {
    val repo = OrderRepository()
    val service = OrderService(repo)

    println("=== Demo da API OrderProcessor Spring Boot ===\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()}")

    // Resumo
    val revenue = service.findAll().filter { it.status != "CANCELLED" }.sumOf { it.total }
    println("\nReceita Total: \$$revenue USD em ${service.findAll().size} pedidos")
}

Saída:

TEXT 📖 Somente leitura
=== Demo da API OrderProcessor Spring Boot ===

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)

Receita Total: $15299.99 USD em 2 pedidos

9. Exemplos práticos rápidos

▶ Exemplo: Spring Boot Application

KOTLIN
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class OrderApplication

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

▶ Exemplo: REST Controller

KOTLIN
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/orders")
class OrderController(private val repo: OrderRepository) {

    @GetMapping
    fun list(): List<Order> = repo.findAll()

    @GetMapping("/{id}")
    fun get(@PathVariable id: String): Order =
        repo.findById(id).orElseThrow { RuntimeException("Pedido não encontrado: $id") }

    @PostMapping
    fun create(@RequestBody order: Order): Order = repo.save(order)

    @PutMapping("/{id}/status")
    fun updateStatus(@PathVariable id: String, @RequestParam status: String): Order {
        val order = repo.findById(id).orElseThrow()
        return repo.save(order.copy(status = status))
    }

    @DeleteMapping("/{id}")
    fun delete(@PathVariable id: String) = repo.deleteById(id)
}

▶ Exemplo: JPA Entity e Repository

KOTLIN
import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository

@Entity
@Table(name = "orders")
data class Order(
    @Id
    val id: String,
    val customerId: String,
    val total: Double,
    val status: String = "PENDING"
)

interface OrderRepository : JpaRepository<Order, String> {
    fun findByStatus(status: String): List<Order>
    fun findByCustomerId(customerId: String): List<Order>
}

▶ Exemplo: Service layer

KOTLIN
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class OrderService(private val repo: OrderRepository) {

    @Transactional
    fun create(order: Order): Order {
        require(order.total > 0) { "Total deve ser positivo" }
        return repo.save(order.copy(status = "PENDING"))
    }

    @Transactional(readOnly = true)
    fun findById(id: String): Order =
        repo.findById(id).orElseThrow { NoSuchElementException("Pedido $id") }

    @Transactional
    fun confirm(id: String): Order {
        val order = findById(id)
        check(order.status == "PENDING") { "Pedido não está pendente" }
        return repo.save(order.copy(status = "CONFIRMED"))
    }

    @Transactional
    fun cancel(id: String, reason: String): Order {
        val order = findById(id)
        return repo.save(order.copy(status = "CANCELLED"))
    }
}

▶ Exemplo: Validation

KOTLIN
import jakarta.validation.constraints.*

data class CreateOrderRequest(
    @field:NotBlank(message = "ID é obrigatório")
    val id: String,

    @field:NotBlank
    val customerId: String,

    @field:Positive(message = "Total deve ser positivo")
    val total: Double
)

@PostMapping
fun create(@Valid @RequestBody request: CreateOrderRequest): ResponseEntity<Order> {
    val order = Order(
        id = request.id,
        customerId = request.customerId,
        total = request.total
    )
    return ResponseEntity.ok(service.create(order))
}

▶ Exemplo: Testes com MockMvc

KOTLIN
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.junit.jupiter.api.Test
import org.mockito.Mockito.*
import org.springframework.http.MediaType

class OrderControllerTest {
    private val service = mock(OrderService::class.java)
    private val controller = OrderController(service)
    private val mockMvc: MockMvc = MockMvcBuilders.standaloneSetup(controller).build()

    @Test
    fun `criar pedido retorna 200`() {
        val order = Order("ORD-001", "Alice", 299.99)
        `when`(service.create(order)).thenReturn(order)

        val result = mockMvc.perform(
            org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""{"id":"ORD-001","customerId":"Alice","total":299.99}""")
        ).andReturn()

        println("Status: ${result.response.status}")
        assert(result.response.status == 200)
    }
}

▶ Exemplo: Configuration properties

KOTLIN
import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "app")
data class AppProperties(
    val name: String = "OrderApp",
    val maxConnections: Int = 100,
    val features: Map<String, Boolean> = emptyMap()
)

// Habilitar na application class
@SpringBootApplication
@EnableConfigurationProperties(AppProperties::class)
class OrderApplication

// application.yml
// app:
//   name: OrderService
//   max-connections: 500
//   features:
//     notifications: true
//     analytics: false

// Injetar
@Service
class FeatureFlagService(private val props: AppProperties) {
    fun isEnabled(feature: String) = props.features[feature] ?: false
}

❓ Perguntas Frequentes

P: Devo usar MVC ou WebFlux no Spring Boot? R: WebFlux + corrotinas é recomendado para novos projetos — suspend lê como código síncrono mas executa de forma não-bloqueante. Se sua equipe é mais familiarizada com JPA e não precisa de concorrência extrema, MVC + corrotinas também funciona.

P: data class Kotlin pode ser usada como JPA Entity? R: Possível mas com limitações — data classes são imutáveis por natureza, enquanto JPA precisa de entidades mutáveis para dirty checking. Recomendado: use class simples + propriedades var para entidades, data class para DTOs.

P: O que o plugin kotlin-spring faz? R: Ele marca automaticamente classes com anotações @Component, @Transactional, etc. como open, já que Spring AOP precisa criar proxies (requerendo classes herdáveis).

P: Métodos controller suspend requerem WebFlux? R: Sim. Spring MVC não suporta nativamente suspend — dependências WebFlux são necessárias. Spring 6.1+ melhorou o suporte, mas WebFlux ainda é recomendado.

P: Como usar corrotinas Kotlin no Spring Boot? R: Adicione dependência spring-boot-starter-webflux, marque métodos controller como suspend, use funções suspend na camada de Serviço, e use R2DBC ou extensões de corrotinas para o Repository.

P: jackson-module-kotlin é obrigatório? R: Fortemente recomendado. Sem ele, Jackson não consegue desserializar corretamente data classes Kotlin (parâmetros do construtor, nulabilidade e tratamento de valores padrão falham).


📖 Resumo


📝 Exercícios

  1. Iniciante (⭐): Defina CreateUserRequest e UserResponse usando data class, e escreva um controller Spring Boot. Dica: @RestController + @PostMapping
  2. Intermediário (⭐⭐): Implemente um método controller suspend que chama uma função suspend de Serviço para buscar um pedido. Dica: suspend fun getOrder(@PathVariable id: String)
  3. Avançado (⭐⭐⭐): Implemente uma API REST CRUD completa + corrotinas + R2DBC (ou Repositório simulado), incluindo tratamento de exceções e validação. Dica: @RestControllerAdvice + @Valid

← Anterior | Próximo →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%