Pagination Implementation
Updated at 1693492047000Overview
Pagination is one of the common features used in almost any software project. There are 3 types of pagination:
- Retrieve all and then paginate: Convenient but slow, capable of displaying the order of records.
- Offset-based pagination: Quite convenient but slow, and as the data grows, it becomes even slower, capable of displaying the order of records.
- Cursor-based pagination: Complex but guarantees performance in all cases, but cannot display the order of records.
EzyPlatform will provide codebase for the 2nd and 3rd options.
Cursor-based Pagination
We need to create the following classes:

- XxxFilter: Contains information about filtering conditions.
- XxxPaginationParameter: Contains information about comparison conditions for pagination.
- XxxPaginationParameterConverter: Used to convert between text and PaginationParameter objects.
- PaginationXxxRepository: Communicates with the database.
- PaginationXxxService: Calls the Repository class and converts Entity to Model.
1. XxxFilter
You should create a base interface that inherits from CommonStorageFilter
to characterize the pagination business logic you want to implement. For example, here I declare the AdminActivityFilter
interface to represent the pagination of admin activity history.
public interface AdminActivityFilter extends CommonStorageFilter {}
Next, you can declare a specific class that implements the base interface. This class will contain the data fields for filtering conditions, and any field with a null value will be ignored.
You also need to implement one or all of the following methods:
selectionFields
: List of selected fields, default is all fields in the table.countField
: The field used for counting in thecount
query.decorateQueryStringBeforeWhere
: Adds to the query before thewhere
keyword, you can usejoin
here.matchingCondition
: The filtering condition for the query after thewhere
keyword.groupBy
: List of fields required for thegroup by
aggregation query, separated by commas.
For example, the AdminAdminActivityFilter
class provides the filtering condition for the pagination of admin activity history.
@Getter public class AdminAdminActivityFilter implements AdminActivityFilter { public final Long adminId; protected AdminAdminActivityFilter(Builder builder) { this.adminId = builder.adminId; } @Override public String matchingCondition() { EzyQueryConditionBuilder answer = new EzyQueryConditionBuilder(); if (adminId != null) { answer.and("e.adminId = :adminId"); } return answer.build(); } public static Builder builder() { return new Builder(); } public static class Builder implements EzyBuilder<AdminAdminActivityFilter> { private Long adminId; public Builder adminId(Long adminId) { this.adminId = adminId; return this; } @Override public AdminAdminActivityFilter build() { return new AdminAdminActivityFilter(this); } } }
2. XxxPaginationParameter
Similar to the filter, you should also create a base interface that inherits from CommonPaginationParameter
to characterize the pagination business logic you want to implement. For example, here I declare the interface AdminActivityPaginationParameter
to represent the pagination of admin activity history.
public interface AdminActivityPaginationParameter extends CommonPaginationParameter {}
Next, you can declare a specific class that implements the base interface. This class will contain the data fields for pagination conditions, and any field with a null
value will be ignored.
You also need to implement one or all of the following methods:
selectionFields
: List of selected fields, default is all fields in the table.decorateQueryStringBeforeWhere
: Adds to the query before thewhere
keyword, you can usejoin
here.paginationCondition
: Pagination condition for the query after thewhere
keyword.groupBy
: List of fields required for thegroup by
aggregation query, separated by commas.orderBy
: Fields and sorting direction after theorder by
keyword.sortOrder
: Sorting direction used to convert from text to the correspondingPaginationParameter
class.
Below is the AdminIdDescAdminActivityPaginationParameter
class, which provides the pagination condition for the pagination of admin activity history.
@Getter @Setter @NoArgsConstructor @AllArgsConstructor public class AdminIdDescAdminActivityPaginationParameter implements AdminActivityPaginationParameter { public Long id; @Override public String paginationCondition(boolean nextPage) { if (id == null) { return null; } return nextPage ? "e.id < :id" : "e.id > :id"; } @Override public String orderBy(boolean nextPage) { return nextPage ? "e.id DESC" : "e.id ASC"; } @Override public String sortOrder() { return AdminActivityPaginationSortOrder.ID_DESC.toString(); } }
3. XxxPaginationParameterConverter
When we call an API from the client, the data can only be in text form. For example, the search query with this keyword:
https://admin.ezyplatform.com/api/v1/admins/me/activities?limit=12&nextPageToken=eyJzb3J0T3JkZXIiOiJJRF9ERVNDIiwidmFsdWUiOnsiaWQiOjQ4ODN9fQ==
The value of nextPageToken
will be the pagination condition, in base64 form. When decoded, we will receive data in text (json) form:
{"sortOrder":"ID_DESC","value":{"id":4883}}
From this text data, we need to convert it back to the corresponding PaginationParameter
object.
When the API is executed, the client will also receive data in this format:
{ "items": [ ... list of items ], "pageToken": { "next": "eyJzb3J0T3JkZXIiOiJJRF9ERVNDIiwidmFsdWUiOnsiaWQiOjQ4NzF9fQ==", "prev": "eyJzb3J0T3JkZXIiOiJJRF9ERVNDIiwidmFsdWUiOnsiaWQiOjQ4ODJ9fQ==" }, "continuation": { "hasNext": true, "hasPrevious": true }, "count": 12, "total": 4737, "timestamp": 1690813511661 }
The next
and prev
values are also pagination conditions in base64
format for the next page and the previous page. To obtain next
and prev
, we also need to convert the PaginationParameter
object to text (json) form and then convert it to base64
form.
The conversion of data between json and the corresponding PaginationParameter
object is the role of the PaginationParameterConverter
class.
For example, we have an enum class:
public enum AdminActivityPaginationSortOrder { ID_DESC }
Which contains the value ID_DESC
, representing the sorting of records by the id
field in descending order. We can create the AdminActivityPaginationParameterConverter
class as shown below to convert between data in text (json) form and the corresponding PaginationParameter
object, specifically the pair of enum AdminActivityPaginationSortOrder.ID_DESC
and the AdminIdDescAdminActivityPaginationParameter
class.
@EzySingleton public class AdminActivityPaginationParameterConverter extends ComplexPaginationParameterConverter<String, AdminActivityHistoryModel> { public AdminActivityPaginationParameterConverter(AdminPaginationParameterConverter converter) { super(converter); } @Override protected void mapPaginationParametersToTypes(Map<String, Class<?>> map) { map.put(AdminActivityPaginationSortOrder.ID_DESC.toString(), AdminIdDescAdminActivityPaginationParameter.class); } @Override protected void addPaginationParameterExtractors(Map<String, Function<AdminActivityHistoryModel, Object>> map) { map.put(AdminActivityPaginationSortOrder.ID_DESC.toString(), model -> new AdminIdDescAdminActivityPaginationParameter(model.getId())); } }
4. PaginationXxxRepository
This class will use Filter and PaginationParameter to create JPQL queries and access the database. You will need to implement the getEntityType
method to return the Entity class. Below is an example of the AdminPaginationAdminActivityRepository
class for the pagination of admin activity history:
- The Id type is
Long
. - The Entity class is
AdminActivityHistory
. - The filter class is
AdminActivityFilter
. - The PaginationParameter class is
AdminActivityPaginationParameter
.
@EzyRepository public class AdminPaginationAdminActivityRepository extends CommonPaginationRepository< AdminActivityFilter, AdminActivityPaginationParameter, Long, AdminActivityHistory > { @Override protected Class<AdminActivityHistory> getEntityType() { return AdminActivityHistory.class; } }
5. PaginationXxxService
This class is responsible for:
- Using the PaginationParameterConverter to convert data between text and PaginationParameter objects.
- Calling the Repository to query the data.
- Converting the data from Entity to Model format.
You will need to implement the following:
serializeToPageToken
: Function to convert PaginationParameter object to text.deserializePageToken
: Function to convert text data to PaginationParameter object.defaultPaginationParameter
: Function to provide the default PaginationParameter when no information about PaginationParameter is provided.convertEntity
: Function to convert data from Entity to Model format.
In the AdminPaginationAdminActivityService
class below for the pagination of admin activity history, we have:
- The Id type is
Long
. - The Entity class is
AdminActivityHistory
. - The filter class is
AdminActivityFilter
. - The PaginationParameter class is
AdminActivityPaginationParameter
. - The Model class is
AdminActivityHistoryModel
.
@Service public class AdminPaginationAdminActivityService extends DefaultPaginationService< AdminActivityHistoryModel, AdminActivityFilter, AdminActivityPaginationParameter, Long, AdminActivityHistory > { private final AdminEntityToModelConverter entityToModelConverter; private final AdminActivityPaginationParameterConverter adminActivityPaginationParameterConverter; public AdminPaginationAdminActivityService( AdminPaginationAdminActivityRepository repository, AdminEntityToModelConverter entityToModelConverter, AdminActivityPaginationParameterConverter adminActivityPaginationParameterConverter ) { super(repository); this.entityToModelConverter = entityToModelConverter; this.adminActivityPaginationParameterConverter = adminActivityPaginationParameterConverter; } @Override protected AdminActivityHistoryModel convertEntity(AdminActivityHistory adminActivity) { return entityToModelConverter.toModel(adminActivity); } @Override protected String serializeToPageToken( AdminActivityPaginationParameter paginationParameter, AdminActivityHistoryModel model ) { return adminActivityPaginationParameterConverter.serialize( paginationParameter.sortOrder(), model ); } @Override protected AdminActivityPaginationParameter deserializePageToken(String value) { return adminActivityPaginationParameterConverter.deserialize(value); } @Override protected AdminActivityPaginationParameter defaultPaginationParameter() { return new AdminIdDescAdminActivityPaginationParameter(); } }
Usage
When using this, we only need to pay attention to the PaginationService and the Filter:
- Step one: Create a filter object.
- Step two: Use the PaginationService to retrieve a data page.
Below is an example:
public class AdminApiAdminActivitiesController { private final AdminPaginationAdminActivityService paginationAdminActivityService; private final AdminCommonValidator commonValidator; @DoGet("/admins/me/activities") public PaginationModel<AdminActivityHistoryModel> adminsMeActivities( @AdminId long adminId, @RequestParam(value = "nextPageToken") String nextPageToken, @RequestParam(value = "prevPageToken") String prevPageToken, @RequestParam(value = "lastPage") boolean lastPage, @RequestParam(value = "limit", defaultValue = "5") int limit ) { commonValidator.validatePageSize(limit); AdminAdminActivityFilter filter = AdminAdminActivityFilter.builder() .adminId(adminId) .build(); return PaginationModelFetchers.getPaginationModel( paginationAdminActivityService, filter, nextPageToken, prevPageToken, lastPage, limit ); } @EzyFeature("admin_management") @DoGet("/admins/{adminId}/activities") public PaginationModel<AdminActivityHistoryModel> adminsAdminIdActivities( @PathVariable(value = "adminId") long adminId, @RequestParam(value = "nextPageToken") String nextPageToken, @RequestParam(value = "prevPageToken") String prevPageToken, @RequestParam(value = "lastPage") boolean lastPage, @RequestParam(value = "limit", defaultValue = "5") int limit ) { commonValidator.validatePageSize(limit); AdminAdminActivityFilter filter = AdminAdminActivityFilter.builder() .adminId(adminId) .build(); return PaginationModelFetchers.getPaginationModel( paginationAdminActivityService, filter, nextPageToken, prevPageToken, lastPage, limit ); } }
Another usage when we do not want to use the default pagination object:
public class ApiContractController { private final WebPaginationAddressService paginationAddressService; private final WebChainValidator chainValidator; private final WebCommonValidator webCommonValidator; private final WebAddressPaginationParameterConverter addressPaginationParameterConverter; @DoGet("/chains/{chainId}/contracts") public PaginationModel<ContractModel> chainsChainIdContractsGet( @PathVariable(value = "chainId") String chainId, @RequestParam(value = "nextPageToken") String nextPageToken, @RequestParam(value = "prevPageToken") String prevPageToken, @RequestParam(value = "lastPage") boolean lastPage, @RequestParam(value = "limit", defaultValue = "15") int limit ) { chainValidator.validateChainId(chainId); webCommonValidator.validatePageSize(limit); AddressFilter filter = new AddressTypeAddressFilter( chainId, AddressType.CONTRACT ); String actualNextPageToken = nextPageToken; String actualPrevPageToken = prevPageToken; String sortOrder = AddressPaginationSortOrder.FIRST_BLOCK_HEIGHT_DESC_ADDRESS_DESC.toString(); if (nextPageToken == null && prevPageToken == null) { if (lastPage) { actualPrevPageToken = addressPaginationParameterConverter.getDefaultPageToken(sortOrder); } else { actualNextPageToken = addressPaginationParameterConverter.getDefaultPageToken(sortOrder); } } return PaginationModelFetchers.<AddressModel>getPaginationModel( paginationAddressService, filter, actualNextPageToken, actualPrevPageToken, lastPage, limit ).map(AddressModel::toContractModel); } }
Offset pagination
Pagination using offset will be much simpler compared to cursor pagination because the offset itself acts as a condition for pagination. Therefore, the number of classes we need to create will be fewer, and the complexity will also decrease significantly.

The only difference is the XxxOffsetPaginationParameter class, while all other classes will be the same as cursor pagination.
1. Create XxxOffsetPaginationParameter class
This class provides offset, the field used for sorting, and the sorting direction, for example:
public class YmProjectOffsetPaginationParameter extends OffsetPaginationParameter { public YmProjectOffsetPaginationParameter(long offset) { super(offset, "e.priority DESC"); } }
2. Create XxxPaginationRepository class
Similar to cursor pagination, for example:
@EzyRepository public class WebYmPaginationProjectRepository extends PaginationRepository< PostFilter, YmProjectOffsetPaginationParameter, Long, Post > { @Override protected Class<Post> getEntityType() { return Post.class; } }
3. Create XxxPaginationService class
Similar to cursor pagination, for example:
@Service public class WebYmPaginationProjectService extends OffsetPaginationService< PostModel, PostFilter, YmProjectOffsetPaginationParameter, Long, Post > { private final WebEzyArticleEntityToModelConverter entityToModelConverter; public WebYmPaginationProjectService( WebYmPaginationProjectRepository repository, WebEzyArticleEntityToModelConverter entityToModelConverter ) { super(repository, YmProjectOffsetPaginationParameter::new); this.entityToModelConverter = entityToModelConverter; } @Override protected PostModel convertEntity(Post entity) { return entityToModelConverter.toModel(entity); } }
Usage
It will be similar to cursor pagination, for example:
private final WebYmPaginationProjectService paginationProjectService; private final WebYmProjectModelDecorator projectModelDecorator; public PaginationModel<WebYmProjectResponse> getProjectPagination( String nextPageToken, int limit ) { PaginationModel<PostModel> pagination = getPaginationModel( paginationProjectService, DefaultPostFilter.builder() .postType(YmPostType.PROJECT.toString()) .postStatus(PostStatus.PUBLISHED.toString()) .build(), nextPageToken, null, false, limit ); return projectModelDecorator.decorate(pagination); }
Some Utility Functions
EzyPlatform provides several utility functions to help you easily create a PaginationParameter
class as follows:
Values.isAllNull
: This function helps check if all values are null.PaginationParameters.makePaginationConditionDesc
: This function helps set up thepaginationCondition
function more easily.PaginationParameters.makeOrderByDesc
: This function helps set up theorderBy
function more easily.
For example:
import static org.youngmonkeys.ezyplatform.pagination.PaginationParameters.makeOrderByDesc; import static org.youngmonkeys.ezyplatform.pagination.PaginationParameters.makePaginationConditionDesc; import static org.youngmonkeys.ezyplatform.util.Values.isAllNull; @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class PriceDescIdDescProductPaginationParameter implements ProductPricePaginationParameter { public BigDecimal price; public Long id; @Override public String paginationCondition(boolean nextPage) { return isEmpty() ? null : makePaginationConditionDesc( nextPage, "price", "id" ); } @Override public String orderBy(boolean nextPage) { return makeOrderByDesc(nextPage, "price", "id"); } @Override public String sortOrder() { return ProductPricePaginationSortOrder.PRICE_DESC_ID_DESC.toString(); } @Override public boolean isEmpty() { return isAllNull(price, id); } }