Kotlin e Spring Boot formam uma combinacao poderosa para desenvolvimento backend na JVM. O Spring oferece suporte oficial a Kotlin desde a versao 5, com extensoes dedicadas que aproveitam recursos como null safety, data classes, extension functions e coroutines. Muitas empresas que ja usam Spring estao migrando de Java para Kotlin no backend, beneficiando-se de um codigo mais conciso e seguro sem abrir mao do vasto ecossistema Spring. Este guia cobre desde a configuracao inicial ate padroes avancados para aplicacoes em producao.

Configurando o Projeto

O Spring Initializr (start.spring.io) permite gerar projetos Kotlin com Spring Boot. A configuracao no build.gradle.kts inclui plugins especificos:

plugins {
    id("org.springframework.boot") version "3.2.2"
    id("io.spring.dependency-management") version "1.1.4"
    kotlin("jvm") version "1.9.22"
    kotlin("plugin.spring") version "1.9.22"
    kotlin("plugin.jpa") version "1.9.22"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    runtimeOnly("org.postgresql:postgresql")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

O plugin kotlin-spring adiciona automaticamente o modificador open as classes anotadas com @Component, @Service, @Repository e @Configuration, necessario para os proxies do Spring. O plugin kotlin-jpa gera construtores no-arg para entidades JPA.

Classe Principal da Aplicacao

@SpringBootApplication
class MeuAppApplication

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

A funcao runApplication e uma extension function do Spring para Kotlin, substituindo o padrao Java SpringApplication.run().

Entidades JPA com Kotlin

Data classes nao sao ideais para entidades JPA, pois o JPA requer mutabilidade e proxies. A abordagem recomendada:

@Entity
@Table(name = "produtos")
class Produto(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(nullable = false)
    var nome: String,

    @Column(nullable = false)
    var preco: BigDecimal,

    @Column(length = 1000)
    var descricao: String? = null,

    @Column(name = "criado_em")
    val criadoEm: LocalDateTime = LocalDateTime.now(),

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "categoria_id")
    var categoria: Categoria? = null
)

@Entity
@Table(name = "categorias")
class Categoria(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(nullable = false, unique = true)
    var nome: String,

    @OneToMany(mappedBy = "categoria", cascade = [CascadeType.ALL])
    val produtos: MutableList<Produto> = mutableListOf()
)

Repositories com Spring Data

O Spring Data JPA gera implementacoes automaticamente a partir de interfaces:

interface ProdutoRepository : JpaRepository<Produto, Long> {

    fun findByNomeContainingIgnoreCase(nome: String): List<Produto>

    fun findByCategoriaId(categoriaId: Long): List<Produto>

    @Query("SELECT p FROM Produto p WHERE p.preco BETWEEN :min AND :max")
    fun findByFaixaDePreco(
        @Param("min") min: BigDecimal,
        @Param("max") max: BigDecimal
    ): List<Produto>

    fun findByNomeContainingIgnoreCase(
        nome: String,
        pageable: Pageable
    ): Page<Produto>
}

Camada de Servico

A camada de servico contem a logica de negocio e coordena operacoes entre repositories:

@Service
class ProdutoService(
    private val produtoRepository: ProdutoRepository,
    private val categoriaRepository: CategoriaRepository
) {

    fun listarTodos(pageable: Pageable): Page<ProdutoResponse> {
        return produtoRepository.findAll(pageable).map { it.toResponse() }
    }

    fun buscarPorId(id: Long): ProdutoResponse {
        val produto = produtoRepository.findById(id)
            .orElseThrow { RecursoNaoEncontradoException("Produto $id nao encontrado") }
        return produto.toResponse()
    }

    @Transactional
    fun criar(request: CriarProdutoRequest): ProdutoResponse {
        val categoria = request.categoriaId?.let {
            categoriaRepository.findById(it)
                .orElseThrow { RecursoNaoEncontradoException("Categoria $it nao encontrada") }
        }

        val produto = Produto(
            nome = request.nome,
            preco = request.preco,
            descricao = request.descricao,
            categoria = categoria
        )

        return produtoRepository.save(produto).toResponse()
    }

    @Transactional
    fun atualizar(id: Long, request: AtualizarProdutoRequest): ProdutoResponse {
        val produto = produtoRepository.findById(id)
            .orElseThrow { RecursoNaoEncontradoException("Produto $id nao encontrado") }

        request.nome?.let { produto.nome = it }
        request.preco?.let { produto.preco = it }
        request.descricao?.let { produto.descricao = it }

        return produtoRepository.save(produto).toResponse()
    }

    @Transactional
    fun deletar(id: Long) {
        if (!produtoRepository.existsById(id)) {
            throw RecursoNaoEncontradoException("Produto $id nao encontrado")
        }
        produtoRepository.deleteById(id)
    }
}

DTOs e Mapeamento

Data classes sao perfeitas para DTOs de request e response:

data class CriarProdutoRequest(
    @field:NotBlank(message = "Nome e obrigatorio")
    val nome: String,

    @field:Positive(message = "Preco deve ser positivo")
    val preco: BigDecimal,

    val descricao: String? = null,
    val categoriaId: Long? = null
)

data class AtualizarProdutoRequest(
    val nome: String? = null,
    val preco: BigDecimal? = null,
    val descricao: String? = null
)

data class ProdutoResponse(
    val id: Long,
    val nome: String,
    val preco: BigDecimal,
    val descricao: String?,
    val categoriaNome: String?,
    val criadoEm: LocalDateTime
)

// Extension function para mapeamento
fun Produto.toResponse() = ProdutoResponse(
    id = id,
    nome = nome,
    preco = preco,
    descricao = descricao,
    categoriaNome = categoria?.nome,
    criadoEm = criadoEm
)

REST Controllers

@RestController
@RequestMapping("/api/produtos")
class ProdutoController(
    private val produtoService: ProdutoService
) {

    @GetMapping
    fun listar(
        @RequestParam(defaultValue = "0") pagina: Int,
        @RequestParam(defaultValue = "20") tamanho: Int,
        @RequestParam(defaultValue = "nome") ordenarPor: String
    ): Page<ProdutoResponse> {
        val pageable = PageRequest.of(pagina, tamanho, Sort.by(ordenarPor))
        return produtoService.listarTodos(pageable)
    }

    @GetMapping("/{id}")
    fun buscar(@PathVariable id: Long): ProdutoResponse {
        return produtoService.buscarPorId(id)
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun criar(@Valid @RequestBody request: CriarProdutoRequest): ProdutoResponse {
        return produtoService.criar(request)
    }

    @PutMapping("/{id}")
    fun atualizar(
        @PathVariable id: Long,
        @Valid @RequestBody request: AtualizarProdutoRequest
    ): ProdutoResponse {
        return produtoService.atualizar(id, request)
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    fun deletar(@PathVariable id: Long) {
        produtoService.deletar(id)
    }
}

Tratamento Global de Erros

@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(RecursoNaoEncontradoException::class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    fun handleRecursoNaoEncontrado(ex: RecursoNaoEncontradoException): ErroResponse {
        return ErroResponse(
            status = 404,
            mensagem = ex.message ?: "Recurso nao encontrado",
            timestamp = LocalDateTime.now()
        )
    }

    @ExceptionHandler(MethodArgumentNotValidException::class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    fun handleValidacao(ex: MethodArgumentNotValidException): ErroResponse {
        val erros = ex.bindingResult.fieldErrors.associate {
            it.field to (it.defaultMessage ?: "Valor invalido")
        }
        return ErroResponse(
            status = 400,
            mensagem = "Erro de validacao",
            detalhes = erros,
            timestamp = LocalDateTime.now()
        )
    }
}

data class ErroResponse(
    val status: Int,
    val mensagem: String,
    val detalhes: Map<String, String>? = null,
    val timestamp: LocalDateTime
)

class RecursoNaoEncontradoException(message: String) : RuntimeException(message)

Boas Praticas para Kotlin com Spring Boot

  • Use o plugin kotlin-spring: ele evita a necessidade de abrir classes manualmente para proxies do Spring.
  • Prefira constructor injection: o Kotlin facilita isso com parametros no construtor primario. Evite @Autowired em campos.
  • Nao use data classes para entidades JPA: data classes geram equals/hashCode baseados em todos os campos, o que e problematico com lazy loading.
  • Aproveite null safety: use tipos nullable do Kotlin para campos opcionais em vez de Optional do Java.
  • Use extension functions para mapeamento: elas sao mais limpas que classes Mapper separadas para conversao entre entidades e DTOs.
  • Configure o Jackson corretamente: o modulo jackson-module-kotlin e essencial para serializar/deserializar data classes corretamente.

Erros Comuns e Armadilhas

  • Esquecer o plugin kotlin-jpa: sem ele, entidades JPA sem construtor no-arg causam erros em runtime.
  • Data classes como entidades: o equals e hashCode gerados automaticamente causam problemas com proxies lazy do Hibernate.
  • Nao registrar o modulo Kotlin do Jackson: sem o jackson-module-kotlin, data classes nao sao deserializadas corretamente, resultando em erros confusos.
  • Usar lateinit sem necessidade: em Spring, constructor injection elimina a necessidade de lateinit var na maioria dos casos.
  • Ignorar paginacao: retornar listas completas em endpoints sem paginacao pode causar problemas serios de performance com tabelas grandes.

Conclusao e Proximos Passos

Kotlin com Spring Boot oferece uma experiencia de desenvolvimento backend produtiva e robusta. A combinacao do ecossistema maduro do Spring com a expressividade do Kotlin resulta em aplicacoes mais seguras e concisas. Para ir alem, explore Spring Security para autenticacao, WebFlux com coroutines para aplicacoes reativas e consulte nossos guias sobre testes, Docker e microservicos para completar sua stack de desenvolvimento.