Implementing Data Transfer Objects
1. Creating Request DTOs
| Trait | Detail |
|---|---|
| Purpose | Carry input from API client |
| Naming | Create*Request, Update*Request |
| Validation | Bean Validation annotations |
| No domain logic | Pure data carrier |
Example: Request record with validation
public record CreateOrderRequest(
@NotNull UUID customerId,
@NotEmpty @Valid List<LineRequest> lines,
@Pattern(regexp="^[A-Z]{3}$") String currency
) {}
2. Creating Response DTOs
| Aspect | Guideline |
|---|---|
| Stable contract | Don't expose entity fields directly |
| Include only needed | Avoid leaking internals |
| Format dates | ISO-8601 strings |
| IDs | String or UUID, never internal long |
3. Implementing DTO Mapping
| Approach | Pros | Cons |
|---|---|---|
| Manual | Explicit, debuggable | Boilerplate |
| MapStruct | Compile-time, fast | Annotation processor |
| ModelMapper | Reflection convention | Slow, magical |
| Jackson converters | Inline | Limited |
4. Designing Nested DTOs
Example: Nested response
public record OrderResponse(
String id,
CustomerSummary customer,
List<LineResponse> lines,
MoneyResponse total) {
public record CustomerSummary(String id, String name) {}
public record LineResponse(String productId, int qty, MoneyResponse price) {}
public record MoneyResponse(long cents, String currency) {}
}
5. Using Validation Annotations
| Annotation | Purpose |
|---|---|
@NotNull | Reference must be non-null |
@NotBlank | String non-null and non-empty trimmed |
@NotEmpty | Collection/string non-empty |
@Size(min,max) | Length range |
@Min/@Max | Numeric range |
@Pattern | Regex |
@Email | Email format |
@Valid | Cascade validation |
@Past/@Future | Temporal |
6. Implementing Pagination DTOs
Example: Page response
public record PageResponse<T>(
List<T> items,
int page, int size,
long totalElements, int totalPages,
boolean hasNext, boolean hasPrev) {}
7. Implementing Filter DTOs
Example: Search filter
public record OrderFilter(
@Nullable String status,
@Nullable Instant placedAfter,
@Nullable Instant placedBefore,
@Nullable Long minTotalCents) {}
8. Understanding DTOs vs Domain Models
| Aspect | DTO | Domain |
|---|---|---|
| Purpose | Data transport | Business behavior |
| Validation | Format/syntax | Invariants |
| Lifetime | Per request | Long-lived |
| Coupling | External contract | Internal |
9. Implementing Versioned DTOs
| Strategy | Detail |
|---|---|
| Package per version | v1.OrderResponse, v2.OrderResponse |
| Mapper per version | Domain → versioned DTO |
| Avoid | Adding optional fields without versioning |
10. Handling Null Values
| Approach | Detail |
|---|---|
Jackson @JsonInclude(NON_NULL) | Omit null fields |
| Default values | Empty list/map instead of null |
| Optional | Avoid in DTOs (Jackson quirks) |
| Tri-state | JsonNullable for absent vs explicit null |
11. Implementing Command DTOs
| Trait | Detail |
|---|---|
| Intent | Imperative; "do this" |
| Naming | PlaceOrderCommand |
| Carries | Inputs needed by handler |
| CQRS fit | Write side |
12. Implementing Query DTOs
| Trait | Detail |
|---|---|
| Intent | Interrogative; "give me" |
| Naming | FindOrdersQuery |
| No state change | Pure read |
| CQRS fit | Read side |