The Disadvantages of Using Data Transfer Objects (DTOs) and How to Address Them in Your Codebase
Unsplash

The Disadvantages of Using Data Transfer Objects (DTOs) and How to Address Them in Your Codebase

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

No alt text provided for this image
Unsplash


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:

No alt text provided for this image
Unsplash


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:

No alt text provided for this image
Unsplash


While DTOs have their benefits, they also have several disadvantages:


  1. Bloatware: DTOs can add unnecessary bloatware to a code that should be clean and simple.
  2. Complexity: Using DTOs can introduce extra complexity, which can make it challenging to explain the system's structure or which model to use to new team members.
  3. Maintenance: Over time, DTOs can introduce a business layer that shouldn't exist, which can lead to testing issues and anemic data models.
  4. Decoupling: Using DTOs for decoupling purposes in a microservices architecture does not make sense if it only mirrors changes here and there without a relevant reason.
  5. Mapping: Mapping DTOs can be tedious and time-consuming, particularly if they need to be done for multiple layers or services.


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.

要查看或添加评论,请登录

Marco Antonio Uzcátegui Pescozo的更多文章

社区洞察

其他会员也浏览了