Hybrid Pagination in Rails: Paginating Across Two Prioritized Data Sources
Pagination is simple—until it isn’t. In most apps, paginating one dataset is easy with tools like kaminari or will_paginate. But what if you need to paginate across two datasets, giving one higher priority, and use the second only as a fallback? That’s where hybrid pagination comes in. Here's how I built a clean, scalable hybrid paginator in Ruby. The Problem I had two ActiveRecord scopes: top_scope: High-priority records (e.g. featured or curated) bottom_scope: Regular records (fallback content) The requirements: Combine both into a single, paginated feed Always prioritize top_scope first If the current page isn't filled, pull the remainder from bottom_scope Keep everything performant (no .to_a, no unnecessary loads) Return standard pagination metadata The Solution We built a lightweight Ruby class to handle this logic, using basic SQL pagination (limit and offset) and a clean API. # frozen_string_literal: true class HybridPaginator PER_PAGE = 10 MAX_PER_PAGE = 30 def initialize(top_scope:, bottom_scope: nil, per_page:, page:) raise ArgumentError, 'top_scope must be an ActiveRecord::Relation' unless top_scope.respond_to?(:count) raise ArgumentError, 'bottom_scope must be an ActiveRecord::Relation or nil' unless bottom_scope.nil? || bottom_scope.respond_to?(:count) @page = page.to_i.positive? ? page.to_i : 1 @per_page = per_page.to_i.between?(1, MAX_PER_PAGE) ? per_page.to_i : PER_PAGE @top_scope = top_scope @bottom_scope = bottom_scope end def call top_records = top_scope_paginated remaining = @per_page - top_records.size if remaining > 0 && @bottom_scope bottom_records = bottom_scope_paginated(limit: remaining) top_records + bottom_records else top_records end end def meta { current_page: @page, per_page: @per_page, total_pages: total_pages, count: total_records } end private def total_records @top_scope.count + (@bottom_scope ? @bottom_scope.count : 0) end def total_pages (total_records.to_f / @per_page).ceil end def top_scope_paginated @top_scope.limit(@per_page).offset((@page - 1) * @per_page) end def bottom_scope_paginated(limit:) offset = [(@page - 1) * @per_page - @top_scope.count, 0].max @bottom_scope.limit(limit).offset(offset) end end Where This Is Useful Hybrid pagination is the right solution when you want a single feed, but your records don’t have equal priority. Here are some real-world scenarios:

Pagination is simple—until it isn’t.
In most apps, paginating one dataset is easy with tools like kaminari
or will_paginate
. But what if you need to paginate across two datasets, giving one higher priority, and use the second only as a fallback?
That’s where hybrid pagination comes in. Here's how I built a clean, scalable hybrid paginator in Ruby.
The Problem
I had two ActiveRecord scopes:
-
top_scope
: High-priority records (e.g. featured or curated) -
bottom_scope
: Regular records (fallback content)
The requirements:
- Combine both into a single, paginated feed
- Always prioritize
top_scope
first - If the current page isn't filled, pull the remainder from
bottom_scope
- Keep everything performant (no
.to_a
, no unnecessary loads) - Return standard pagination metadata
The Solution
We built a lightweight Ruby class to handle this logic, using basic SQL pagination (limit
and offset
) and a clean API.
# frozen_string_literal: true
class HybridPaginator
PER_PAGE = 10
MAX_PER_PAGE = 30
def initialize(top_scope:, bottom_scope: nil, per_page:, page:)
raise ArgumentError, 'top_scope must be an ActiveRecord::Relation' unless top_scope.respond_to?(:count)
raise ArgumentError, 'bottom_scope must be an ActiveRecord::Relation or nil' unless bottom_scope.nil? || bottom_scope.respond_to?(:count)
@page = page.to_i.positive? ? page.to_i : 1
@per_page = per_page.to_i.between?(1, MAX_PER_PAGE) ? per_page.to_i : PER_PAGE
@top_scope = top_scope
@bottom_scope = bottom_scope
end
def call
top_records = top_scope_paginated
remaining = @per_page - top_records.size
if remaining > 0 && @bottom_scope
bottom_records = bottom_scope_paginated(limit: remaining)
top_records + bottom_records
else
top_records
end
end
def meta
{
current_page: @page,
per_page: @per_page,
total_pages: total_pages,
count: total_records
}
end
private
def total_records
@top_scope.count + (@bottom_scope ? @bottom_scope.count : 0)
end
def total_pages
(total_records.to_f / @per_page).ceil
end
def top_scope_paginated
@top_scope.limit(@per_page).offset((@page - 1) * @per_page)
end
def bottom_scope_paginated(limit:)
offset = [(@page - 1) * @per_page - @top_scope.count, 0].max
@bottom_scope.limit(limit).offset(offset)
end
end
Where This Is Useful
Hybrid pagination is the right solution when you want a single feed, but your records don’t have equal priority.
Here are some real-world scenarios: