Blogg
Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn
Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn
In this blog we dive into the concept of Data Transfer Objects (DTOs), providing a detailed explanation of their use, design best practices, and how they relate to other object patterns like Value Objects (VO), Domain Objects (DO), and Business Objects (BO). You will gain the understanding that the key to effective use of DTOs lies in their purpose: transferring data efficiently while keeping domain logic separate and testable.
It all started with a code review. A developer had added a method in a class that created a hard dependency between several other domain classes and value objects and then exposed everything to the Web UI.
My statement was “That violates the DTO Pattern, move that method to an Assembler instead”. A sentence that for me made perfect sense, since I have read mr Fowlers book on Enterprise Pattern several times.
But that was not the case with all developers. So let us dive into the world of Data Transfer objects, Value Objects and Business Objects.
In the field of programming a data transfer object, DTO, is an object that carries data between processes. The motivation for its use is is that communication between processes is usually done resorting to remote interfaces, e.g. web services, where each call is an expensive operation - Wikipedia
That is the booring definition of a DTO. In many of the object oriented languages I have used, there is a notation of a data class or record that is used to carry around information needed in differnt parts of the application. The DTO encapsulation pattern is not new. In C++ you do it like this:
class PersonDTO {
public:
std::string firstName;
std::string lastName;
int age;
PersonDTO(std::string fn, std::string ln, int a) : firstName(fn), lastName(ln), age(a) {}
};
and in C# it had evolved to this that is a lot less verbose:
public class PersonDTO
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
In Java, we have a bit more code to write to do the same thing.
public class PersonDTO {
private final String firstName;
private final String lastName;
private final int age;
public PersonDTO(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getAge() {
return age;
}
}
but luckly for us, we have Project Lombok so it can be written like this:
import lombok.Data;
@Data
public class PersonDTO {
private String firstName;
private String lastName;
private int age;
}
and since Java 14 (and by the looks of it fully supported in Java 23) the keyword record has been added so that we can write it like this:
public record PersonDTO(String firstName, String lastName, int age) {}
The big difference between the new record and the POJO DTO is that the record is immutable and designed specifically to be passed around as a concise data carrier. We will come back to why this is good later.
Let us look at some of the best practises when designing a DTO.
Many of the patterns used looks similar to each other. Let us look into the difference.
A DTO is used to transfer data between two differnt processes. The Assembly and Disassembly of the DTO is the responsability of each process. There is a cost of creating, transfering and maintaining a DTO that should not be under-estimated.
A Local DTO is a DTO that is not really transfered to another process, but used to mitigate differences
in Domain Models that works on similar data. One common use of a Local DTO is when you need to be multi-threaded.
A L-DTO can then be used, with an internal queue, to eliminate the need for concurrency controlls. The java.util.concurrent
Collections works best over L-DTOs.
Value Object is what is used to represent a concept in a domain model. Where the DTO is used to just transfer data, the Value Object contains domain logic and business rules.
If we look at a Money VO, it contains the business logic that you can’t create a negative denomination.
public class MoneyVO {
private final BigDecimal amount;
private final String currency;
public MoneyVO(BigDecimal amount, String currency) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
this.amount = amount;
this.currency = currency;
}
public BigDecimal getAmount() {
return amount;
}
public String getCurrency() {
return currency;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MoneyVO money = (MoneyVO) o;
return amount.equals(money.amount) && currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
}
The Money DTO don’t need this at all.
import lombok.Data;
import java.math.BigDecimal;
@Data
public class MoneyDTO {
private BigDecimal amount;
private String currency;
}
To convert to and from DTO and VO, we need to create a mapper. Don’t do the misstake of putting this into the VO and DTO classes. Create a MoneyMapper class. This makes the testing easier.
public class MoneyMapper {
// Convert MoneyVO to MoneyDTO
public static MoneyDTO toDTO(Money money) {
return new MoneyDTO(money.getAmount(), money.getCurrency());
}
// Convert MoneyDTO to MoneyVO
public static Money toValueObject(MoneyDTO dto) {
return new Money(dto.getAmount(), dto.getCurrency());
}
}
Now we have come to the Domain Object. Where the DTO is used to transfer Data, the Domain Object represents business concepts or entities of a domain. The domain objects is a part of the domain layer (doh!) and encapsulates rules, behaviours and data. In a way, the DTO can be seen as a subset of the DO.
A domain object is by design mutable and depending on how you mutate it, different things happens. Validation is a big part of the business logic. Another big part is the relationship between domain objects. They are represented by unique identifiers and mapping of relations. This makes the Domain Object a perfect candidate to apply Persistence mappings to, like JPA or Hibernate. You don’t want a Hibernate notation on a DTO.
Now we only have the Business Objects left. As with the Domain Object, it represents a real-world concept. However, they are used in different layers and serve different purposes. While the Domain Objects represents a core business concept, the Business Object represents a Use-Case specific logic that uses the Domain Objects. A Business Object is never persisted, but a product of the Business Object might be, if it produces a Domain Object.
My go-to example is when you create an Order in a WebShop. That one is handled in the OrderService (BO) that orchestrates the Customer (DO) and Order (DO) with the OrderRepository (BO) and the PaymentService (BO) so that when the Order is registered and PaymentDetails (DO) is received, it can send it to the WarehouseService (BO) just to get the information that the items you ordered is shortlisted and will be in stock again after the ski season is over. (Why can’t they fix the Kafka cluster so that the StockSyncronizationService works!? ;) )
Choosing whether to use a DTO or not depends on your system’s design and the specific needs of your application. While DTOs can simplify data transfer across boundaries, it’s crucial to avoid overuse and maintain clear separation of concerns between data transfer, business logic, and domain modeling.
By following best practices and understanding the distinctions between DTOs, Value Objects, and Domain Objects, you as a developer can ensure more maintainable, efficient, and understandable code.