← início

Refatoração Layered Architecture para Clean Architecture

Refatorando meu projeto para Clean Architecture: o que eu aprendi no caminho

Quando comecei esse projeto, ele estava funcionando. Isso é importante dizer para não associarmos refatoração com “código ruim”. Às vezes o código está ok para o momento em que foi desenvolvido, mas conforme fui aprendendo mais comecei a enxergar uma boa oportunidade para refatorar este projeto antigo que seguia uma arquitetura em camadas tradicional, comum em aplicações em Spring Boot:

Controller -> Service -> Repository -> Entity

Para uma Clean Architecture (não purista) + TDD (Test Driven Development). Precisava? Não é uma aplicação realmente muito pequena e feita somente para estudos nada pensando em uma aplicação profissional para uso em ambiente de produção.

A arquitetura por camadas no começo, resolve muita coisa. É simples, direta e fácil de entender. Mas, conforme fui olhando para o projeto com mais calma, percebi que algumas responsabilidades estavam misturadas demais.

As entidades eram ao mesmo tempo entidades de banco e modelos de negócio. Os services conheciam repositories diretamente. Os controllers chamavam services que carregavam detalhes de persistência. E no momento eu não tinha teste nenhum nesta aplicação.

🔗 Repositório do projeto — GameSearch

Como o projeto estava antes

Antes da refatoração, a estrutura era parecida com isso:

dev.java10x.gamesearch
├── config
├── controllers
├── entities
├── mapper
├── repositories
└── services

Era uma arquitetura em camadas clássica.

O fluxo era basicamente:

Controller -> Service -> Repository -> Entity JPA

Não está errado. Na verdade, para um projeto pequeno igual ao Game Search, esse modelo funciona muito bem. Mas levando em consideração o que ando estudando sobre Clean Architecture, primeiro problema que notei foi quando a regra de negócio passa a depender demais dos detalhes externos.

No meu caso:

Um exemplo claro era a entidade Game.

Ela representava um jogo no sistema, mas também era uma entidade JPA anotada com @Entity, @Table, @ManyToMany e outras coisas de persistência.

Ou seja: meu modelo de negócio sabia demais sobre banco de dados.

A decisão: Clean Architecture, mas sem exagero

Quando pensei em praticar a Clean Architecture, tentei não cair em dois extremos.

O primeiro seria continuar do jeito que está, só arrumando um bug aqui e outro ali.

O segundo extremo seria criar uma arquitetura pesada demais, com interface para tudo, use case minúsculo para qualquer método e uma quantidade de arquivos que deixaria o projeto mais difícil de navegar.

Queria um meio-termo.

A nova estrutura ficou assim:

dev.java10x.gamesearch
├── domain
│   ├── model
│   └── exception
├── application
│   ├── gateway
│   └── usecase
├── infrastructure
│   ├── persistence
│   │   ├── entity
│   │   ├── repository
│   │   ├── mapper
│   │   └── gateway
│   └── security
├── interfaceadapter
│   └── rest
│       ├── controller
│       ├── docs
│       ├── dto
│       └── mapper
└── config

O fluxo passou a ser:

Controller -> UseCase -> Gateway -> Infrastructure -> JpaRepository

E a regra principal virou:

O centro da aplicação não deve depender dos detalhes externos.

Na prática:

Começando pelo mais simples

Uma coisa que me ajudou muito foi não começar pelo recurso mais complexo.

Eu poderia ter começado por Game, porque era o coração da aplicação. Mas isso teria sido mais arriscado, já que Game se relaciona com categorias e plataformas.

Então comecei por Category/Platform.

Por quê?

Porque categoria e plataforma eram simples:

Esse foi um aprendizado importante:

Quando for refatorar arquitetura, comece por uma parte pequena e controlada.

Se o padrão ficar ruim em algo simples, ele vai ficar muito pior em algo complexo.

TDD fez a arquitetura ficar mais clara

Outro ponto importante: eu decidi seguir com TDD nos use cases.

Antes de criar controller novo, mapper REST ou entidade JPA nova, eu criava o teste do caso de uso.

Por exemplo, para CategoryUseCase, os testes cobriam:

E para testar isso, eu não precisava subir Spring.

Eu usava um gateway fake em memória:

CategoryUseCase
  -> CategoryGateway
    -> InMemoryCategoryGateway

Isso deixou uma coisa muito clara: o use case não precisava saber se os dados vinham de PostgreSQL, H2, arquivo, cache ou qualquer outra coisa.

Ele só precisava de um contrato.

Esse foi um dos momentos em que a Clean Architecture começou a fazer sentido de verdade para mim.

Não era só uma organização bonita de pacotes. Era uma forma de conseguir testar a regra de negócio sem carregar a aplicação inteira.

Separando domínio de persistência

Uma das maiores mudanças foi separar os modelos de domínio das entidades JPA.

Antes, eu tinha algo como:

entities/Game.java

Depois, passei a ter:

domain/model/Game.java
infrastructure/persistence/entity/GameEntity.java

O Game de domínio representa o conceito da aplicação.

O GameEntity representa como esse conceito é salvo no banco.

Entre os dois, existe um mapper:

GamePersistenceMapper

Esse tipo de separação pode parecer repetição no começo. E, sinceramente, em um projeto muito pequeno igual este talvez pareça mesmo. Mas ela traz um ganho grande: o domínio deixa de ser refém do banco.

Se algum dia eu precisar mudar detalhes da persistência, a regra de negócio não precisa sofrer junto.

Gateways: o contrato entre aplicação e mundo externo

Na camada application, criei interfaces chamadas gateways.

Por exemplo:

GameGateway
CategoryGateway
PlatformGateway
UserGateway
TokenGateway
PasswordEncoderGateway

Esses gateways são contratos.

O use case diz:

Eu preciso salvar um jogo.

Mas ele não diz:

Eu preciso usar Spring Data JPA para salvar um jogo em uma tabela PostgreSQL.

Essa diferença é pequena no código, mas grande na arquitetura.

A implementação concreta fica na infraestrutura:

GameDatabaseGateway
CategoryDatabaseGateway
PlatformDatabaseGateway
UserDatabaseGateway

Esses adapters sabem falar com repository, entity JPA e mapper.

O use case não.

Migrando Game com mais cuidado

Depois de validar Category e Platform, fui para Game.

Esse era o recurso mais delicado, porque um jogo tem categorias e plataformas.

Foi aqui que os testes ajudaram bastante.

No código antigo, quando eu criava ou atualizava um jogo com uma categoria inexistente, essa categoria podia simplesmente ser ignorada.

Na nova versão, isso virou regra explícita:

Também corrigi um problema que existia no update de jogos: alguns campos como genre, developer e publisher não eram atualizados corretamente.

Com testes no GameUseCase, ficou mais fácil garantir que esse bug não voltaria.

Auth e segurança: onde ser pragmático importa

A parte de autenticação foi a que mais exigiu pragmatismo.

Spring Security naturalmente traz detalhes de framework, então a ideia não foi fingir que isso não existe. A ideia foi isolar esses detalhes no lugar certo.

Antes, o usuário antigo implementava UserDetails.

Depois, o domínio passou a ter um User limpo:

domain/model/User.java

E a persistência passou a ter:

infrastructure/persistence/entity/UserEntity.java

Para senha, criei contratos:

PasswordEncoderGateway
PasswordMatcherGateway

E a implementação real ficou em:

BCryptPasswordGateway

Para JWT, a aplicação depende de:

TokenGateway
TokenVerifierGateway

E a implementação real ficou em:

JwtTokenGateway

Assim, o use case de login não sabe como o token é gerado. Ele só sabe que precisa gerar um token.

O papel dos controllers agora

Depois da refatoração, os controllers ficaram mais simples.

Eles recebem HTTP, validam DTOs, chamam use cases e retornam responses.

Só isso.

Nada de regra de negócio no controller.

Nada de repository no controller.

Nada de entity JPA no controller.

A camada REST ficou em:

interfaceadapter/rest

Com:

Também separei as anotações de Swagger em interfaces de documentação. Isso deixou os controllers mais limpos e manteve a documentação na borda REST, que é em tese onde ela pertence.

Testes: o que mudou

Antes, o teste principal era basicamente o contextLoads.

Depois da refatoração, os use cases ganharam testes reais.

Agora existem testes para:

O mais importante é que esses testes não precisam subir Spring.

Eles testam regra de aplicação direto.

Para o contexto Spring, configurei um profile de teste usando H2:

src/test/resources/application-test.yml

Isso evita depender de PostgreSQL real para rodar a suíte de testes.

Melhorias finas que também fizeram diferença

Além da arquitetura em si, alguns ajustes deixaram o projeto mais redondo:

Essas coisas parecem pequenas, mas fazem diferença quando alguém clona o projeto, sobe a aplicação e tenta entender como tudo funciona.

Como eu penso em Clean Architecture depois dessa refatoração

Depois desse processo, minha visão ficou mais prática.

Clean Architecture não é sobre criar pastas bonitas.

Também não é sobre colocar interface em tudo.

Para mim, o ponto principal é:

A regra de negócio deve ser fácil de entender, fácil de testar e pouco dependente de detalhes externos.

Se o use case depende diretamente de JPA, HTTP, JWT ou framework, talvez ele esteja sabendo demais.

Mas, ao mesmo tempo, não adianta criar abstração sem necessidade.

O equilíbrio que tentei seguir foi:

Essa última parte é importante.

Clean Architecture pragmática não é arquitetura preguiçosa. É arquitetura com intenção.

O que eu faria diferente?

Eu provavelmente começaria com testes mais cedo.

No começo do projeto, eu estava mais focado em fazer a API funcionar. Isso é normal. Mas, depois que comecei a refatorar com TDD, percebi que os testes me ajudavam a decidir melhor a arquitetura.

Outra coisa: eu documentaria as decisões enquanto refatorava.

Durante o processo, várias escolhas foram feitas:

Essas decisões valem ouro depois, porque ajudam a lembrar que arquitetura é feita de trade-offs.

Como o projeto está agora

Hoje o projeto está bem mais organizado.

O fluxo ficou claro:

REST Controller
  -> Use Case
    -> Gateway Interface
      -> Gateway Implementation
        -> JpaRepository

O domínio ficou limpo.

Os use cases ficaram testáveis.

A infraestrutura ficou isolada.

A API continuou com os mesmos endpoints principais.

E o projeto ficou mais preparado para crescer e a base agora está muito melhor.

Conclusão

Essa refatoração me ensinou que Clean Architecture não precisa ser assustadora.

Ela fica bem mais fácil quando a gente começa pequeno, testa o comportamento e vai migrando aos poucos.

No meu caso, começar por Category, depois Platform, depois Game e só então Auth/User fez toda a diferença.

Não foi uma mudança feita de uma vez só.

Foi uma sequência de passos pequenos:

teste -> use case -> gateway -> infraestrutura -> controller -> limpeza

E esse talvez seja o maior aprendizado.

Refatorar arquitetura não precisa ser um grande salto no escuro.

Pode ser uma caminhada bem guiada, uma decisão por vez.

No fim, o código não ficou só “mais bonito”.

Ficou mais claro, mais testável e mais honesto sobre o que cada parte da aplicação realmente faz.