Pular para o conteúdo

Trilha de Kotlin · parte 36 de 59

Gradle Kotlin DSL e multi-módulo

Aula da fase 6 da trilha de Kotlin: Como organizar módulos e um catálogo de versões para que o build continue claro com o crescimento do projeto?

intermediárioPublicada em · 16 min de leitura

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

Todo projeto precisa de uma receita que diga como transformar código em aplicação: quais bibliotecas baixar, em que ordem compilar, que testes rodar e como empacotar o resultado. Em um serviço pequeno, essa receita cabe em um arquivo de vinte linhas que ninguém precisa reler. Quando o serviço cresce para três, quatro ou cinco módulos, o mesmo arquivo vira um emaranhado em que uma versão de biblioteca aparece em cinco lugares e ninguém sabe qual deles manda. Esta aula trata de como o Gradle, com o Kotlin DSL, organiza essa receita para que o build continue legível à medida que o projeto cresce.

As versões desta fase são as mesmas das fases 5 e 6: Kotlin 2.3.21, que é a versão gerenciada pelo BOM do Spring Boot 4.1.1, e Spring Framework 7.0.9, também gerenciado pelo mesmo BOM. A documentação do Spring Boot pede Kotlin 2.2.x como mínimo, então 2.3.21 atende. O Gradle usado como referência é o 9.8.2, que era a versão atual publicada pelo próprio Gradle na data da revisão, 20/10/2026.

Uma comparação de cozinha ajuda a fixar a ideia. Uma receita de bolo que cabe num caderno é fácil de seguir. Um banquete com cinco pratos, em que dois deles usam a mesma calda, precisa de uma lista separada de ingredientes e de uma regra clara para dizer quem fornece o quê. O catálogo de versões e os módulos do Gradle fazem exatamente esse papel: separam o que é compartilhado do que é específico de cada prato.

Uma nota sobre o que foi executado. Esta máquina não tem o Gradle instalado, e a revisão desta aula não rodou nenhum build com ele. Os arquivos mostrados abaixo são o esqueleto que a aula descreve, e foram conferidos contra a documentação oficial. As coordenadas de biblioteca do catálogo foram compiladas com Maven, numa versão de verificação do mesmo projeto. A lacuna do Gradle está declarada na seção de fontes.

o mapa da aula

A aula começa pelo que é um script de build e por que o Kotlin DSL existe, com o Groovy como alternativa. Em seguida mostra o catálogo de versões, o arquivo gradle/libs.versions.toml, que centraliza as versões. Depois entra o build multi-projeto, com settings.gradle.kts declarando os módulos e as dependências entre eles. A diferença entre api e implementation, que controla o que vaza para quem consome um módulo, vem logo depois, porque é o ponto em que um projeto multi-módulo costuma ganhar ou perder clareza. A última seção trata do BOM do Spring Boot, que preenche as versões dos starters, e do que a documentação diz sobre quando ele é importado.

1. o que é o Gradle e o que o script faz

O Gradle é uma ferramenta de build. Ele lê um script, descobre quais tarefas precisam rodar e em que ordem, baixa as dependências e executa cada tarefa, como compilar, testar e empacotar. No mundo JVM ele disputa espaço com o Maven, que usa XML e uma estrutura mais rígida. A diferença prática está no script: o Gradle permite escrever lógica de verdade no arquivo de build. Isso dá flexibilidade, e também dá a chance de o arquivo ficar difícil de ler.

O script pode ser escrito em duas linguagens. O Groovy DSL usa arquivos build.gradle, e o Kotlin DSL usa arquivos build.gradle.kts. A diferença mais importante está em como as propriedades são resolvidas. No Groovy, nomes são resolvidos dinamicamente, e um erro de digitação só aparece quando a tarefa roda. No Kotlin DSL, o script é código Kotlin, e os acessores de módulos, como libs e plugins, são tipados. Um erro de digitação aparece na edição, antes de o build começar, e o autocompletar da IDE funciona sobre o próprio script.

Esse custo tem um lado que vale conhecer. Como o script é código Kotlin compilado e executado pelo Gradle, um erro de sintaxe no build.gradle.kts impede qualquer tarefa de começar. E um bloco plugins { } mal escrito impede o acesso aos acessores tipados que vêm depois dele, porque os acessores só existem depois que os plugins são aplicados.

2. catálogo de versões

Em um projeto com vários módulos, a mesma biblioteca aparece em vários build.gradle.kts. Se cada um escreve a versão por extenso, a chance de duas versões do Spring Boot coexistirem no mesmo classpath cresce a cada módulo novo. O catálogo de versões resolve isso com um arquivo único, gradle/libs.versions.toml, que o Gradle importa automaticamente.

O arquivo tem quatro seções principais. [versions] declara identificadores de versão que podem ser reutilizados. [libraries] associa um apelido a uma coordenada Maven, e pode apontar para uma versão declarada antes com version.ref. [bundles] agrupa vários apelidos sob um só nome. [plugins] declara plugins por identificador e versão. O apelido vira um acessor com notação de ponto: spring-boot-starter-web é acessado como libs.spring.boot.starter.web, porque cada hífen vira um nível.

A documentação do Gradle diz que os catálogos declaram versões solicitadas, mas não as impõem: “version catalogs declare requested versions but do not enforce them”. Ou seja, se duas bibliotecas do grafo pedem versões diferentes de uma terceira, o Gradle pode escolher outra versão, e uma plataforma ou um BOM também pode alterar a escolha. O catálogo centraliza o pedido, mas não garante o resultado final. Para saber a versão efetiva, o comando ./gradlew dependencies mostra o grafo resolvido.

Este é o catálogo do projeto de exemplo, com as versões publicadas no Maven Central na data da revisão:

[versions]
kotlin = "2.3.21"
spring-boot = "4.1.1"
kotest = "6.2.6"
mockk = "1.14.11"
archunit = "1.5.1"
testcontainers = "2.0.5"
resilience4j = "2.4.0"

[libraries]
spring-boot-dependencies = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" }
kotest-runner = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" }
mockk = { module = "io.mockk:mockk-jvm", version.ref = "mockk" }
archunit-junit5 = { module = "com.tngtech.archunit:archunit-junit5", version.ref = "archunit" }
testcontainers-junit = { module = "org.testcontainers:testcontainers-junit-jupiter", version.ref = "testcontainers" }
testcontainers-postgresql = { module = "org.testcontainers:testcontainers-postgresql", version.ref = "testcontainers" }
resilience4j-circuitbreaker = { module = "io.github.resilience4j:resilience4j-circuitbreaker", version.ref = "resilience4j" }
resilience4j-retry = { module = "io.github.resilience4j:resilience4j-retry", version.ref = "resilience4j" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }

Os nomes dos artefatos foram conferidos no Maven Central. Um deles merece atenção. A documentação do Spring Boot ainda mostra org.testcontainers:junit-jupiter, mas essa coordenada não existe na versão 2.0.5: a consulta ao repositório retornou 404. O artefato publicado é org.testcontainers:testcontainers-junit-jupiter, e é por ele que o catálogo acima aponta. Esse caso mostra por que o catálogo vale o trabalho: a correção muda uma linha, e não cinco módulos.

Com o catálogo pronto, um módulo usa a dependência assim:

dependencies {
    implementation(libs.resilience4j.retry)
    testImplementation(libs.kotest.runner)
}

O plugin do Spring Boot é aplicado pelo alias, e não por string, o que deixa a versão em um único lugar:

plugins {
    alias(libs.plugins.kotlin.jvm)
    alias(libs.plugins.kotlin.spring)
    alias(libs.plugins.spring.boot)
}

Vale notar uma restrição da própria documentação. Ela diz que um plugin declarado no catálogo não pode ser usado no arquivo de settings nem em settings plugins: “You cannot use a plugin declared in a version catalog in your settings file or settings plugin.” Por isso, qualquer plugin usado em settings.gradle.kts precisa ter sua versão escrita ali, fora do catálogo.

3. build multi-projeto

Um build multi-projeto tem um projeto raiz e um ou mais subprojetos. O objetivo é modularizar o código para que cada parte tenha limites claros, seja compilada uma vez e possa ser testada isoladamente. O Gradle também executa tarefas de módulos independentes em paralelo, o que ajuda quando o projeto cresce.

O arquivo settings.gradle.kts declara quais módulos participam do build. Com include, cada módulo é registrado pelo nome. O nome vira o caminho do projeto: include("services:api") aponta para o diretório services/api. O settings do projeto de exemplo fica assim:

rootProject.name = "pedidos"

pluginManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenCentral()
    }
}

include("dominio", "aplicacao", "adaptadores", "arquitetura")

O repositoriesMode com FAIL_ON_PROJECT_REPOS faz o Gradle recusar repositórios declarados dentro de um módulo. A ideia é que a origem das bibliotecas fique centralizada em um lugar, e não espalhada pelos módulos. Esse valor não foi conferido na documentação durante a revisão, então trate-o como uma escolha da aula e confirme no guia de repositórios antes de copiar.

A dependência entre módulos é declarada com project(":nome"). Em aplicacao/build.gradle.kts, a linha implementation(project(":dominio")) diz que o módulo de aplicação precisa do domínio. Dois efeitos seguem disso: o módulo requerido é compilado antes, e suas classes entram no classpath do consumidor. Ao rodar ./gradlew :adaptadores:compileKotlin, o Gradle constrói dominio e aplicacao antes de compilar adaptadores. Essa ordem vem da análise de dependências do próprio Gradle, não de uma lista escrita à mão.

O projeto de exemplo tem quatro módulos, e cada um tem uma responsabilidade:

  • dominio é Kotlin puro, sem Spring e sem nenhuma biblioteca de infraestrutura. Contém o Pedido, o Centavos e os estados.
  • aplicacao depende só do domínio. Declara as portas (interfaces) e o caso de uso RegistrarPedido.
  • adaptadores é a aplicação Spring Boot. Implementa as portas com JDBC, expõe o controller HTTP e usa Resilience4j e Micrometer.
  • arquitetura não tem código de produção. Contém apenas testes ArchUnit que leem os três módulos anteriores.

A figura abaixo mostra as setas de dependência entre esses módulos. Leia a partir do centro: o domínio é o único módulo que não depende de nenhum outro, e os demais apontam para ele.

Grafo de módulos. aplicacao → dominio (implementation(project)); adaptadores → aplicacao (implementation(project))…
figura 1 · Grafo de módulos

A seta sempre aponta para quem é necessário. aplicacao aponta para dominio, e adaptadores aponta para os dois. O módulo arquitetura aparece com linhas pontilhadas porque a dependência é só de teste: ele precisa enxergar as classes para verificá-las, mas o código de produção não depende dele. Esse desenho é o que a arquitetura hexagonal exige, e a Aula Arquitetura hexagonal com ArchUnit mostra como a regra é verificada em código.

Um cuidado com subprojects { } e allprojects { }. Esses blocos configuram vários módulos de uma vez a partir da raiz, o que parece prático, mas espalha a configuração num lugar que só se entende lendo o projeto inteiro. Neste projeto, cada módulo tem o próprio build.gradle.kts, com um bloco plugins e as dependências ao lado. Para projetos maiores, a documentação do Gradle trata de convention plugins, que encapsulam uma configuração reutilizável. Essa página não foi consultada durante a revisão, então esta aula não descreve a sintaxe.

4. api e implementation

Essa é a decisão que mais afeta a velocidade e a clareza de um projeto multi-módulo. Quando um módulo A depende de uma biblioteca B, a pergunta é se essa biblioteca faz parte do contrato de A ou se é um detalhe interno. O plugin java-library do Gradle separa as duas coisas com as configurações api e implementation.

A documentação descreve cada uma com clareza. A configuração api deve ser usada para declarar dependências exportadas pela API da biblioteca. Uma dependência declarada com implementation é interna ao componente, e não fica exposta aos consumidores, embora continue disponível em tempo de execução. A página recomenda, nas palavras dela, preferir implementation a api sempre que possível. O motivo é que os tipos internos não vazam para a API pública, e um erro de uso aparece logo na compilação do consumidor.

O motivo prático também está na documentação, e é a velocidade. Ela afirma que a separação traz compilação mais rápida, graças a um classpath de compilação menor, e que mudanças em dependências implementation provocam menos recompilações. Em um projeto com dezenas de módulos, essa diferença se acumula a cada alteração.

A figura abaixo mostra o efeito no classpath de um consumidor.

api x implementation. modulo-consumidor → modulo-biblioteca (usa); modulo-biblioteca → Biblioteca A (api); modulo-biblioteca → Biblioteca B (implementation)
figura 2 · api x implementation

O módulo que publica a API expõe a Biblioteca A com api, e por isso ela aparece no caminho de compilação do consumidor. A Biblioteca B fica com implementation, de modo que o consumidor não a enxerga ao compilar. Em tempo de execução, as duas continuam no classpath. A regra prática é simples: use api apenas para tipos que aparecem na assinatura pública do módulo, como o Pedido que uma porta devolve, e use implementation para todo o resto.

No projeto de exemplo, o módulo aplicacao usa implementation(project(":dominio")). Isso significa que o Pedido não é uma dependência transitiva exposta por aplicacao ao compilar. Quem quiser usar aplicacao precisa declarar o domínio por conta própria, o que é uma escolha consciente e não um acidente.

5. o BOM do Spring Boot e as versões dos starters

Um starter do Spring Boot é declarado sem versão, porque a versão vem do BOM (bill of materials), um módulo que lista as versões compatíveis de centenas de bibliotecas. A documentação do plugin de gerenciamento de dependências diz que, quando ele é aplicado, importa automaticamente o spring-boot-dependencies da versão do Spring Boot em uso, e que o plugin do Spring Boot depende desse gerenciamento. Com isso, a declaração implementation("org.springframework.boot:spring-boot-starter-webmvc") deveria receber a versão do BOM sem nenhuma linha extra.

Quando a importação não acontece, a documentação oferece o caminho explícito, que é tratar o BOM como uma plataforma. A diferença entre platform e enforcedPlatform está no peso que o Gradle dá às versões do BOM: platform as trata como recomendações, e enforcedPlatform as trata como exigências que sobrescrevem as outras. A forma usada nesta aula é:

dependencies {
    implementation(platform(libs.spring.boot.dependencies))
    testImplementation(platform(libs.spring.boot.dependencies))
    implementation("org.springframework.boot:spring-boot-starter-webmvc")
}

O platform faz o Gradle usar o BOM só como fonte de versões, sem adicioná-lo como dependência real. A plataforma restringe apenas a configuração em que foi declarada e as que estendem dela. Por isso o BOM aparece também em testImplementation.

Há uma mudança de nomes que vale conhecer antes de copiar exemplos antigos. No Spring Boot 4, o starter web passou a se chamar spring-boot-starter-webmvc. A compilação do projeto de verificação, com Maven, funcionou com esse nome e com o BOM 4.1.1. O post oficial sobre a modularização, que descreve a mudança de nomes, não foi reconferido nesta revisão.

Um requisito de Kotlin também aparece na documentação do Spring Boot: ela diz que o framework precisa de no mínimo Kotlin 2.2.x e que kotlin-stdlib e kotlin-reflect precisam estar no classpath. O módulo adaptadores usa Kotlin 2.3.21, o que atende ao mínimo e é a versão gerenciada pelo BOM 4.1.1.

Se um starter for declarado sem versão e o BOM não estiver importado, a versão fica vazia e o Gradle não consegue resolver o artefato. Esse erro não foi reproduzido nesta revisão, porque não há projeto Gradle na máquina. O que a documentação confirma é o mecanismo do BOM e o caminho da plataforma, e é isso que a aula ensina.

6. erros comuns e boas práticas

O primeiro erro é repetir a versão em cada módulo. Ela parece inofensiva no começo, mas cada cópia é uma chance de divergência. A prática recomendada é que a versão viva em um lugar só, o catálogo, e que os módulos leiam dali.

O segundo erro é tratar implementation e api como equivalentes. Quando um tipo interno acaba na assinatura pública, o consumidor passa a depender dele sem saber, e uma mudança interna quebra módulos que nem sabiam da dependência. A boa prática é começar sempre com implementation e subir para api só quando o tipo aparece de fato na API.

O terceiro erro é colocar regra de negócio no mesmo módulo que o framework. Um módulo dominio que importa Spring não tem mais nada que o Gradle possa proteger, porque a fronteira entre os módulos ficou só no nome. A Aula Arquitetura hexagonal com ArchUnit mostra como o ArchUnit cobre esse caso dentro do mesmo módulo.

O quarto erro é declarar um repositório dentro de um módulo. Com FAIL_ON_PROJECT_REPOS, o build falha, o que parece irritante até o dia em que um módulo passa a baixar uma biblioteca de um servidor que ninguém controla.

O quinto erro é achar que o catálogo garante a versão final. Como a documentação deixa claro, ele pede uma versão, e o grafo pode resolver outra. A boa prática é conferir o resultado com ./gradlew dependencies depois de cada mudança de versão.

7. exercícios

  1. Crie um quinto módulo relatorios que depende de aplicacao com implementation. Compile e confirme pela saída do Gradle que dominio é construído antes de aplicacao, e que aplicacao é construído antes de relatorios.
  2. Troque a dependência implementation(project(":dominio")) de aplicacao por api(project(":dominio")). Sem alterar outro arquivo, crie em relatorios um uso direto do Pedido e verifique se a compilação continua. Depois volte para implementation e explique por que a compilação de relatorios falharia.
  3. Remova a linha implementation(platform(...)) do módulo adaptadores e rode ./gradlew :adaptadores:dependencies. Verifique se a versão do spring-boot-starter-webmvc continua aparecendo. Depois explique o resultado com base na seção 5.
  4. Mude a versão do Kotlin no catálogo para 2.1.21 e rode a compilação do adaptadores. Leia a mensagem e relacione com o requisito de Kotlin do Spring Boot.

8. glossário

Gradle: ferramenta de build que executa tarefas como compilar, testar e empacotar, descritas em um script.

Kotlin DSL: forma de escrever o script do Gradle em Kotlin, em arquivos .gradle.kts, com acessores tipados.

Catálogo de versões: arquivo gradle/libs.versions.toml que declara versões, bibliotecas, bundles e plugins em um lugar só.

Build multi-projeto: build com um projeto raiz e vários subprojetos, declarados no settings.gradle.kts.

BOM (bill of materials): módulo que lista versões compatíveis de um conjunto de bibliotecas, usado para preencher versões sem repeti-las.

Plataforma (platform): forma de usar um BOM no Gradle como fonte de versões, sem adicioná-lo como dependência.

Configuração api: dependência que faz parte da API da biblioteca e entra no classpath de compilação dos consumidores.

Configuração implementation: dependência interna, invisível na compilação dos consumidores e visível em tempo de execução.

fontes

Consultadas em 20/10/2026.

Não verificado nesta revisão: a página de repositórios do Gradle (que sustenta FAIL_ON_PROJECT_REPOS), a página de convention plugins, o post oficial de modularização do Spring Boot 4 e qualquer execução com Gradle, porque o Gradle não está instalado nesta máquina. O nome spring-boot-starter-webmvc foi confirmado pela compilação com Maven, não pela leitura do post.

Executado com Maven, em projeto de verificação com Spring Boot 4.1.1, Spring Framework 7.0.9 e Kotlin 2.3.21, JDK 21 (Temurin 21): a compilação e os testes passaram com as coordenadas do catálogo. A saída do Gradle não aparece nesta aula.

relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?