A arquitetura começa a complicar quando a gente escreve código
A arquitetura começa a complicar quando a gente escreve código#
Uma entidade, alguns Value Objects e a descoberta de que separar responsabilidades também tem seus custos.
Publicado em 19 de outubro de 2026 · Diário de desenvolvimento
No artigo anterior, registrei as primeiras mudanças do Event Driven Architecture Lab. A infraestrutura estava funcionando, a stack havia mudado de NestJS para Symfony e o próximo passo era finalmente começar a escrever código de negócio.
Pois bem, comecei.
A ideia era simples: criar as primeiras entidades do domínio e avançar até o caso de uso de criação de pedidos. Nada muito sofisticado.
Mas bastou começar a modelar Customer e Order para aparecerem questões que não se resolvem apenas escolhendo uma arquitetura e seguindo suas recomendações.
Começando pelos módulos#
Minha primeira decisão foi organizar o código por módulo de negócio:
src/
└── Modules/
├── Customer/
│ └── Domain/
└── Order/
└── Domain/
A motivação era prática. Conforme os casos de uso crescem, um diretório cheio de Create*, Update*, Delete* e outras operações começa a ficar pouco interessante de navegar.
Agrupar o código por módulo parece uma alternativa melhor. Tudo relacionado a clientes fica em Customer; o que pertence aos pedidos fica em Order.
Também pensei numa possível extração futura para microserviços. Ter módulos com responsabilidades mais claras pode ajudar a identificar fronteiras, embora isso não signifique que cada módulo deva virar um serviço independente.
Até aqui, parecia uma boa escolha.
Uma entidade não deveria nascer inválida#
Comecei pela entidade Customer, com identificador, nome e e-mail.
O identificador deveria ser um UUID. Receber uma string no construtor seria simples, mas permitiria passar qualquer valor. Se queremos que a entidade nasça em um estado válido, faz sentido representar sua identidade por um tipo próprio.
Assim surgiu a ideia de CustomerId, um Value Object que será responsável por validar e representar o identificador.
O mesmo raciocínio vale para Email: em vez de espalhar regras de validação pelo sistema, podemos concentrá-las no objeto que representa esse conceito.
Não precisamos de uma fábrica, uma interface ou uma camada adicional para cada valor. A ideia é criar tipos que expressem o domínio e protejam suas invariantes.
Mas essa decisão trouxe uma pergunta que eu não tinha considerado inicialmente.
O pedido precisa conhecer o cliente inteiro?#
A primeira versão de Order receberia uma instância de Customer. Afinal, um pedido pertence a um cliente.
Só que nosso modelo conceitual já previa uma tabela chamada order_customer_snapshot, destinada a preservar os dados do cliente no momento da compra.
Se o cliente mudar de nome ou de e-mail depois, não queremos que um pedido antigo passe a representar os dados atuais em vez daqueles registrados no checkout.
A solução começou a aparecer: criar um CustomerSnapshot no módulo Order, contendo os dados necessários para representar o cliente naquele pedido.
Modules/
├── Customer/
│ └── Domain/
│ ├── Customer.php
│ ├── CustomerId.php
│ └── Email.php
└── Order/
└── Domain/
├── Order.php
├── OrderId.php
└── CustomerSnapshot.php
O snapshot pertence ao contexto do pedido, mesmo que seus dados tenham origem no cliente.
Essa decisão reduz a necessidade de manter uma referência à entidade Customer inteira. O pedido preserva as informações relevantes para a compra sem depender de todo o estado atual do cliente.
E então apareceu outra questão: CustomerSnapshot precisava de CustomerId e Email, definidos no módulo Customer.
E os Value Objects compartilhados?#
Poderíamos criar um módulo Shared e mover para lá os objetos de valor utilizados por mais de um módulo.
Mas compartilhar uma classe só porque duas partes do sistema usam um e-mail ou um identificador é sempre uma boa ideia?
Reutilizar código reduz duplicação, mas cria dependências. Duplicar um conceito pode dar autonomia aos módulos, mas também pode duplicar regras e permitir que eles evoluam de maneiras incompatíveis.
Não existe uma resposta universal que possamos aplicar sem considerar o significado daquele conceito no domínio.
Por enquanto, a questão permanece aberta. A implementação atual reutiliza os tipos de Customer, mas isso não significa que essa seja necessariamente a melhor solução definitiva.
O importante é reconhecer que organizar o código em módulos não elimina as dependências. A estrutura ajuda a torná-las visíveis; cabe a nós decidir quais fazem sentido e quais podem se tornar um problema.
Clean Architecture, DDD e o risco de complicar demais#
Essas discussões aparecem com frequência quando tentamos aplicar conceitos conhecidos de arquitetura e modelagem de domínio.
Clean Architecture ajuda a pensar nas dependências entre regras de negócio e detalhes técnicos. DDD oferece conceitos como entidades e Value Objects para representar melhor o domínio. SOLID fornece princípios para pensar nas responsabilidades e nos acoplamentos.
O problema começa quando tentamos transformar cada recomendação em uma estrutura obrigatória.
De repente, uma entidade precisa de uma interface, uma fábrica, um contrato, uma pasta compartilhada e mais algumas abstrações antes mesmo de existir um caso de uso funcional.
Essas ferramentas têm seu lugar. A questão é saber quando resolvem um problema real e quando estamos apenas antecipando problemas hipotéticos.
Quero manter os módulos e tornar as responsabilidades explícitas, mas não preciso criar todos os diretórios antes de ter código que justifique sua existência.
A arquitetura deve ajudar a entender e modificar o sistema, não transformar cada nova classe em uma reunião de planejamento.
Pensando em microserviços antes da hora?#
Confesso que já estava pensando em como esses módulos se comportariam caso fossem separados em microserviços. KKKK.
Essa preocupação não é necessariamente ruim. Fronteiras claras podem facilitar uma extração futura, e dependências excessivas entre módulos podem tornar essa separação mais difícil.
Mas existe uma diferença entre observar essas fronteiras e construir um monólito como se cada módulo já fosse um serviço independente.
Dentro do mesmo processo, compartilhar uma classe PHP é simples. Quando os módulos passam a ser serviços separados, precisamos lidar com contratos de comunicação, serialização e evolução independente.
Uma separação que parece elegante pode cobrar um preço desnecessário hoje. Uma abstração útil agora pode criar acoplamento amanhã.
Não quero ignorar essas consequências, mas também não quero pagar antecipadamente por uma arquitetura distribuída que ainda nem existe.
Um laboratório em andamento#
Até aqui, não temos o caso de uso de criação de pedidos implementado. Ainda não há resultados de performance ou uma arquitetura orientada a eventos funcionando para apresentar.
O que temos são decisões concretas que surgiram enquanto começávamos a modelar o domínio:
- Agrupar o código por módulo ajuda a evitar diretórios gigantes de casos de uso.
- Value Objects podem proteger invariantes sem espalhar validações pelas entidades.
- Um snapshot pode preservar os dados relevantes para um pedido sem depender da entidade completa do cliente.
- Compartilhar tipos reduz duplicação, mas cria dependências que precisam ser avaliadas.
- Pensar em microserviços desde o início pode ajudar a enxergar fronteiras, mas também pode incentivar abstrações prematuras.
Nada disso é uma descoberta revolucionária. São problemas conhecidos, discutidos há muito tempo.
A diferença é que agora eles apareceram no código que estou tentando construir, e preciso decidir o que fazer com eles.
O próximo passo é continuar a modelagem e implementar o caso de uso de criação de pedidos. Depois, com código executável e testes, teremos mais elementos para avaliar as decisões que tomamos até aqui.
O repositório registra as mudanças. Este espaço serve para registrar o raciocínio, as dúvidas e as consequências dessas escolhas.
Não há uma arquitetura perfeita para apresentar. Há um experimento em andamento e a intenção de entender o que estamos construindo, em vez de apenas reproduzir uma estrutura encontrada em um livro ou tutorial.
É por aí que vamos continuar.
Este artigo foi elaborado em parceria com Gepeto, com auxílio de inteligência artificial, e revisado pelo autor. As decisões e discussões descritas correspondem ao estado do laboratório em 19 de outubro de 2026.