Evolve Cursor Pagination Without Breaking Clients
CoinbaseBackend Engineer · OnsiteLast updated
This problem tests the ability to design stable, deterministic cursor-based pagination systems that handle non-unique sort keys and schema evolution without breaking backward compatibility.
The Scenario
You are tasked with fixing a cursor-based pagination system for a transaction service. Currently, the API uses a cursor string which is a base64-encoded timestamp (created_at). Clients consume data using GET /transactions?limit=20&cursor=....
The Problem
- Ambiguity: Many transactions share the exact same millisecond timestamp, leading to missing or duplicate entries during pagination because the database
ORDER BY created_atdoes not guarantee a stable row order. - Requirement: You must add support for sorting by
amount(e.g.,GET /transactions?limit=20&sort=amount&cursor=...). - Constraint: Existing clients must continue to function. You cannot force a breaking change on the cursor format if they are already integrated.
Your Task
- Define a stable composite sort key that resolves the timestamp collision issue.
- Design a versioned opaque cursor format that allows you to embed the sort strategy, ensuring the server can differentiate between old (timestamp-only) and new (timestamp + ID + amount) cursors.
- Write the pseudocode or logic for the database query construction that handles both the old and new cursor formats.
Constraints
- The database contains millions of transactions.
- You must avoid
OFFSETpagination due to performance degradation at high page counts. - Assume
transaction_idis a unique UUID.
Example
- Current Cursor:
MTcxNTg5MzAwMDAwMA==(Base64 of '1715893000000') - Required format: A system that can distinguish between
v1(simple timestamp) andv2(composite key) cursors.
Questions
- How do you construct the SQL
WHEREclause for the newamountsort to ensure no data is skipped when multiple records have the sameamount? - How do you handle the transition period where the database still holds legacy data?
Hints
Hint 1
To achieve stability, always include a unique column like `id` as the final part of your composite sort key.
Hint 2
Prefix your cursor string with a version identifier (e.g., 'v1:' or 'v2:') before base64 encoding to allow the server to parse it correctly.
Hint 3
For the SQL query, use the row tuple comparison syntax: (amount, id) < (cursor_amount, cursor_id) to correctly jump to the next page.
Follow-up questions
- How does your design handle a scenario where a transaction's amount is updated while a user is paginating through the list?
- What are the trade-offs between using a global sequence number versus a composite timestamp-plus-UUID key for pagination?
- How would you implement 'prev' pagination (going backward) given that your cursor contains an inequality condition?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.