Today I will share a real life scenario where I was able to use Java generics for a real life use case. I was tasked with a portal, suppose that it was a student portal. Nothing fancy, just some CRUD’s. So when I was working on it, I found some patterns. Using those patterns I was able to reduce the number of classes. I will also share my thought process behind each improvement I’ve applied.
For start I will create an api to access the student list. Pagination makes sense. Because there might be thousands of students, and displaying them all together will not only create unnecessary load on server and browser, it will be difficult for the user too.
Surely we can send a Pageable request to JPA then get a Page object containing a chunk of students and some other necessary data like total number of items, current page etc. Yes, you can return this object from your service through the controller, but your frontend might not be aligned with the resultant format. So, it’s a good idea to introduce a DTO.
For example, I’m using vuetify in the frontend. I created a StudentPaginatedResponseDto class so that the produced response can be consumed by vuetify’s data table component.
public class StudentPaginatedResponseDto { private int size; private long totalElements; private int totalPages; private List<Student> content; private int currentPage; public StudentPaginatedResponseDto(Page<Student> pageResult){ this.setSize(pageResult.getSize()); this.setTotalElements(pageResult.getTotalElements()); this.setTotalPages(pageResult.getTotalPages()); this.setCurrentPage(pageResult.getPageable().getPageNumber()); this.setContent(pageResult.getContent()); } public StudentPaginatedResponseDto get(){ return this; } }
And the service method looks like this:
@Override public StudentPaginatedResponseDto getPaginatedList(int page, int size) { Pageable pageable = PageRequest.of(page, size); Page<Student> studentsPage = studentRepository.findAll(pageable); return new StudentPaginatedResponseDto(studentsPage).get(); }
It takes page number and size of the page and produces StudentPaginatedResponseDto with content and details.
Suppose, the student list page feature is complete and we want to start working on the teacher list page. Yeah we need a repository, a service that we shouldn’t avoid. We also need this:
@NoArgsConstructor @Data public class TeacherPaginatedResponseDto { private int size; private long totalElements; private int totalPages; private List<Teacher> content; private int currentPage; public TeacherPaginatedResponseDto(Page<Teacher> pageResult){ this.setSize(pageResult.getSize()); this.setTotalElements(pageResult.getTotalElements()); this.setTotalPages(pageResult.getTotalPages()); this.setCurrentPage(pageResult.getPageable().getPageNumber()); this.setContent(pageResult.getContent()); } public TeacherPaginatedResponseDto get(){ return this; } }
Now pause for a second. Think about our next requirement. That could be janitor list page, admins list page, guardian’s list page and so on. Should we write a …PaginatedResponseDto for every type of list? No we shouldn’t. Human brain is a marvelous pattern making machine. With just two examples you definitely can see so many similarities these two classes have. Can we write this class in a generic way so that we can reuse it for our use cases?
Yes we do, we will use generics. We will create a new class like this:
@AllArgsConstructor @NoArgsConstructor @Data public class GenericPaginatedResponseDto<T>{ private int size; private long totalElements; private int totalPages; private List<T> content; private int currentPage; public GenericPaginatedResponseDto(Page<T> pageResult){ this.setSize(pageResult.getSize()); this.setTotalElements(pageResult.getTotalElements()); this.setTotalPages(pageResult.getTotalPages()); this.setCurrentPage(pageResult.getPageable().getPageNumber()); this.setContent(pageResult.getContent()); } public GenericPaginatedResponseDto<T> get(){ return this; } }
Here created that class in a way that it is parameterized over type. So the service method for getting student list becomes:
@Override public GenericPaginatedResponseDto<Student> getPaginatedList(int page, int size) { Pageable pageable = PageRequest.of(page, size); Page<Student> studentsPage = studentRepository.findAll(pageable); return new GenericPaginatedResponseDto<Student>(studentsPage).get(); }
Now we can remove StudentPaginatedResponseDto. Same goes for TeacherPaginatedResponseDto, we can remove this and refactor the service and controller. Even if we need to create a paginated response for another entity, we can reuse this too.
Simple right? Let’s make things a bit complex. Suppose, the Teacher entity contains some things for eg password, that you don’t want to show in api response. For this you can add another Dto like this:
@AllArgsConstructor @NoArgsConstructor @Data public class TeacherSummaryDto { private Long id; private String email; private String phoneNumber; private String joiningDate; private String name; public static TeacherSummaryDto fromEntity(Teacher teacher){ TeacherSummaryDto teacherSummaryDto = new TeacherSummaryDto(); teacherSummaryDto.setId(teacher.getId()); teacherSummaryDto.setEmail(teacher.getEmail()); teacherSummaryDto.setPhoneNumber(teacher.getPhoneNumber()); teacherSummaryDto.setJoiningDate(teacher.getJoiningDate()); return teacherSummaryDto; } }
Now, what can you do?
You can’t pass Page<TeacherSummaryDto> to GenericPaginatedResponseDto’s constructor. At least not in any easy way. You have to create another class extending abstract class Page and implement all the necessary methods. Possible, but overkill for our use case.
Another thing we can do is, refactor GenericPaginatedResponseDto like this:
@AllArgsConstructor @NoArgsConstructor @Data public class GenericPaginatedResponseDto<T, U>{ private int size; private long totalElements; private int totalPages; private List<U> content; private int currentPage; public GenericPaginatedResponseDto(Page<T> pageResult, List<U> contents){ this.setSize(pageResult.getSize()); this.setTotalElements(pageResult.getTotalElements()); this.setTotalPages(pageResult.getTotalPages()); this.setCurrentPage(pageResult.getPageable().getPageNumber()); this.setContent(contents); } public GenericPaginatedResponseDto<T, U> get(){ return this; } }
We are generalizing the type of both Page and content list.
Now, service method for student looks like this:
@Override public GenericPaginatedResponseDto<Student, Student> getPaginatedList(int page, int size) { Pageable pageable = PageRequest.of(page, size); Page<Student> studentsPage = studentRepository.findAll(pageable); return new GenericPaginatedResponseDto<Student, Student>(studentsPage, studentsPage.getContent()).get(); }
And for teacher list:
@Override public GenericPaginatedResponseDto<Teacher, TeacherSummaryDto> getPaginatedList(int page, int size) { Pageable pageable = PageRequest.of(page, size); Page<Teacher> teacherPage = teacherRepository.findAll(pageable); List<TeacherSummaryDto> teacherSummaryDtos = teacherPage.getContent().stream().map(TeacherSummaryDto::fromEntity).collect(Collectors.toList()); return new GenericPaginatedResponseDto<Teacher, TeacherSummaryDto>(teacherPage, teacherSummaryDtos).get(); }
Yea, GenericPaginatedResponseDto<Student, Student> looks pretttty weird. Can we do better?
Still we can.
If we see GenericPaginatedResponseDto’s constructor. We can see, the parameter pageResult only stays within the scope of this constructor. So we can remove the T from class’s type declaration, and place it before the constructor. Just like we write generic functions:
public <T> GenericPaginatedResponseDto(Page<T> pageResult, List<U> contents){ this.setSize(pageResult.getSize()); this.setTotalElements(pageResult.getTotalElements()); this.setTotalPages(pageResult.getTotalPages()); this.setCurrentPage(pageResult.getPageable().getPageNumber()); this.setContent(contents); }
Now, teacher list function looks like this:
@Override public GenericPaginatedResponseDto<TeacherSummaryDto> getPaginatedList(int page, int size) { Pageable pageable = PageRequest.of(page, size); Page<Teacher> teacherPage = teacherRepository.findAll(pageable); List<TeacherSummaryDto> teacherSummaryDtos = teacherPage.getContent().stream().map(TeacherSummaryDto::fromEntity).collect(Collectors.toList()); return new GenericPaginatedResponseDto<>(teacherPage, teacherSummaryDtos).get(); }
One last scope for improvement. See, we don’t really need to prepare the teacherSummaryDtos here. If we take a step back and see what’s happening. Basically we are applying a function to every element of a page’s content. Either it’s for teachers or for student’s or any other entity. If we can pass the function to the GenericPaginatedResponseDto’s constructor. We can do the whole thing there and keep our service method thinner.
Like this:
public <T> GenericPaginatedResponseDto(Page<T> pageResult, Function<T, U> function){ this.setSize(pageResult.getSize()); this.setTotalElements(pageResult.getTotalElements()); this.setTotalPages(pageResult.getTotalPages()); this.setCurrentPage(pageResult.getPageable().getPageNumber()); this.setContent(pageResult.getContent().stream().map(function).collect(Collectors.toList())); }
Now teacher list method is:
@Override public GenericPaginatedResponseDto<TeacherSummaryDto> getPaginatedList(int page, int size) { Pageable pageable = PageRequest.of(page, size); Page<Teacher> teacherPage = teacherRepository.findAll(pageable); return new GenericPaginatedResponseDto<>(teacherPage, TeacherSummaryDto::fromEntity).get(); }
Yes, by using TeacherSummaryDto::fromEntity method reference we are transforming an instance of Teacher to TeacherSummaryDto. We are not transforming the instances of Student, so what should we pass? It’s easy just a lambda like this:
return new GenericPaginatedResponseDto<>(studentsPage, (x)->x).get();
Feel free to provide your feedback for any kind of improvements. Here is the full project: https://github.com/Ruhshan/spring-boot-pagination-generics-example
Happy coding!
Great write up! Your use of Java generics to create a generic paginated response shows efficient coding and problem solving skills. Your thought process behind reducing classes and reusing code is commendable. Keep up the good work!
Thanks a lot vaia for inspiring words. Have a great day!