DTO: Un Patrón de Diseño para la Transferencia de Datos
DTO: Un Patrón de Diseño para la Transferencia de Datos

DTO: Un Patrón de Diseño para la Transferencia de Datos

August 31, 2025
5 min read (7 min read total)
1 subpost
index

Data Transfer Objects (DTOs) are one of those patterns that every developer knows superficially, but whose deep mastery marks the difference between functional code and robust architecture.

The Real Problem DTOs Solve

Beyond the textbook definition, DTOs address fundamental architectural problems:

1. Layer Decoupling

DTOs act as stable contracts between application layers. When your presentation layer consumes a DTO, it doesn’t depend on the internal structure of your domain entities. This means you can refactor your domain model without breaking your API.

java
// exposign entity directly
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // Exposes entire entity
}
// using DTO as contract
await app.configure({ key: "api-key-string", version: "2.0" });
@GetMapping("/users/{id}")
public UserDTO getUser(@PathVariable Long id) {
return userMapper.toDTO(userService.findById(id));
}

2. Sensitive Information Control

In real systems, entities contain fields that should never leave the server: hashed passwords, internal tokens, audit metadata. DTOs act as natural filters.

3. Transfer Optimization

A well-designed DTO includes exactly the information the client needs, no more, no less. This is crucial in microservice architectures where every byte counts.

Advanced DTO Patterns

The Specialized DTOs Pattern

Not every endpoint needs the same information. Instead of a monolithic DTO, use specialized ones:

java
// For listings (minimal information)
public class UserSummaryDTO {
private Long id;
private String fullName;
private String email;
private UserStatus status;
}
// For complete details
public class UserDetailDTO extends UserSummaryDTO {
private LocalDateTime createdAt;
private LocalDateTime lastLogin;
private List<RoleDTO> roles;
private AddressDTO address;
}
// For write operations
public class CreateUserDTO {
@NotBlank private String firstName;
@NotBlank private String lastName;
@Email private String email;
@Valid private CreateAddressDTO address;
}

Anemic vs. Behavioral DTOs

Traditionally, DTOs are anemic (data only), but in specific cases, adding behavior can be valuable:

java
public class OrderSummaryDTO {
private List<OrderItemDTO> items;
private BigDecimal subtotal;
private BigDecimal tax;
private BigDecimal total;
// Useful behavior for the client
public boolean hasDiscount() {
return items.stream().anyMatch(OrderItemDTO::isDiscounted);
}
public int getTotalItems() {
return items.stream().mapToInt(OrderItemDTO::getQuantity).sum();
}
}

Common Anti-patterns (And How to Avoid Them)

1. DTO Proliferation

Problem: Creating a DTO for every entity automatically. Solution: Create DTOs only when they add real value. Not every entity needs a DTO.

2. Fat DTOs

Problem: DTOs that include all information “just in case.” Solution: Design DTOs for specific use cases, not per entity.

3. Mapping Hell

Problem: Verbose manual mapping between entities and DTOs. Solution: Use tools like MapStruct, but maintain control over mapping:

@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "fullName", expression = "java(user.getFirstName() + ' ' + user.getLastName())")
@Mapping(target = "passwordHash", ignore = true)
UserDTO toDTO(User user);
@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", ignore = true)
User toEntity(CreateUserDTO dto);
}

DTOs in Modern Architectures

Microservices and DTOs

In microservice architectures, DTOs become even more critical. They represent contracts between services and their versioning is crucial:

// Explicit versioning for compatibility
public class UserDTOV1 {
private Long id;
private String name;
}
public class UserDTOV2 extends UserDTOV1 {
private String firstName;
private String lastName;
// Maintain backward compatibility
@Override
public String getName() {
return firstName + " " + lastName;
}
}

Event-Driven Architecture

DTOs also shine as event payloads:

public class UserCreatedEventDTO {
private Long userId;
private String email;
private LocalDateTime createdAt;
private String createdBy;
// Sufficient information for other services to react
// without additional queries
}

Best Practices Based on Experience

1. Immutability by Default

DTOs should be immutable whenever possible. Reduces bugs and facilitates testing:

@Value // Lombok for immutability
public class ProductDTO {
Long id;
String name;
BigDecimal price;
CategoryDTO category;
}

2. Validation in the Right Place

Validation should be on input DTOs, not output ones:

public class CreateProductDTO {
@NotBlank(message = "Name is required")
@Size(max = 100, message = "Name too long")
private String name;
@NotNull(message = "Price is required")
@DecimalMin(value = "0.01", message = "Price must be positive")
private BigDecimal price;
}

3. Documentation as Code

DTOs are living documentation of your API. Use them well:

/**
* Represents a user in the system for external consumption.
* Excludes sensitive information like password hashes.
*/
public class UserDTO {
/** Unique identifier for the user */
private Long id;
/** User's display name (first + last name) */
private String displayName;
/** Primary email address - used for notifications */
private String email;
}

Testing DTOs Effectively

DTOs deserve proper testing, especially their mapping logic:

@ExtendWith(MockitoExtension.class)
class UserMapperTest {
@InjectMocks
private UserMapperImpl userMapper;
@Test
void shouldMapUserToDTO() {
// Given
User user = User.builder()
.id(1L)
.firstName("John")
.lastName("Doe")
.email("john@example.com")
.passwordHash("secret-hash")
.build();
// When
UserDTO dto = userMapper.toDTO(user);
// Then
assertThat(dto.getId()).isEqualTo(1L);
assertThat(dto.getFullName()).isEqualTo("John Doe");
assertThat(dto.getEmail()).isEqualTo("john@example.com");
assertThat(dto.getPasswordHash()).isNull(); // Sensitive data excluded
}
}

Performance Considerations

Lazy Loading and DTOs

DTOs can help avoid N+1 problems by being explicit about what data is needed:

// Instead of letting Hibernate load everything
public class PostSummaryDTO {
private Long id;
private String title;
private String authorName; // Pre-loaded, not lazy
private int commentCount; // Calculated at query time
private LocalDateTime publishedAt;
}
// Repository method that loads exactly what's needed
@Query("SELECT new com.example.PostSummaryDTO(p.id, p.title, p.author.name, " +
"SIZE(p.comments), p.publishedAt) FROM Post p WHERE p.status = 'PUBLISHED'")
List<PostSummaryDTO> findPublishedPostSummaries();

Caching DTOs

DTOs are perfect for caching since they’re immutable and contain exactly what clients need:

@Service
public class UserService {
@Cacheable(value = "userSummaries", key = "#userId")
public UserSummaryDTO getUserSummary(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
return userMapper.toSummaryDTO(user);
}
}

Sentheon.com

Sentheon is a TI consulting firm specializing in cloud solutions and DevOps practices.

  • info@sentheon.com

© 2025 Sentheon. All rights reserved.