Offset/Limit Pagination for JSON-RPC Services
Axis2 provides a generic pagination framework for JSON-RPC services
backed by SQL databases. The two classes —
PaginationRequest and PaginatedResponse<T> —
map directly to JPA/Hibernate's setFirstResult(offset) and
setMaxResults(limit) pattern.
Package: org.apache.axis2.json.rpc
Wire Format
A paginated response wraps the result list with metadata:
{
"response": {
"data": [ ... ],
"pagination": {
"offset": 0,
"limit": 50,
"totalCount": 1247,
"hasMore": true
}
}
}
- offset — zero-based index of the first item in this page
- limit — maximum items requested (page size)
- totalCount — total items matching the query across all pages
- hasMore — true when
offset + limit < totalCount
Service Integration
A typical service method delegates offset/limit to the DAO:
public PaginatedResponse<Product> findProducts(ProductQuery query) {
List<Product> items = dao.findList(query.getOffset(), query.getLimit());
long total = dao.count(query);
return PaginatedResponse.of(items, query.getOffset(), query.getLimit(), total);
}
The request POJO can embed PaginationRequest fields directly
or accept them as separate parameters:
// Client sends:
{
"searchTerm": "AAPL",
"offset": 100,
"limit": 50
}
Unpaginated Responses
For small lookup tables (e.g., a list of 15 departments), use the
convenience factory to wrap the full list with hasMore=false:
return PaginatedResponse.unpaginated(departments); // → offset=0, limit=15, totalCount=15, hasMore=false
Safety: maxLimit Clamping and Input Validation
PaginationRequest enforces safety constraints at the getter level:
| Input | Behavior |
|---|---|
offset < 0 |
Clamped to 0 |
limit <= 0 |
Default: 50 |
limit > maxLimit |
Capped at maxLimit (default: 2000) |
Services that handle expensive entities can lower the cap per-operation:
// Large text fields — cap at 100 per page request.setMaxLimit(100); int safeLimit = request.getLimit(); // capped at 100
Frontend Patterns
Page Controls ("Showing 151–200 of 1,247")
// JavaScript / TypeScript
const { offset, limit, totalCount } = pagination;
const currentPage = Math.floor(offset / limit) + 1;
const totalPages = Math.ceil(totalCount / limit);
const showingFrom = offset + 1;
const showingTo = offset + data.length;
Virtual Scroll / Infinite Scroll
// Load next chunk when user scrolls
const nextOffset = pagination.offset + pagination.limit;
if (pagination.hasMore) {
fetchPage(nextOffset, pagination.limit);
}
Grid startRow/endRow Translation
// Grid sends startRow=300, endRow=350 // Service translates: offset = startRow, limit = endRow - startRow int offset = startRow; int limit = endRow - startRow;
Why Offset/Limit Instead of Cursor
- DAO compatibility — existing Hibernate/JPA DAOs use
query.setFirstResult(offset)andquery.setMaxResults(limit). Cursor pagination requires a stable sort key and stateful server-side tokens. - Frontend grids — data grids (AG Grid, React Table, etc.) natively
speak offset/limit via
startRow/endRoworpage/pageSize. - totalCount — enables "Showing 1–50 of 1,247" UI patterns
and page-count calculations. Cursor APIs typically omit total counts because
they are expensive for the cursor model, but they are cheap when the DAO
already runs
SELECT COUNT(*).
Test Coverage
The PaginatedResponseTest class provides 20 tests covering:
- First page, last page, partial last page, single page, empty result
- Null data treated as empty list
- Unpaginated convenience factory
- Negative offset clamping, zero/negative limit defaults, maxLimit enforcement
- Enterprise scenarios: 8,543-item virtual scroll, soft-delete filtering, service-specific maxLimit, grid startRow/endRow translation
- Request → response round-trip simulation
Apache Axis2
