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 finaland there aregetters)- It’s conventionally used within the controller layer (⭐Spring MVC)
- 🐣Spring & SpringBoot
- For mapping, we use mapper and mapstruct
- Baeldung - The DTO Pattern (Data Transfer Object)
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
userIdandmessageIdare 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.
Classes & Inner Classes
Tip
A great DTO (Data Transfer Object) design pattern, especially for larger applications, is to group related
RequestandResponsemodels as static inner classes within a shared outer class.
- 어플리케이션이 커지면 이게 더 유용해짐
- gives clarity
- clearly namespaces the DTOs, preventing class name conflicts (e.g., you can have
User.Request,Order.Request, etc.).
- clearly namespaces the DTOs, preventing class name conflicts (e.g., you can have
- reduces file clutter in your project directory
Example
public class ReadStatusDto {
@Schema(description = "메시지 읽음 상태 생성 요청")
public record ReadStatusRequest(
@Schema(description = "사용자 ID", example = "123e4567-e89b-12d3-a456-426614174000", requiredMode = RequiredMode.REQUIRED)
UUID userId,
@Schema(description = "채널 ID", example = "a1b2c3d4-e5f6-7890-1234-567890abcdef", requiredMode = RequiredMode.REQUIRED)
UUID channelId,
@Schema(description = "마지막으로 읽은 시각", example = "2025-07-10T10:41:52Z", requiredMode = RequiredMode.REQUIRED)
Instant lastReadAt
) {
}
@Schema(description = "읽음 상태 정보 응답")
public record ReadStatusResponse(
UUID id,
UUID userId,
UUID channelId,
Instant lastReadAt,
Instant createdAt,
Instant updatedAt
) {
}Validation
- DTOs in controllers
- It is considered an anti-pattern to wrap a request body DTO in an
Optionalin your controller handler. - Instead of using
Optionalto handle a potentially missing request body, you should let the framework handle it. A missing or non-deserializable body will correctly result in a400 Bad Requesterror. For validating the fields inside the DTO, use validation annotations.
- It is considered an anti-pattern to wrap a request body DTO in an
- related
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record CreateUserRequest(
@NotBlank(message = "Username cannot be blank.") // << validation
@Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters.")
String username,
@NotBlank(message = "Email cannot be blank.") // << validation
@Email(message = "Email should be valid.")
String email
) {}The Anti-Pattern
@RestController
@RequestMapping("/users-optional")
public class OptionalUserController {
@PostMapping
public ResponseEntity<String> createUser(@RequestBody Optional<CreateUserRequest> request) {
// This is unnecessary boilerplate code.
if (request.isEmpty()) {
return ResponseEntity.badRequest().body("Request body is missing.");
}
CreateUserRequest user = request.get();
// You still haven't validated the fields inside the DTO.
// You would need to add more manual checks here.
System.out.println("Creating user (optional): " + user.username());
return ResponseEntity.ok("User created successfully (the hard way).");
}
}- BAD
- You are manually handling a case (missing body) that the framework is designed to handle for you
- It doesn’t automatically validate the fields (
username,email). You would have to trigger the validation manually, which defeats the purpose of the annotations
Correct
@RestController
@RequestMapping("/users-valid")
public class ValidUserController {
@PostMapping
public ResponseEntity<String> createUser(@Valid @RequestBody CreateUserRequest request) {
// If the code reaches this point, you can be certain that:
// 1. 'request' is not null.
// 2. All validation rules (@NotBlank, @Email, etc.) have passed.
// The controller logic is clean and focused on its job.
System.out.println("Creating user (valid): " + request.username());
return ResponseEntity.ok("User created successfully (the right way).");
}
}@RequestBodytells Spring to deserialize the JSON body into theCreateUserRequestobject.@RequestBodytriggersHttpMessageConverterto handle conversion → JSON to instance of the DTO (CreateUserRequest)
@Validthen triggers the validation process on that object.