class Article < ApplicationRecord
scope :ranked, -> { order(score: :desc, id: :desc) }
scope :after_cursor, ->(cursor) do
return all if cursor.nil?
where(
"(articles.score < :score) OR (articles.score = :score AND articles.id < :id)",
score: cursor.score,
id: cursor.id
)
end
def to_cursor
Cursor.new(score: score, id: id)
end
end
require "base64"
class Cursor
attr_reader :score, :id
def initialize(score:, id:)
@score = score
@id = id
end
def encode
payload = [score, id].join(":")
Base64.urlsafe_encode64(payload, padding: false)
end
def self.decode(token)
return nil if token.blank?
raw = Base64.urlsafe_decode64(token)
score, id = raw.split(":", 2)
return nil if score.nil? || id.nil?
new(score: Integer(score), id: Integer(id))
rescue ArgumentError
nil
end
end
class ArticlesController < ApplicationController
PAGE_SIZE = 20
def index
cursor = Cursor.decode(params[:cursor])
records = Article.ranked
.after_cursor(cursor)
.limit(PAGE_SIZE + 1)
.to_a
has_more = records.size > PAGE_SIZE
page = records.first(PAGE_SIZE)
render json: {
data: page.map { |a| { id: a.id, title: a.title, score: a.score } },
next_cursor: has_more ? page.last.to_cursor.encode : nil
}
end
end
class AddRankingIndexToArticles < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_index :articles,
[:score, :id],
order: { score: :desc, id: :desc },
name: "index_articles_on_score_and_id_desc",
algorithm: :concurrently
end
end
When a list is sorted only by a column that has duplicate values — created_at timestamps that collide, a score shared by many rows — the database is free to return tied rows in any order. That non-determinism is invisible until it breaks pagination: a row can appear on two consecutive pages, or be skipped entirely, because the boundary between pages falls in the middle of a tie group. The fix is a deterministic total order, achieved by appending a unique secondary key (usually the primary key) to every ORDER BY so no two rows ever compare equal.
In Article model, the ranked scope encodes this rule: it orders by score DESC and then id DESC so the sort is stable and fully determined. The after_cursor scope implements keyset (seek) pagination against that exact ordering. Its WHERE clause uses row-value comparison logic — a row is "after" the cursor when its score is strictly smaller, OR its score ties and its id is smaller. This lexicographic condition mirrors the two-column ORDER BY precisely; the secondary key is what makes the boundary unambiguous.
The cursor itself is handled in Cursor value object. It packs the tuple [score, id] into an opaque, URL-safe Base64 token so clients treat it as a handle rather than guessable state. encode and decode round-trip the pair, and decode returns nil for malformed input so a bad cursor simply starts from the beginning instead of raising.
ArticlesController wires it together: it decodes the incoming params[:cursor], applies after_cursor when present, fetches one extra row to detect whether more pages exist, and emits a next_cursor built from the last visible record.
Keyset pagination this way is O(1) per page regardless of depth, unlike OFFSET which scans and discards skipped rows. The trade-off is that arbitrary page jumps are not supported — only next/previous relative to a cursor. It also requires a composite index matching the sort, shown in add_index migration as (score DESC, id DESC), so Postgres can satisfy both the ordering and the seek predicate with a single index scan. The common pitfall it avoids is forgetting the tie-breaker: without id in the ORDER BY and the WHERE, duplicate score values silently corrupt the result set.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
Share this code
Here's the card — post it anywhere.