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) and query.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/endRow or page/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