Mastering DTOs in Java: Essential Techniques for Developers
Written on
Introduction to DTOs
When developing a REST API, developers frequently encounter situations where they need to return specific objects requested by users or accept user input to create an object for database storage. Often, these interactions involve models, such as Hibernate entities, which may not always align perfectly with the API's needs. This misalignment might require exposing only a portion of the model or combining data from multiple entities into a single object. This is where the Data Transfer Object (DTO) design pattern becomes essential.
Understanding DTOs
A Data Transfer Object (DTO) is a design pattern that facilitates the transfer of data between processes, particularly in a network environment. It allows developers to define request and response bodies that fit their specific requirements while avoiding the inclusion of unnecessary fields that may bloat the data transmitted.
Crafting DTOs in Java
To illustrate how to create DTOs, let's consider two entities: Book and Author. The assumption here is that each book is authored by a single individual.
@Entity
@Table(name = "books")
@NoArgsConstructor
@Data
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long bookId;
private String title;
private int pages;
private String description;
@ManyToOne
@JoinColumn(name = "author_id")
private Author author;
}
@Entity
@Table(name = "authors")
@NoArgsConstructor
@Data
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long authorId;
private String firstName;
private String lastName;
@OneToMany(mappedBy = "author")
private Set<Book> books;
}
In designing a simple CRUD application, we will implement endpoints for saving books and retrieving all books. However, we aim to avoid unnecessary data retrieval loops, meaning we want to return the Author object without the associated books field. Additionally, the bookId and the complete Author object are not required when saving a new book. Hence, we will create DTOs for this purpose.
The traditional approach to constructing a DTO involves creating a Plain Old Java Object (POJO). While this method is straightforward, it often leads to excessive boilerplate code:
public class BookDTO {
private long bookId;
private String title;
private int pages;
private String description;
private AuthorDTO author;
public BookDTO(long bookId, String title, int pages, String description, AuthorDTO author) {
this.bookId = bookId;
this.title = title;
this.pages = pages;
this.description = description;
this.author = author;
}
public long getBookId() {
return bookId;}
public String getTitle() {
return title;}
public int getPages() {
return pages;}
public String getDescription() {
return description;}
public AuthorDTO getAuthor() {
return author;}
}
For DTOs, setters are unnecessary as these objects should be immutable. While this approach functions correctly, there are more efficient alternatives.
Utilizing Lombok to Reduce Boilerplate
One effective strategy is to leverage the Lombok library, which helps eliminate unnecessary boilerplate code. After applying Lombok, our DTO can be simplified as follows:
@AllArgsConstructor
@Getter
public class BookDTO {
private long bookId;
private String title;
private int pages;
private String description;
private AuthorDTO author;
}
While this is an improvement, there's an even better solution that requires no external libraries. With the introduction of the record feature in Java 14, developers can significantly reduce boilerplate code. When using records, we automatically gain:
- Private, final fields for each data item
- Getters for each field
- A public constructor that corresponds to each field
- An equals method that checks for object equality based on field values
- A hashCode method that generates consistent hash values
- A toString method that provides a string representation of the class and its fields
Given these advantages, records are an ideal choice for DTOs:
public record BookDTO(long bookId, String title, int pages, String description, AuthorDTO author) {}
We can also create a DTO for saving a new book, requiring only the author's ID and excluding the book ID:
public record BookSaveDTO(String title, int pages, String description, long authorId) {}
Using DTOs in the Controller
Here's how we can implement these DTOs within a controller:
@RequiredArgsConstructor
public class BookController {
private final BookService bookService;
@GetMapping
public ResponseEntity<List<BookDTO>> getAllBooks() {
return ResponseEntity.ok(bookService.getBooks().stream()
.map(this::toBookDTO)
.collect(Collectors.toList()));
}
@PostMapping
public ResponseEntity<BookDTO> saveBook(@RequestBody BookSaveDTO bookSaveDTO) {
Book savedBook = bookService.saveBook(bookSaveDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(toBookDTO(savedBook));
}
private BookDTO toBookDTO(Book book) {
Author author = book.getAuthor();
AuthorDTO authorDTO = new AuthorDTO(
author.getAuthorId(),
author.getFirstName(),
author.getLastName()
);
return new BookDTO(book.getBookId(), book.getTitle(), book.getPages(), book.getDescription(), authorDTO);
}
}
And that's a straightforward implementation!
Common Pitfalls to Avoid
- Injecting Business Logic into DTOs: Remember, DTOs should solely handle data transfer and should not contain any business logic. A common mistake is using DTOs in the service layer, which is contrary to their intended purpose.
- Overusing DTOs: Avoid the trap of creating separate DTOs for every single use case or endpoint.
- Using a Single DTO for All Cases: Conversely, avoid the opposite mistake of relying on a single DTO for all scenarios, even if it means including unnecessary fields. Striking the right balance is key.
Conclusion
The Data Transfer Object pattern is widely utilized among developers. While many still rely on traditional POJOs, I hope this discussion has illustrated the superior options available. If you found this article helpful, please show your support with a clap and share your thoughts in the comments. Follow me for more insights, and feel free to visit my personal website for additional resources.