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 aregetters
)- 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
) {}
- Completeness: The response should be a complete representation of the resource that was just created. Since
userId
andmessageId
are part of theBinaryContent
ās state, they should be in the response. - Confirmation: It explicitly confirms to the client that the server correctly associated the binary content with the user and message they intended.
- 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.
- Never include raw
-
- 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넼 ė°ķķ“ģ¼ķØ
- Any method that creates, updates, or retrieves (find) information about a resource (like
- Use DTOs for All Metadata Operations
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
).
- Password hashes (
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.