Не знаю как лучше, расскажу как сделано у нас. На клиенте рисуется грид, который запрашивает данные с сервера с заголовком Range: 0-19 (это типа первая страница). Сервер сей заголовок понимает и оборачивает запрос к БД в такой вот вид
select rownum rn, main.*, count(1) over() total from (/*тут основной запрос*/) main where rn >= 0 and rn <= 19;
Вот собссно и вся магия. Т.е. если Вы научите клиент отдавать на сервер диапазон запрашиваемых записей, таким вот образом их можно отдавать. count(1) over() total выберет Вам кол-во записей в выборке всего в отдельную колонку total.
Никаких состояний, редисов и прочих эластиксёрчей.