A comprehensive breakdown of architectural responsibilities in object-oriented applications, clarifying the distinct roles of each layer and their interactions.
Entity, DTO, Repository, Service… So what does each do really?
Preface
At some point in your journey as a developer, you've probably wondered which layer should handle the database connection, whether business logic belongs in the Service or Entity, if it's acceptable to send emails directly from the Controller, or if DTOs are really necessary when the Entity already has the fields. If these questions have crossed your mind, know that this is more common than you might think. As projects evolve from simple CRUD applications to more complex systems, code organization ceases to be a detail and becomes a concrete concern. Mixing responsibilities might work initially, but over time, coupling increases, changes become more delicate, and the code starts to require disproportionate effort for what should be simple operations.
This article stems from that very inquietude. The goal is to demystify some of the main terms that appear when discussing object-oriented architecture—Entity, Model, DAO, Repository, DTO, Service, Controller, and Action—and clarify the role of each within a project. More than memorizing names or following patterns out of trend, the objective is to understand responsibilities. Software architecture isn't synonymous with complexity. Above all, it's the conscious separation of responsibilities.
Where do these concepts come from?
These ideas didn't emerge by accident. They're influenced by principles discussed in books like Robert C. Martin's Clean Code and Clean Architecture, and Eric Evans' Domain-Driven Design. These works don't provide ready-made recipes or rigid structures. Instead, they propose principles such as separation of responsibilities, low coupling, high cohesion, and dependency inversion. Over time, these fundamentals have come to influence frameworks, modern architectures, and real-world projects.
The problem is that we often learn "how to use" before understanding "why to use." It's exactly at this point that confusion begins.
A starting point: The Controller doesn't own the rules
Let's start with a simple premise: the Controller exists to handle the external world. In an HTTP API, this means receiving requests, validating input data for format, types, and mandatory fields, converting this information into the application's internal format, delegating business logic execution, and finally returning an appropriate HTTP response. All these responsibilities relate to communication. The Controller translates the external world into something the application understands and then translates the response back.
What it shouldn't do is make business decisions, talk directly to the database, send emails, consume external APIs, execute complex rules, or control transactions. When the Controller starts assuming these responsibilities, it ceases to be merely an entry layer and begins to concentrate decisions that don't belong to it.
The consequences tend to be predictable: high coupling, difficulty in testing, greater complexity for evolving the code, and repetition of logic across different endpoints. The system might continue functioning, but it loses organization. And without organization, maintenance inevitably becomes more costly.
About validation
When discussing validation in the Controller, it's important to make a distinction. Not all validation is equal. There's structural validation, responsible for verifying format, typing, and mandatory fields, and there's business rule validation, which involves domain rules like email already registered, insufficient balance, or limit exceeded.
Structural validation makes sense in the entry layer since it's tied to data integrity. Business rule validation, however, belongs to the application layer or the domain itself. In some frameworks like Laravel, these responsibilities might seem mixed. That's why this distinction is relevant: when we don't differentiate these types of validation, the Controller ends up accumulating responsibilities without realizing it.
If the Controller isn't responsible for business logic, a natural question arises: where should that rule reside? It's at this moment that layer separation ceases to be just theory and starts making practical sense. The intention isn't to add unnecessary complexity, but to distribute responsibilities clearly and coherently.
Layers
DTO — The contract with the external world
If the Controller interacts with the external world, it needs a structure that organizes this data exchange. This is where the DTO (Data Transfer Object) comes in. The DTO doesn't represent the application's domain; it represents the input or output contract. A CreateUserRequest, for example, might contain only name and email, exactly as expected by the API. It doesn't contain business logic, doesn't know about databases, and doesn't depend on infrastructure. Its function is to transport data between layers.
At this point, a common doubt arises: if the Entity already has these fields, why create a DTO? Because the domain shouldn't depend on the external format of the application. The DTO protects the Entity from API changes, prevents exposure of sensitive fields, and stops internal details from leaking outside. It acts as a barrier between the external world and the application's core.
Service — Where decisions happen
If the Controller shouldn't make decisions, someone needs to assume that responsibility. This function usually resides in the Service—the more common approach—or, in use-case-oriented architectures, in an Action. The Service is responsible for orchestrating the application flow. This is where business logic happens. It can apply domain validations, consult repositories, create or modify entities, coordinate side effects like email sending or external API integration, and control transactions when necessary.
Unlike the Controller, the Service knows nothing about HTTP. It doesn't know if the application is an API, CLI, or scheduled job. It simply executes a business flow. This separation brings an important benefit: the rule no longer depends on how the system is accessed. If tomorrow it becomes necessary to expose the same flow through a queue, CLI, or another endpoint, the Service remains the same, which contributes to scalability and reusability.
A word of caution: a Service shouldn't exist merely to pass calls. If it only invokes repository.save() without making decisions, it might not be adding real value to the flow.
Entity — The heart of the domain
If the Service decides the flow, the Entity represents what the system is. It's not just a data structure with getters and setters, but the representation of a domain concept. A User, an Order, or an Invoice are clear examples of entities. An Entity has identity. Even if its attributes change, it remains the same entity within the system. Depending on the architectural maturity level, it may contain rules related to its own consistency. An Order might prevent being finalized twice; an Account might prevent withdrawal if the balance is insufficient; a User might normalize its own email upon creation. These rules concern the object's internal coherence, not the application flow.
The Entity doesn't need to know about databases, HTTP, frameworks, email sending, or query execution. It represents the domain, not the infrastructure. When it starts depending on external details, the domain loses independence and becomes coupled to the technology.
Initially, I had considerable confusion between Entity and DTO. Simply put, I can say that the Entity is closer to the domain and often to the mapping with the database or ORM, while the DTO is related to data transfer between layers. This means it's perfectly normal to have both Entities and DTOs in the same project.
Repository — Persistence abstraction
If the Entity represents the domain and the Service concentrates decisions, someone needs to handle persistence. The Repository fulfills this role. It represents a set of operations related to an entity and communicates in the domain's language, not the table's language. Instead of thinking about "users table", the Repository thinks about "Users". It can offer operations like saving a user, searching by email, listing active ones, or removing by ID.
The central point is that the Service shouldn't know persistence details. It doesn't need to know if it's using JPA, plain SQL, MongoDB, or any other technology. It interacts only with a contract. This separation allows changing the storage technology, testing business logic in isolation, and reducing coupling with infrastructure.
The Repository doesn't execute business logic; it executes persistence.
DAO — The more technical aspect of persistence
DAO (Data Access Object) is an older, more technical pattern. While the Repository is domain-oriented, the DAO tends to be more infrastructure-focused. It encapsulates direct database operations like query execution, result mapping, connection control, and manipulation of specific SQL.
In simple projects, DAO and Repository might end up performing very similar roles. Conceptually, however, there's a subtle difference: the DAO speaks the database's language; the Repository speaks the domain's language. This distinction tends to become more evident as the system grows.
Conclusions
As we deepen the separation of responsibilities, it's common to find variations of these concepts in more structured architectures. Instead of a Service with multiple methods, some approaches adopt the Use Case or Action concept, where each business flow is represented by a specific class, like CreateUser or CancelOrder. This reduces class size and makes each behavior more explicit.
Another recurring concept is the Mapper, responsible for converting DTOs to Entities and vice versa. In smaller projects, this conversion can remain in the Service without major issues; in larger systems, centralizing it tends to improve organization and avoid repetition.
There's also the Domain Service, discussed in Domain-Driven Design, used when a rule doesn't clearly belong to a single entity. This deserves its own in-depth exploration, but its mention helps show that responsibility separation can evolve as the domain becomes more complex.
We don't always need all these layers. In very small projects or applications that are unlikely to evolve, creating multiple layers can generate more complexity than benefit. On the other hand, when the system starts growing, new rules emerge, integrations are added, and different entry points appear, the absence of separation starts to extract a high price.
Architecture isn't formality; it's preparation for change. It's common for a system to start simple and, over time, undergo refactoring that separates previously mixed responsibilities. This doesn't mean the project was wrong initially, but that it has matured. Clean Code itself emphasizes continuous improvement through refactoring. Separating layers can be one of those improvements.
Many developers believe that architectural decisions need to be perfect from day one. In practice, this rarely happens. Projects can adopt layer separation even after years in production, gradually: first extracting rules from the Controller, then isolating persistence, and finally introducing clearer contracts.
Architecture also evolves. This evolution leads us to an important discussion: how do these layers behave when using ORM, Active Record, or plain SQL? Depending on the technology chosen, some layers might assume different roles. In frameworks using Active Record, like Laravel's Eloquent, the Model class itself often accumulates entity and persistence responsibilities. This simplifies initial development but can mix domain and infrastructure. In ORM-based architectures like Java's JPA, it's common for Entities to be mapped to the database via annotations, while data access occurs through Repository. In projects using plain SQL without ORM, patterns like DAO make even more sense, as they encapsulate queries, result mapping, and connection control.
The technology influences how these patterns are applied, but the principles of responsibility separation remain the same. Regardless of using ORM or direct SQL, the question remains valid: who decides, who represents the domain, and who persists?
Closing
When I started studying these concepts, my question was always very direct: "in which layer do I put this?" Over time, I realized that the more important question wasn't where to place the code, but whether that responsibility truly belonged there. This change in perspective transforms how we view architecture. It ceases to be a set of complex names and becomes a constant exercise in organization.
Not every project needs all layers from day one. But every growing project needs, at some point, to organize its responsibilities. If this text can make this distinction a bit clearer, then it has already fulfilled its purpose.

Comments
Please log in or register to join the discussion