Build lento é um imposto diário em times Kotlin. Cada ./gradlew test, cada pipeline de pull request e cada geração de APK ou artefato backend consome minutos que se repetem centenas de vezes por semana. Em projetos pequenos, isso parece só irritante. Em monorepos Kotlin com módulos Android, bibliotecas compartilhadas, Kotlin Multiplatform, Ktor, Spring Boot e ferramentas de qualidade, build lento vira gargalo de produto.
A boa notícia é que o ecossistema Gradle amadureceu muito. Em 2026, um projeto Kotlin bem configurado pode combinar Gradle build cache, configuration cache, convention plugins, paralelismo e fronteiras modulares claras para reduzir tempo de feedback sem transformar o build em uma caixa-preta frágil. O ganho não vem de uma única flag milagrosa. Ele vem de disciplina: tarefas determinísticas, inputs declarados, outputs cacheáveis e CI desenhado para reaproveitar trabalho com segurança.
Este guia mostra uma estratégia prática para acelerar builds Kotlin em monorepos. Se você está organizando a base do projeto, leia também o guia de Kotlin com Gradle, o tutorial de Gradle com Kotlin DSL e o artigo sobre CI/CD para Kotlin.
O que o build cache realmente faz
O Gradle build cache reaproveita o resultado de tarefas quando os inputs são equivalentes. Se uma tarefa de compilação, teste, lint ou empacotamento já rodou com os mesmos arquivos, parâmetros e ambiente relevante, o Gradle pode baixar ou reutilizar o output em vez de executar tudo de novo.
Isso é diferente de simplesmente “não rodar porque nada mudou”. O estado UP-TO-DATE vale dentro do mesmo workspace. O build cache permite reaproveitamento entre workspaces, branches, máquinas de dev e jobs de CI. Em um monorepo, isso é enorme: um pull request que muda apenas um módulo de feature não deveria recompilar tudo que já foi validado em outro job.
A regra central é determinismo. Uma tarefa cacheável precisa produzir o mesmo output para os mesmos inputs. Se ela lê hora atual, caminho absoluto local, variável de ambiente não declarada ou arquivo fora do conjunto de inputs, o cache fica perigoso. Por isso, acelerar build não é só “ligar cache”; é remover comportamento implícito.
Configuração mínima no settings.gradle.kts
O ponto de partida costuma ficar em settings.gradle.kts:
buildCache {
local {
isEnabled = true
directory = File(rootDir, ".gradle/build-cache")
removeUnusedEntriesAfterDays = 7
}
remote<HttpBuildCache> {
url = uri("https://gradle-cache.example.com/cache/")
isEnabled = !System.getenv("CI").isNullOrBlank()
isPush = System.getenv("CI") == "true" &&
System.getenv("GIT_BRANCH") == "main"
}
}
Em produção, evite copiar esse exemplo sem pensar. O cache remoto deve ter autenticação, isolamento por organização e política clara de push. Um padrão seguro é permitir que branches de pull request leiam do cache, mas só a branch principal publique resultados. Assim você reduz o risco de poluir cache compartilhado com uma branch experimental.
Para projetos open source ou times pequenos, apenas o cache local já ajuda. Para empresas com CI pesado, o cache remoto costuma pagar o esforço rapidamente.
Configuration cache: acelere a fase esquecida
Muita gente mede apenas tempo de compilação, mas builds Gradle também gastam tempo configurando projetos, plugins e tarefas. Em monorepos com dezenas ou centenas de módulos, a configuração pode dominar o tempo de comandos pequenos.
O configuration cache guarda o resultado da fase de configuração para comandos equivalentes. Ative em gradle.properties:
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
kotlin.incremental=true
No começo, use problems=warn para mapear plugins e scripts incompatíveis. Depois de estabilizar, trate warnings recorrentes como dívida técnica. Scripts que leem ambiente diretamente, chamam processos externos durante configuração ou acessam APIs internas do Gradle costumam quebrar o cache.
A prática saudável é mover lógica imperativa para tarefas declaradas e convention plugins. O arquivo de build deve declarar o que o módulo é, não executar descoberta complexa toda vez que o Gradle abre.
Convention plugins deixam o monorepo previsível
Monorepo Kotlin sofre quando cada módulo copia blocos de Gradle com pequenas variações. Um módulo Android usa uma versão de JVM target, outro esquece explicitApi, outro configura testes de forma diferente. Além de confundir pessoas, isso atrapalha cache porque tarefas parecidas deixam de ter inputs parecidos.
Convention plugins resolvem esse problema. Em vez de repetir configuração, você cria plugins internos como kotlin-brasil.android-library, kotlin-brasil.jvm-library ou kotlin-brasil.kmp-library dentro de build-logic.
// build-logic/src/main/kotlin/KotlinJvmConventionPlugin.kt
class KotlinJvmConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("org.jetbrains.kotlin.jvm")
extensions.configure<KotlinJvmProjectExtension> {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
allWarningsAsErrors.set(false)
}
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
maxParallelForks = Runtime.getRuntime().availableProcessors().coerceAtMost(4)
}
}
}
A partir daí, cada módulo fica pequeno:
plugins {
id("kotlin-brasil.jvm-library")
}
dependencies {
implementation(project(":shared:domain"))
testImplementation(libs.junit.jupiter)
}
Esse padrão conversa bem com Clean Architecture em Kotlin porque deixa fronteiras explícitas: domínio, aplicação, infraestrutura, adapters HTTP, apps Android e módulos KMP podem ter convenções próprias sem duplicação.
Modularização que ajuda o cache
Cache não salva uma arquitetura com dependências emaranhadas. Se todo módulo depende de :app, qualquer mudança no app invalida muita coisa. Se modelos de domínio ficam misturados com DTOs de API externa, mudanças pequenas atravessam o grafo inteiro.
Um monorepo Kotlin mais rápido costuma seguir algumas regras:
- módulos de domínio não dependem de frameworks;
- módulos de infraestrutura dependem do domínio, não o contrário;
- features Android dependem de contratos pequenos, não de um módulo gigante comum;
- código gerado fica isolado para não invalidar módulos que não precisam dele;
- testes pesados ficam separados de testes unitários rápidos;
- APIs compartilhadas mudam com cuidado, porque invalidam consumidores.
Antes de mexer em cache, rode ./gradlew projects e ./gradlew :module:dependencies nos módulos críticos. O objetivo é entender o grafo. Builds rápidos são consequência de dependências previsíveis.
CI: leia cache em PR, publique no main
Uma configuração de CI comum para monorepos Kotlin é separar validação por camadas. Um job roda checks rápidos, outro roda testes Android, outro gera artefatos, outro faz análise estática. Todos leem cache remoto. Apenas jobs confiáveis no main publicam.
Um fluxo prático:
- pull request abre e restaura cache remoto;
- CI roda
./gradlew affectedCheckou uma seleção equivalente de tarefas; - jobs de PR nunca fazem
pushpara o cache remoto; - merge no
mainroda validação completa; - se passar, publica outputs cacheáveis para acelerar próximos PRs.
Se o time ainda não tem detecção de módulos afetados, comece simples com ./gradlew check. Depois evolua para seleção por paths alterados, dependências reversas ou ferramentas como Gradle Enterprise/Develocity quando fizer sentido financeiro. Não transforme a otimização em risco: pular teste errado custa mais que alguns minutos de CI.
Cuidado com segredos e ambiente
Um erro perigoso é deixar tarefas cacheáveis dependerem de segredos, tokens ou arquivos locais. Build cache não deve guardar outputs que embutem credenciais, URLs privadas sensíveis ou configurações de produção. Variáveis como API_TOKEN, keystore Android e credenciais de publicação devem ficar em tarefas não cacheáveis ou declaradas com cuidado.
Para Android, atenção especial a signing configs, google-services.json, flavors e geração de recursos. Para backend, cuidado com arquivos .env, migrações geradas e clientes OpenAPI criados a partir de endpoints instáveis. Sempre pergunte: “se esse output for reutilizado em outra máquina, isso é correto e seguro?”
Também vale padronizar versões de JDK, Android Gradle Plugin, Kotlin e Gradle Wrapper. Cache compartilhado entre ambientes diferentes perde eficiência e pode criar bugs difíceis. Use toolchains quando possível:
kotlin {
jvmToolchain(21)
}
Como medir antes e depois
Sem métrica, otimização vira superstição. Antes de mudar tudo, registre tempos para comandos representativos:
./gradlew clean
./gradlew :app:assembleDebug --scan
./gradlew check --scan
./gradlew test --scan
Depois, rode os mesmos comandos em três cenários: workspace limpo, segunda execução local e CI com cache remoto aquecido. O número mais importante para devs costuma ser feedback incremental; o número mais importante para a empresa costuma ser tempo total de PR até merge.
Mesmo sem build scan pago, você pode usar --profile para gerar relatório HTML local:
./gradlew check --profile
Procure tarefas não incrementais, módulos que sempre recompilam, testes lentos e tempo excessivo de configuração. Corrija os maiores gargalos primeiro.
Checklist de adoção segura
Uma implantação gradual reduz risco:
- ativar
org.gradle.caching=truelocalmente; - corrigir tarefas customizadas que não declaram inputs e outputs;
- habilitar configuration cache em modo warning;
- extrair configuração repetida para convention plugins;
- padronizar JDK via toolchains;
- isolar tarefas com segredos como não cacheáveis;
- configurar cache remoto somente leitura em PR;
- permitir push apenas em CI confiável no
main; - medir tempo de build antes e depois;
- documentar como limpar cache quando houver suspeita.
Se algo estranho acontecer, desative por escopo em vez de abandonar tudo. Uma tarefa ruim não invalida a estratégia inteira.
Conclusão
Gradle build cache em projetos Kotlin não é detalhe de infraestrutura para “quando sobrar tempo”. Ele muda a velocidade de aprendizado do time. Em monorepos com Android, backend e KMP, cada minuto economizado no ciclo de feedback reduz fila de revisão, aumenta confiança para refatorar e libera energia para produto.
O caminho mais seguro é incremental: entenda o grafo, padronize convenções, ative cache local, estabilize configuration cache, depois leve o cache remoto para CI com política conservadora. Kotlin e Gradle dão ferramentas suficientes para builds rápidos, mas a arquitetura precisa colaborar. Quando módulos têm fronteiras claras e tarefas são determinísticas, o cache deixa de ser aposta e vira multiplicador diário.
Para continuar, aprofunde em Kotlin com GitHub Actions, Detekt e ktlint em Kotlin e Monólito Modular em Kotlin. Se o seu time também trabalha com Go, o guia de rate limiting em Go ajuda a comparar decisões de CI, performance e confiabilidade entre stacks backend.