Durante muito tempo, a conversa sobre arquitetura backend parecia presa entre dois extremos: o monólito bagunçado e os microsserviços complexos demais. Em 2026, uma abordagem voltou com força para o centro do debate técnico: o monólito modular. E quando combinamos isso com Kotlin + Spring, o resultado pode ser uma base de código mais organizada, testável e simples de operar.
Esse tema ganhou relevância recente com discussões no ecossistema JetBrains e Spring, especialmente em conteúdos sobre Spring Modulith e modelagem por domínio. Para equipes que usam Kotlin no backend, o assunto é valioso porque oferece um caminho intermediário muito prático: manter um único deploy, mas com fronteiras internas claras.
Se você já acompanha nossos conteúdos sobre Kotlin server-side em 2026, Kotlin com Spring Boot e o guia de backend com Spring, este artigo aprofunda o próximo passo arquitetural.
O que é um monólito modular?
Um monólito modular é uma aplicação implantada como uma única unidade, mas organizada em módulos que representam domínios ou capacidades de negócio. Em vez de colocar tudo em uma estrutura genérica de controller, service e repository soltos pelo projeto inteiro, você separa responsabilidades por contexto.
Em um sistema de e-commerce, por exemplo, seria natural ter módulos como:
produtopedidopagamentoestoquecliente
Cada módulo tem sua própria lógica, contratos internos, eventos e acesso a dados. O deploy continua simples, mas o acoplamento tende a cair muito.
Por que esse tema voltou a crescer em 2026?
Existem três razões principais.
1. Cansaço com complexidade operacional
Muitas equipes perceberam que adotar microsserviços cedo demais gera overhead de deploy, observabilidade, segurança, tracing distribuído e governança.
2. Busca por domínio mais explícito
Times maduros querem refletir melhor o negócio na estrutura do código. Monólitos modulares ajudam nisso.
3. Ferramentas melhores
Com o Spring Modulith, ficou mais fácil declarar limites entre módulos, testar arquitetura e detectar violações de dependência.
Ou seja: a ideia não é “voltar ao passado”, mas sim construir sistemas mais sustentáveis com ferramentas atuais.
Estrutura de módulos em Kotlin
Vamos imaginar uma aplicação simples de pedidos. Em vez de começar pela clássica divisão horizontal, podemos estruturar o projeto assim:
src/main/kotlin/br/dev/kotlinbrasil/app
├── pedido
│ ├── PedidoController.kt
│ ├── PedidoService.kt
│ ├── PedidoRepository.kt
│ └── package-info.java
├── produto
│ ├── ProdutoController.kt
│ ├── ProdutoService.kt
│ ├── ProdutoRepository.kt
│ └── package-info.java
├── pagamento
│ ├── PagamentoService.kt
│ ├── PagamentoGateway.kt
│ └── package-info.java
└── Application.kt
Essa organização conversa bem com Kotlin porque a linguagem favorece código mais coeso, conciso e expressivo. Se você está revisando fundamentos importantes, vale retomar nosso guia completo de Kotlin e também o conteúdo sobre design patterns em Kotlin.
Configurando Spring Modulith com Kotlin
Um setup simplificado pode começar assim:
plugins {
kotlin("jvm") version "2.3.20"
kotlin("plugin.spring") version "2.3.20"
id("org.springframework.boot") version "3.4.4"
id("io.spring.dependency-management") version "1.1.7"
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.modulith:spring-modulith-starter-core")
testImplementation("org.springframework.modulith:spring-modulith-starter-test")
}
A ideia é usar o Spring Boot normalmente, mas adicionar a camada do Modulith para declarar e validar fronteiras entre módulos.
Declarando o aplicativo como modulith
Na classe principal, a aplicação pode ficar assim:
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.modulith.Modulithic
@Modulithic
@SpringBootApplication
class KotlinBrasilApplication
fun main(args: Array<String>) {
runApplication<KotlinBrasilApplication>(*args)
}
Essa anotação indica que a aplicação deve ser tratada como um conjunto de módulos bem definidos, com suporte a documentação e verificação arquitetural.
Declarando dependências permitidas entre módulos
Um dos recursos mais úteis do Spring Modulith é permitir declarar explicitamente quais módulos podem depender de quais outros.
No package-info.java do módulo pedido, por exemplo:
@org.springframework.modulith.ApplicationModule(
allowedDependencies = {"produto", "pagamento"}
)
package br.dev.kotlinbrasil.app.pedido;
Com isso, o módulo pedido pode conversar com produto e pagamento, mas não deveria acessar diretamente cliente, por exemplo, se isso não fizer sentido arquitetural.
Esse tipo de restrição ajuda muito a evitar o crescimento silencioso de acoplamento. Em projetos Kotlin que duram anos, esse cuidado faz diferença real.
Exemplo prático: módulo de produto
Vamos definir um módulo de produto com uma API mínima.
package br.dev.kotlinbrasil.app.produto
data class Produto(
val id: Long,
val nome: String,
val preco: Double,
val estoqueDisponivel: Int,
)
interface ProdutoRepository {
fun buscarPorId(id: Long): Produto?
}
class ProdutoService(
private val produtoRepository: ProdutoRepository,
) {
fun validarDisponibilidade(produtoId: Long, quantidade: Int): Boolean {
val produto = produtoRepository.buscarPorId(produtoId)
?: throw IllegalArgumentException("Produto não encontrado")
return produto.estoqueDisponivel >= quantidade
}
}
Perceba que o objetivo não é apenas ter classes separadas, mas encapsular a lógica de um domínio específico.
Exemplo prático: módulo de pedido
Agora, o módulo de pedido pode depender de produto sem conhecer detalhes internos desnecessários.
package br.dev.kotlinbrasil.app.pedido
import br.dev.kotlinbrasil.app.produto.ProdutoService
data class CriarPedidoCommand(
val produtoId: Long,
val quantidade: Int,
val clienteId: String,
)
data class Pedido(
val id: Long,
val produtoId: Long,
val quantidade: Int,
val clienteId: String,
val status: String,
)
class PedidoService(
private val produtoService: ProdutoService,
private val pedidoRepository: PedidoRepository,
) {
fun criar(command: CriarPedidoCommand): Pedido {
require(produtoService.validarDisponibilidade(command.produtoId, command.quantidade)) {
"Produto sem estoque disponível"
}
val pedido = Pedido(
id = System.currentTimeMillis(),
produtoId = command.produtoId,
quantidade = command.quantidade,
clienteId = command.clienteId,
status = "CRIADO",
)
return pedidoRepository.salvar(pedido)
}
}
interface PedidoRepository {
fun salvar(pedido: Pedido): Pedido
}
Essa interação parece simples, mas já mostra uma vantagem importante: o fluxo respeita domínio e responsabilidade.
Quando usar eventos em vez de chamada direta?
Nem toda integração entre módulos precisa ser síncrona. Em muitos casos, eventos de aplicação deixam o sistema mais desacoplado.
Por exemplo: depois de criar um pedido, pode ser interessante disparar um evento para o módulo de pagamento iniciar o processamento.
package br.dev.kotlinbrasil.app.pedido
import org.springframework.context.ApplicationEventPublisher
class PedidoService(
private val produtoService: ProdutoService,
private val pedidoRepository: PedidoRepository,
private val eventPublisher: ApplicationEventPublisher,
) {
fun criar(command: CriarPedidoCommand): Pedido {
require(produtoService.validarDisponibilidade(command.produtoId, command.quantidade)) {
"Produto sem estoque disponível"
}
val pedido = pedidoRepository.salvar(
Pedido(
id = System.currentTimeMillis(),
produtoId = command.produtoId,
quantidade = command.quantidade,
clienteId = command.clienteId,
status = "CRIADO",
)
)
eventPublisher.publishEvent(PedidoCriadoEvent(pedido.id, pedido.clienteId))
return pedido
}
}
data class PedidoCriadoEvent(
val pedidoId: Long,
val clienteId: String,
)
No módulo de pagamento:
package br.dev.kotlinbrasil.app.pagamento
import br.dev.kotlinbrasil.app.pedido.PedidoCriadoEvent
import org.springframework.modulith.events.ApplicationModuleListener
class PagamentoListener {
@ApplicationModuleListener
fun aoCriarPedido(event: PedidoCriadoEvent) {
println("Iniciando pagamento do pedido ${event.pedidoId} para cliente ${event.clienteId}")
}
}
Esse padrão é especialmente útil quando você quer diminuir acoplamento temporal entre módulos.
Como o Spring Modulith ajuda nos testes?
Aqui entra uma das partes mais valiosas da abordagem. Não basta organizar módulos visualmente; é preciso validar se a arquitetura está sendo respeitada.
Um teste simples pode verificar a estrutura do modulith:
import org.junit.jupiter.api.Test
import org.springframework.modulith.core.ApplicationModules
class ArquiteturaModularTest {
@Test
fun `deve validar a estrutura modular da aplicacao`() {
ApplicationModules.of(KotlinBrasilApplication::class.java)
.verify()
}
}
Esse tipo de teste ajuda a detectar violações logo cedo. Se um módulo começar a acessar outro sem permissão, a suíte acusa.
Também é possível testar módulos de forma mais isolada, o que melhora muito a confiança do time ao evoluir o sistema.
Se seu foco é qualidade de backend, vale revisar nosso guia de testes em Kotlin e o conteúdo sobre JUnit 5 e MockK.
Monólito modular não é apenas organização de pastas
Esse é um erro comum. Muita gente acha que basta agrupar arquivos por pacote e pronto. Não é assim.
Um monólito modular de verdade exige:
- fronteiras explícitas;
- dependências controladas;
- contratos claros entre módulos;
- testes arquiteturais;
- eventos ou APIs internas bem definidas;
- disciplina para evitar atalhos.
Sem isso, você acaba apenas com um monólito com pastas mais bonitas.
Quais são as principais vantagens?
Simplicidade operacional
Você continua com um único deploy, um pipeline principal e menos overhead de infraestrutura.
Melhor modelagem de domínio
Os módulos refletem áreas do negócio, o que ajuda tanto a engenharia quanto a comunicação com produto.
Testabilidade
Com limites melhores, fica mais fácil testar partes específicas da aplicação.
Evolução gradual
Se um dia fizer sentido extrair um módulo para microsserviço, o caminho tende a ser menos traumático.
E as limitações?
É importante não romantizar.
Monólito modular ainda é monólito. Isso significa que:
- o deploy continua único;
- a escalabilidade mais fina por serviço não existe da mesma forma;
- falhas graves ainda podem impactar a aplicação toda;
- disciplina arquitetural continua sendo obrigatória.
Ou seja: essa abordagem não substitui todos os casos de microsserviços. Mas para muitas equipes, ela resolve melhor o problema real.
Observabilidade também importa aqui
Uma parte interessante do conteúdo recente sobre monólitos modulares é a ligação com observabilidade. Mesmo com deploy único, vale monitorar interações entre módulos, eventos internos e fluxos críticos.
Você pode combinar essa abordagem com práticas já conhecidas no ecossistema Kotlin, como:
- logs estruturados;
- métricas por domínio;
- tracing com OpenTelemetry;
- documentação de interação entre módulos.
Nesse ponto, nosso artigo sobre observabilidade em Kotlin complementa bem a discussão.
Quando faz sentido adotar monólito modular em Kotlin?
Essa abordagem costuma ser excelente para:
- produtos em crescimento que ainda não precisam de microsserviços;
- times pequenos ou médios;
- plataformas internas;
- SaaS B2B em fase de consolidação;
- sistemas que precisam de mais organização sem explosão operacional.
Ela também pode ser uma ótima etapa intermediária para projetos legados que hoje sofrem com acoplamento excessivo.
Conclusão
O monólito modular em Kotlin com Spring é uma das pautas mais úteis de 2026 para times backend porque responde a um problema muito concreto: como crescer sem afundar em complexidade cedo demais. Com Spring Modulith, fica mais fácil declarar módulos, verificar dependências, testar arquitetura e criar uma base mais sustentável para evoluir o produto.
Para equipes que já usam Kotlin com Spring Boot, estudam arquitetura limpa ou querem fortalecer a estrutura do backend sem partir direto para microsserviços, essa é uma abordagem que vale muito acompanhar.
Se quiser continuar estudando, veja também:
Para quem trabalha com backend poliglota, vale observar como Go aborda modularização com packages e internal — uma filosofia diferente, mas com o mesmo objetivo de manter fronteiras claras entre componentes.
O mais importante é lembrar: arquitetura boa não é a mais complexa, e sim a que permite evoluir com clareza, segurança e ritmo sustentável.