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á
- Configuração Spring Boot + Kotlin
- Controllers:
@RestController+ corpos de requisição/resposta com data class - Suporte a corrotinas: métodos controller
suspend - JPA + Kotlin: plugin de compilador
kotlin-jpa - Charlie na prática: microsserviço OrderProcessor
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
// 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
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
@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
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
// 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
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
@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
// 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
// ============================================
// 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:
=== 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
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
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
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
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
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
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
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 —
suspendlê 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
classsimples + propriedadesvarpara entidades, data class para DTOs.
P: O que o plugin
kotlin-springfaz? R: Ele marca automaticamente classes com anotações@Component,@Transactional, etc. comoopen, 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 comosuspend, use funçõessuspendna 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
- Configuração Spring Boot + Kotlin requer plugins
kotlin-springekotlin-jpa - data class para DTOs de requisição/resposta: 1 linha substitui 30+ linhas de Java
- Métodos controller
suspend+ WebFlux habilitam APIs não-bloqueantes - Plugin
kotlin-jpagera construtores sem argumentos para entidades - Funções de extensão como
toResponse()separam modelos de domínio de modelos de API - WebFlux + corrotinas com throughput muito superior a MVC + pools de threads
📝 Exercícios
- Iniciante (⭐): Defina
CreateUserRequesteUserResponseusando data class, e escreva um controller Spring Boot. Dica:@RestController+@PostMapping - Intermediário (⭐⭐): Implemente um método controller
suspendque chama uma funçãosuspendde Serviço para buscar um pedido. Dica:suspend fun getOrder(@PathVariable id: String) - 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