Desenvolvimento modularizado com pacote por recurso (Package by Feature)
É bastante comum que você já tenha feito manutenção ou desenvolvido uma aplicação organizando os pacotes através das camadas de responsabilidade do código (controllers, services, dtos…), algo mais ou menos assim:
/app
....Application.java
..../controllers
........UserController.java
........ContactController.java
........PaymentController.java
........NotificationController.java
..../entities
........User.java
........Contact.java
........Payment.java
........Notification.java
..../dtos
........UserDto.java
........ContactDto.java
........PaymentDto.java
........NotificationDto.java
..../services
........UserService.java
........ContactService.java
........PaymentService.java
........NotificationService.java
..../repositories
........UserRepository.java
........ContactRepository.java
........PaymentRepository.java
........NotificationRepository.java
Essa forma, por quantidade de exemplos que vimos acaba sendo a maneira mais intuitiva de estruturar nosso código, acontece que há algumas desvantagens nessa abordagem.
O primeiro ponto é consulta e alteração de código, se quisermos alterar ou adicionar uma funcionalidade, temos que percorrer e abrir vários diretórios no projeto para conseguir. As IDEs facilitam esse processo, mas ele não deixa de existir. Em casos de buscar essas informações em repositórios de git, por exemplo, não temos essa facilidade.
Outro ponto específico no Java é que os diretórios tem relação direta com os pacotes (packages) da nossa aplicação. Interferindo assim em dois pontos do design de código: Coesão e acoplamento.
Baixa coesão, pois temos classes normalmente sem relação direta umas com as outras dentro de um mesmo pacote e um alto acoplamento entre os pacotes, pois quase tudo precisará de chamadas externas. O inverso do que queremos.
Um pilar da orientação à objetos também interferido é o encapsulamento, já que a maioria das classes precisam ser públicas para os demais pacotes conseguirem acessar.
Uma outra forma que podemos organizar as classes da nossa aplicação então é através de pacote por recurso (package by feature), onde buscamos colocar em cada pacote somente classes relacionadas ao contexto:
/app
....Application.java
..../user
........UserController.java
........ContactController.java
........UserService.java
........ContactService.java
........UserRepository.java
........ContactRepository.java
........User.java
........Contact.java
........UserDto.java
........ContactDto.java
..../payment
........PaymentController.java
........PaymentService.java
........PaymentRepository.java
........Payment.java
........PaymentDto.java
..../notification
........NotificationController.java
........NotificationService.java
........NotificationRepository.java
........Notification.java
........NotificationDto.java
Pelo costume, em um primeiro momento parece até um pouco confuso ou bagunçado ver classes de responsabilidades diferentes em um mesmo diretório, mas logo vamos nos familiarizando e nos sentindo confortáveis com essa forma. Com isso já resolvermos o primeiro ponto de percorrer vários diretórios para alteração do código.
Perceba que cada pacote não representa estritamente a manipulação de uma única entidade, mas de uma funcionalidade/contexto. Nosso pacote de usuários manipula dados de usuários e contatos do usuário, já que pertencem ao mesmo assunto de maneira geral.
O outro ponto é que podemos definir algumas classes, atributos ou métodos com visibilidade apenas para o pacote e melhorando assim o encapsulamento.
Um repositório, por exemplo, não faz muito sentido ser público e acessível para os outros módulos. O ideal é que a chamada a partir de outros módulos seja feita através do serviço (service), onde aplicam-se regras de negócio para o acesso e alteração de dados.
O acoplamento sempre vai existir, mas o objetivo no desenvolvimento é sempre ter o menor possível, com a maior coesão possível.
Para classes que sabemos que sempre devem ser públicas, podemos criar uma organização um pouco mais rebuscada com subpacotes para organizar o código e não ter tantas classes espalhadas, algo parecido com a estrutura apresentada abaixo:
Embora seja uma divisão por responsabilidade, parecido com o pacote por camadas (package by layer), será algo limitado apenas ao módulo, com poucas chances de ganhar grande volume de arquivos.
Monolito para microsseriços
Um artigo bastante famoso do Martin Fowler é o Monolith First, onde ele cita a importância de começar com uma aplicação monolítica ao invés de já começar com microsserviços. Estruturar de uma forma direcionada aos contextos e domínios da aplicação ajuda bastante nesse processo de migração para microsserviços quando o projeto e time envolvido crescerem.
Particularmente já realizei tarefas de refatorações de mover recursos de um microsserviço para outro e percebi um esforço grande em buscar tudo relacionado ao que estava querendo mover por vários diretórios do projeto, sem ficar com a sensação de que não estava esquecendo nada.
Relacionamento de entidades
Os frameworks ORMs facilitam muito nosso processo de criação de tabelas e relacionamento entre elas através das entidades, porém, nesse modelo de modularização é mais interessante que só existam relacionamentos entre entidades do mesmo pacote (ou modulo, recurso, como queria chamar). Uma forma de enxergar é como se cada módulo fosse um microsserviço, mas em um mesmo executável.
Podemos imaginar ter uma Notificação enviada para um usuário, onde nela guardamos o endereço de contato, a mensagem e o ID do usuário. É interessante dessa forma, mantendo apenas o ID do usuário e não uma relação a nível de entidade, pois não teremos tanto acoplamento entre módulos e em caso de migrarmos para microsserviço e Notificação passar a ser externo, teremos menos trabalho na reestruturação das chamadas.
Vantagens e desvantagens
Decisões são uma eterna análise de trade-offs e aqui estamos.
Como vantagens: Maior coesão, menor acoplamento, maior manutenibilidade, maior facilidade para migração para microsserviços.
Como desvantagem: Acabarmos escrevendo mais código devido à comunicação entre módulos e relacionamentos, duplicidade de código e dificuldade de decisão do que deveria conter em cada módulo.
Não há decisão absoluta escrita em pedra de como você deve estruturar seu projeto, mas espero ter ajudado nessa outra visão.