The Disadvantages of Using Data Transfer Objects (DTOs) and How to Address Them in Your Codebase
Marco Antonio Uzcátegui Pescozo
EY Senior Manager | AWS Certified Cloud Practitioner | AI Bots & Cloud Lover | Brand Ambassador
Introduction:
The objectives and constraints of organizations can vary significantly. As a Senior Software Engineer working on a range of projects, I observed that what is considered clean and efficient in one context is not necessarily the case in another.
The use of Data Transfer Objects (DTOs) is one topic that frequently arose during this period. In this article, I aim to share my experience of when DTOs can cause more harm than good, and provide five reasons why you should avoid using them.
What are DTOs?
A DTO is an object that bundles some data together, which is then sent from one application to another or within the same application to transfer the data in a structured and easy way. For instance, the following code represents a simple example of a DTO:
// Before DT
public void AddUser(string firstName, string lastName, int age, ...);
// After DTO
public void AddUser(User user);
public class User
{
public string FirstName {get; set;}
public string LastName {get; set;}
public int Age {get; set;}
}
DTOs are commonly used in the following contexts
External Contracts:
For instance, when building an API or SDK, the GET User endpoint will require some arguments to fetch a User from the database. We do not want to expose the internal database ID of the user for security reasons, or we might want to filter out unnecessary properties.
Therefore, we can create models or contracts for our API, such as the GetUserResponse class, which contains only the required fields. We can then map our User object to the GetUserResponse object before sending it back to the caller. Contracts can be shared between different applications, repositories, or teams, allowing us to introduce versioning and cater for breaking changes.
Internal Contracts:
This is when DTOs are used within the same application but between different layers, such as the Clean Architecture. We might have an Infrastructure layer that manipulates the database, an Application layer for our business logic, and other layers.
Each layer can have its own interfaces, which are a form of contract that needs to be fulfilled. DTOs are often used to create database models and map them to the Application or business logic models.
Convenience:
DTOs can also be used for convenience, where multiple pieces of information are bundled together and passed around without any contract.
领英推荐
Common Microservices use-case:
A common use case for microservices involves defining or honoring a contract. This involves duplicating classes and properties to achieve separation, which can give the impression of isolation and independence. However, in a microservices architecture, scalability and growth are essential, and standard contracts can be crafted and shared across services.
For instance, a GetUserResponse of Service A could be part of a library that Service B calls and uses, with both services mapping from this contract. At first, this may seem like a simple and harmless use case. However, after several months of adding features and integrations, this approach may cause some issues.
Service B may have to save new versions of the contract in the database, and Service A's breaking changes may require Service B to change its internal models. In this scenario, decoupling with contracts and DTOs does not make sense because Service B's purpose is to save data from Service A, and mirroring changes without a pertinent reason can introduce bloatware, extra complexity, and lower the ramp-up for new team members. It can also lead to a business layer that is considered anemic and introduce logic leaks in testing.
While adding layers and contracts everywhere may seem like a good practice, it is important to remember that simpler is sometimes better. Delaying decisions and isolating things that will grow independently can help work smarter, not harder. It is crucial to be aware of the tradeoffs that DTOs add to projects and only use them when necessary.
Disadvantages of Using DTOs:
While DTOs have their benefits, they also have several disadvantages:
The considerations discussed in the previous section apply to any design, regardless of its architecture. However, it can be easier to fall into the trap of over-engineering in a monolithic environment where modules and layers are tightly coupled and the codebase is larger.
In such an environment, there are numerous internal contracts that one needs to deal with, and some people may consider adding layers and contracts everywhere to be a good practice. In fact, there are instances where I have come across CRUD APIs implementing three distinct contracts and layers.
While some individuals may consider duplicating work multiple times to be acceptable, such a practice can lead to unnecessary complexity, such as adding new properties to mappings throughout the codebase or writing tests to guard against the omission of enums.
To avoid such pitfalls, it is important to isolate components that have the potential to grow independently. Coupling your database with your logic is acceptable if it is unlikely to change. In general, simpler designs tend to be better, and delaying decisions until it is necessary, based on project vision and constraints, can help to work smarter, not harder.
It is worth noting that the use of DTOs is not being opposed; rather, the tradeoffs that they introduce to a project should be taken into consideration, and they should only be employed when truly necessary.
Conclusion:
In conclusion, while DTOs may seem like a convenient way to transfer data between layers or services, they often violate the principles of Clean Architecture and can cause more harm than good in the long run. Instead, developers should consider using contracts or domain models to define the data structure and avoid unnecessary mappings and bloatware.
It's important to keep in mind that the specific approach may vary depending on the project requirements and constraints, and ultimately it's up to the developers to decide what's best for their specific use case. However, by understanding the drawbacks of DTOs and the alternatives available, developers can make more informed decisions and create more maintainable and scalable code.