DEV Community

Vitor Fábio
Vitor Fábio

Posted on

Entity, DTO, Repository, Service… afinal, quem faz o quê?

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.


Reflexões

À medida que aprofundamos a separação de responsabilidades, é natural que surjam variações desses mesmos conceitos em arquiteturas mais estruturadas.

Em vez de concentrar várias responsabilidades dentro de um único Service com muitos métodos, algumas abordagens preferem trabalhar com Use Cases ou Actions, em que cada fluxo de negócio ganha sua própria classe, como CreateUser ou CancelOrder. Isso tende a reduzir o tamanho das classes e deixa cada comportamento mais explícito, quase como uma narrativa do que o sistema faz.

Outro conceito que costuma aparecer nesse contexto é o Mapper, responsável por converter DTOs em Entities e vice-versa. Em projetos menores, essa conversão pode permanecer dentro do Service sem grandes problemas. Em sistemas maiores, porém, centralizar essa responsabilidade ajuda a evitar repetição e torna o código mais organizado.

Também existe o Domain Service, bastante discutido em Domain-Driven Design. Ele entra em cena quando uma regra não pertence claramente a uma única entidade. É um tema que merece um aprofundamento próprio, mas vale a menção para mostrar que a separação de responsabilidades não é estática, ela pode evoluir conforme o domínio se torna mais complexo.

No fundo, o que estamos fazendo ao organizar essas camadas é aproximar o código do domínio do negócio. E essa é justamente a essência do DDD: estruturar o sistema em torno do que ele representa, e não da tecnologia que o sustenta.


Arquitetura é preparação para mudança

Nem todo projeto precisa nascer com todas essas camadas bem definidas. Em aplicações muito pequenas ou sistemas que dificilmente irão evoluir, criar múltiplas abstrações 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.

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 que antes estavam misturadas. Isso não significa que o projeto estava errado no início; significa que ele amadureceu.

O próprio Clean Code reforça essa ideia de melhoria contínua por meio de refatoração. Separar camadas pode ser apenas mais um passo nessa direção.


Arquitetura também evolui

Existe a ideia de que decisões arquiteturais precisam ser perfeitas desde o primeiro dia. Na prática, isso raramente acontece.

Projetos podem adotar separação de camadas mesmo após anos em produção, e essa reorganização costuma acontecer de forma gradual: primeiro extraindo regras do Controller, depois isolando persistência, depois introduzindo contratos mais claros entre as partes.

Quando essa organização começa a se repetir em diferentes áreas do sistema, surge algo maior do que apenas camadas. Surgem módulos organizados por contexto, por domínio, por significado.

Nesse momento, a arquitetura deixa de ser apenas uma divisão técnica e passa a refletir o próprio negócio. E é justamente aí que conceitos do Domain-Driven Design começam a fazer mais sentido.


Conclusão

No fim das contas, esses padrões não existem para tornar o código mais sofisticado nem para atender a uma formalidade arquitetural. Eles existem para deixar claro quem faz o quê e para manter o domínio no centro da aplicação.

Quando essa clareza aparece, o sistema deixa de ser apenas funcional e passa a ser sustentável. Sustentável no sentido de poder crescer, mudar e evoluir sem que cada nova alteração gere insegurança ou retrabalho excessivo.

Arquitetura não é sobre criar barreiras artificiais. É sobre dar espaço para que o domínio respire e para que o sistema acompanhe a complexidade do mundo real sem perder organização.


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)