Prefácio
Em algum momento da sua jornada como desenvolvedor, você já se perguntou em qual camada deveria colocar a conexão com o banco, se a regra de negócio pertence ao Service ou à Entity, se é aceitável enviar e-mails diretamente do Controller ou se o DTO realmente é necessário quando a Entity já possui os campos?
Se essas dúvidas já passaram pela sua cabeça, saiba que isso é mais comum do que parece.
À medida que os projetos deixam de ser apenas CRUDs simples e começam a crescer, a organização do código deixa de ser um detalhe e passa a ser uma preocupação concreta. Misturar responsabilidades pode até funcionar no início, mas, com o tempo, o acoplamento aumenta, as mudanças se tornam mais delicadas e o código passa a exigir um esforço desproporcional para algo que deveria ser simples.
Este artigo nasce justamente dessa inquietação.
A proposta é desmistificar alguns dos principais termos que aparecem quando falamos em arquitetura orientada a objetos — Entity, Model, DAO, Repository, DTO, Service, Controller e Action — e esclarecer o papel de cada um dentro de um projeto.
Mais do que decorar nomes ou seguir padrões por tendência, o objetivo é compreender responsabilidades.
Arquitetura de software não é sinônimo de complexidade. É, acima de tudo, separação consciente de responsabilidades.
De onde vêm esses conceitos?
Essas ideias não surgiram por acaso. Elas são influenciadas por princípios discutidos em livros como Clean Code e Clean Architecture, de Robert C. Martin, e Domain-Driven Design, de Eric Evans.
Essas obras não apresentam receitas prontas nem estruturas rígidas. O que propõem são princípios como separação de responsabilidades, baixo acoplamento, alta coesão e inversão de dependência.
Com o tempo, esses fundamentos passaram a influenciar frameworks, arquiteturas modernas e projetos reais. O problema é que, muitas vezes, aprendemos o “como usar” antes de compreender o “por que usar”.
É exatamente nesse ponto que a confusão começa.
2. Um ponto de partida: o Controller não é dono da regra
Vamos partir de uma premissa simples: o Controller existe para lidar com o mundo externo.
Em uma API HTTP, isso significa receber requisições, validar dados de entrada quanto a formato, tipos e obrigatoriedade, converter essas informações para o formato interno da aplicação, delegar a execução da regra de negócio e, por fim, retornar uma resposta HTTP adequada.
Todas essas responsabilidades estão relacionadas à comunicação. O Controller traduz o mundo externo para algo que a aplicação entende e depois traduz a resposta de volta.
O que ele não deveria fazer é tomar decisões de negócio, conversar diretamente com o banco, enviar e-mails, consumir APIs externas, executar regras complexas ou controlar transações.
Quando o Controller passa a assumir esse tipo de responsabilidade, deixa de ser apenas uma camada de entrada e começa a concentrar decisões que não lhe pertencem. As consequências tendem a ser previsíveis: alto acoplamento, dificuldade de testes, maior complexidade para evoluir o código e repetição de lógica em diferentes endpoints.
O sistema pode continuar funcionando, mas perde organização. E sem organização, a manutenção inevitavelmente se torna mais custosa.
Sobre validação
Ao falar em validação no Controller, é importante fazer uma distinção.
Nem toda validação é igual. Existe a validação estrutural, responsável por verificar formato, tipagem e campos obrigatórios, e existe a validação de regra de negócio, que envolve regras do domínio, como e-mail já cadastrado, saldo insuficiente ou limite excedido.
A validação estrutural faz sentido na camada de entrada, pois está ligada à integridade dos dados recebidos. Já a validação de regra pertence à camada de aplicação ou ao próprio domínio.
Em alguns frameworks, como o Laravel, essas responsabilidades podem parecer misturadas. Por isso, essa distinção é relevante: quando não diferenciamos esses tipos de validação, o Controller acaba acumulando responsabilidades sem perceber.
Se o Controller não é responsável pela regra de negócio, surge então uma pergunta natural: onde essa regra deve ficar?
É nesse momento que a separação de camadas deixa de ser apenas teoria e passa a fazer sentido na prática. A intenção não é adicionar complexidade desnecessária, mas distribuir responsabilidades de maneira clara e coerente.
Camadas
DTO — O contrato com o mundo externo
Se o Controller interage com o mundo externo, ele precisa de uma estrutura que organize essa troca de dados. É aqui que entra o DTO (Data Transfer Object).
O DTO não representa o domínio da aplicação; ele representa o contrato de entrada ou saída. Um CreateUserRequest, por exemplo, pode conter apenas nome e e-mail, exatamente como esperado pela API.
Ele não contém regra de negócio, não conhece banco de dados e não depende de infraestrutura. Sua função é transportar dados entre camadas.
Nesse ponto, surge uma dúvida comum: se a Entity já possui esses campos, por que criar DTO?
Porque o domínio não deve depender do formato externo da aplicação. O DTO protege a Entity de mudanças na API, evita a exposição de campos sensíveis e impede que detalhes internos vazem para fora. Ele funciona como uma barreira entre o mundo externo e o núcleo da aplicação.
Service — Onde a decisão acontece
Se o Controller não deve tomar decisões, alguém precisa assumir essa responsabilidade.
Essa função costuma ficar no Service — abordagem mais comum — ou, em arquiteturas orientadas a caso de uso, em uma Action.
O Service é responsável por orquestrar o fluxo da aplicação. É nele que a regra de negócio acontece. Pode aplicar validações de domínio, consultar repositórios, criar ou modificar entidades, coordenar efeitos colaterais como envio de e-mails ou integração com APIs externas e controlar transações quando necessário.
Diferente do Controller, o Service não sabe nada sobre HTTP. Ele não sabe se a aplicação é uma API, um CLI ou um job agendado. Ele simplesmente executa um fluxo de negócio.
Essa separação traz um benefício importante: a regra deixa de depender da forma como o sistema é acessado. Se amanhã for necessário expor o mesmo fluxo por meio de fila, CLI ou outro endpoint, o Service continuará o mesmo, o que contribui para escalabilidade e reaproveitamento.
Vale uma observação: Service não deve existir apenas para repassar chamadas. Se ele apenas invoca repository.save() sem tomar decisões, talvez não esteja agregando valor real ao fluxo.
Entity — O coração do domínio
Se o Service decide o fluxo, a Entity representa o que o sistema é.
Ela não é apenas uma estrutura de dados com getters e setters, mas a representação de um conceito do domínio. Um Usuário, um Pedido ou uma Fatura são exemplos claros de entidades.
Uma Entity possui identidade. Mesmo que seus atributos mudem, ela continua sendo a mesma entidade dentro do sistema.
Dependendo do nível de maturidade arquitetural, pode conter regras relacionadas à sua própria consistência. Um Pedido pode impedir que seja finalizado duas vezes; uma Conta pode impedir saque se o saldo for insuficiente; um Usuário pode normalizar seu próprio e-mail ao ser criado.
Essas regras dizem respeito à coerência interna do objeto, não ao fluxo da aplicação.
A Entity não precisa conhecer banco de dados, HTTP, frameworks, envio de e-mails ou execução de queries. Ela representa o domínio, não a infraestrutura. Quando passa a depender de detalhes externos, o domínio perde independência e se torna acoplado à tecnologia.
Inicialmente, tive bastante confusão entre Entity e DTO. De maneira simples, posso dizer que a Entity está mais próxima do domínio e, muitas vezes, do mapeamento com o banco ou ORM, enquanto o DTO está relacionado à transferência de dados entre camadas. Isso significa que é perfeitamente normal existir Entities e DTOs no mesmo projeto.
Repository — Abstração de persistência
Se a Entity representa o domínio e o Service concentra a decisão, alguém precisa cuidar da persistência.
O Repository cumpre esse papel. Ele representa um conjunto de operações relacionadas a uma entidade e se comunica na linguagem do domínio, não na linguagem da tabela.
Em vez de pensar em “tabela users”, o Repository pensa em “Usuários”. Pode oferecer operações como salvar um usuário, buscar por e-mail, listar ativos ou remover por id.
O ponto central é que o Service não deve conhecer os detalhes da persistência. Ele não precisa saber se está utilizando JPA, SQL puro, MongoDB ou qualquer outra tecnologia. Interage apenas com um contrato.
Essa separação permite trocar a tecnologia de armazenamento, testar a regra de negócio de forma isolada e reduzir o acoplamento com a infraestrutura.
O Repository não executa regra de negócio; ele executa persistência.
DAO — O aspecto mais técnico da persistência
DAO (Data Access Object) é um padrão mais antigo e mais técnico.
Enquanto o Repository é orientado ao domínio, o DAO costuma ser mais voltado à infraestrutura. Ele encapsula operações diretas com o banco, como execução de queries, mapeamento de resultados, controle de conexão e manipulação de SQL específico.
Em projetos simples, DAO e Repository podem acabar desempenhando papéis muito semelhantes. Conceitualmente, porém, existe uma diferença sutil: o DAO fala a linguagem do banco; o Repository fala a linguagem do domínio.
Essa distinção tende a ficar mais evidente conforme o sistema cresce.
Conclusões
À medida que aprofundamos a separação de responsabilidades, é comum encontrar variações desses conceitos em arquiteturas mais estruturadas.
Em vez de um Service com vários métodos, algumas abordagens adotam o conceito de Use Case ou Action, em que cada fluxo de negócio é representado por uma classe específica, como CreateUser ou CancelOrder. Isso reduz o tamanho das classes e torna cada comportamento mais explícito.
Outro conceito recorrente é o Mapper, responsável por converter DTOs em Entities e vice-versa. Em projetos menores, essa conversão pode permanecer no Service sem grandes problemas; em sistemas maiores, centralizá-la tende a melhorar a organização e evitar repetições.
Também existe o Domain Service, discutido em Domain-Driven Design, utilizado quando uma regra não pertence claramente a uma única entidade. É um tema que merece aprofundamento próprio, mas sua menção ajuda a mostrar que a separação de responsabilidades pode evoluir conforme o domínio se torna mais complexo.
Nem sempre precisamos de todas essas camadas. Em projetos muito pequenos ou aplicações que dificilmente irão evoluir, criar múltiplas camadas pode gerar mais complexidade do que benefício.
Por outro lado, quando o sistema começa a crescer, novas regras surgem, integrações são adicionadas e diferentes pontos de entrada aparecem, a ausência de separação passa a cobrar um preço alto.
Arquitetura não é formalidade; é preparação para mudança.
É comum que um sistema comece simples e, com o tempo, passe por refatorações que separem responsabilidades antes misturadas. Isso não significa que o projeto estava errado no início, mas que ele amadureceu.
O próprio Clean Code reforça a ideia de melhoria contínua por meio de refatoração. Separar camadas pode ser uma dessas melhorias.
Muitos desenvolvedores acreditam que decisões arquiteturais precisam ser perfeitas desde o primeiro dia. Na prática, raramente isso acontece. Projetos podem adotar separação de camadas mesmo após anos em produção, de forma gradual: primeiro extraindo regras do Controller, depois isolando persistência e, por fim, introduzindo contratos mais claros.
Arquitetura também evolui.
E essa evolução nos leva a uma discussão importante: como essas camadas se comportam quando utilizamos ORM, Active Record ou SQL puro?
Dependendo da tecnologia escolhida, algumas camadas podem assumir papéis diferentes. Em frameworks que utilizam Active Record, como o Eloquent do Laravel, a própria classe Model costuma acumular responsabilidades de entidade e persistência. Isso simplifica o desenvolvimento inicial, mas pode misturar domínio e infraestrutura.
Em arquiteturas baseadas em ORM mais tradicionais, como JPA no ecossistema Java, é comum que as Entities sejam mapeadas para o banco por meio de anotações, enquanto o acesso aos dados ocorre via Repository.
Já em projetos que utilizam SQL puro, sem ORM, padrões como DAO fazem ainda mais sentido, pois encapsulam queries, mapeamento de resultados e controle de conexão.
A tecnologia influencia a forma como esses padrões são aplicados, mas os princípios de separação de responsabilidades permanecem os mesmos.
Independentemente de usar ORM ou SQL direto, a pergunta continua válida: quem decide, quem representa o domínio e quem persiste?
Encerramento
Quando comecei a estudar esses conceitos, a dúvida sempre era muito direta: “em qual camada eu coloco isso?”. Com o tempo, percebi que a pergunta mais importante não era onde colocar o código, mas se aquela responsabilidade realmente pertencia ali.
Essa mudança de perspectiva transforma a forma como enxergamos arquitetura. Ela deixa de ser um conjunto de nomes complexos e passa a ser um exercício constante de organização.
Nem todo projeto precisa de todas as camadas desde o primeiro dia. Mas todo projeto que cresce precisa, em algum momento, organizar suas responsabilidades.
Se este texto conseguir tornar essa distinção um pouco mais clara, então ele já cumpriu seu papel.
Top comments (0)