Data

A design pattern used to transfer data between different layers of an application, most commonly between the service layer and the client/presentation layer. (Layered Architecture)

  • A simple object that should only contain data, with no business logic. Should be immutable conventionally (rarely it can be mutable with setters)
  • can use classes or records (in records, the field is public final and there are getters)
  • It’s conventionally used within the controller layer (⭐Spring MVC)

Request and Response DTOs

Info

A DTO should be designed from the client’s perspective. It must contain exactly the data a specific API endpoint needs to receive or send for a particular use case, and nothing more.

  • Don’t just copy fields from your database Entity. Always start by asking what the client actually needs.

Example

The data a client sends to you is often different from the data you send back.

  • It’s a best practice to create separate DTOs for requests (RequestDto) and responses (ResponseDto
Scenario 1: Creating a new user (POST /users)
  • Client’s Need: ā€œI need to provide a username, email, and password.ā€
    • The client needs to provide the necessary information to create a user. This DTO can also include validation annotations.
  • Your DTO: UserCreationRequestDto
// DTO for data coming FROM the client
public record UserCreationRequestDto(
	@NotBlank String username,
	@Email String email,
	@Size(min = 8) String password
) {}
Scenario 2: Responding after creating the user.
  • Server’s Response: After creation, the server confirms success. The response should include the new user’s ID but never return sensitive information like the password.
// DTO for data going TO the client
public record UserCreationResponseDto(
	UUID id,
	String username,
	String email
) {}

Another example of DTO Response (binarycontent)

/**
 * DTO returned after creating, updating, or finding metadata for binary content.
 * It confirms the resource's state and relationships.
 */
public record BinaryContentResponseDto(
    UUID id,              // The ID of the binary content itself
    String fileName,
    String contentType,   // e.g., "image/jpeg", "application/pdf"
    long size,            // File size in bytes
    UUID userId,          // The user who uploaded it
    UUID messageId,       // The message it's associated with
    Instant createdAt
) {}
  1. Completeness: The response should be a complete representation of the resource that was just created. Since userId and messageId are part of the BinaryContent’s state, they should be in the response.
  2. Confirmation: It explicitly confirms to the client that the server correctly associated the binary content with the user and message they intended.
  3. Convenience: The client immediately gets the full object back. They don’t have to make a second API call to find() the content just to get all its details.

Best practices

  • Separate Metadata from Content
    • Never include raw byte[] data directly in a DTO. JSON is a text format and is inefficient for handling binary data, leading to huge payloads and serialization issues.
    • Use DTOs for All Metadata Operations
      • Any method that creates, updates, or retrieves (find) information about a resource (like create, update, findMetadata) should return a DTO. This provides the client with a complete, predictable, and useful representation of the resource’s state.
      • ģ‘ė‹µģ„ ģ£¼ėŠ” ė©”ģ„œė“œ (create, update, find) ėŠ” 다 dto넼 ė°˜ķ™˜ķ•“ģ•¼ķ•Ø

Protecting Sensitive and Internal Data

  • A primary role of a DTO is to act as a protective barrier for your internal domain model (your Entities). This is a critical security check.
  • Always Omit:
    • Password hashes (user.getPassword()).
    • Internal security flags (user.getLoginAttempts(), user.isBanned()).
    • Internal versioning or metadata (user.getVersion(), user.getLastUpdatedBy()).
    • Any personal information not essential for that specific view (e.g., don’t include a user’s address in a UserSummaryResponseDto).

Handling Relationships (Nested Objects)

1. Flattening (Use an ID)

  • Simple, lean, and fast. The client receives the related object’s ID and can make a separate API call (e.g., /users/{authorId}) if they need more details. This is often the default choice.
  • DTO: BlogPostResponseDto
public record BlogPostResponseDto(
	UUID id,
	String title,
	String content,
	UUID authorId // Just the ID
) {}

2. Nesting (Use another DTO)

  • More convenient for the client, as it prevents them from having to make a second API call. Can lead to larger payloads.
  • DTO: BlogPostResponseDto
public record BlogPostResponseDto(
	UUID id,
	String title,
	String content,
	UserSummaryResponseDto author // Use the summary DTO, not the detail one!
) {}

The best choice depends on how the client will use the data. Usually, for lists, you nest summary DTOs.