Interview question
Test stable pagination across timestamp ties
Walks API pages through equal sort values and verifies no duplicate or missing resource ids.
TL;DR
Walks API pages through equal sort values and verifies no duplicate or missing resource ids.
Cursor contracts, deterministic tie-breakers, page walking, insert stability, and result reconciliation.
Practice the problem like a real interview: restate, reason, implement, and test.
Seed more equal-timestamp records than fit on one page in an isolated store. Follow nextCursor until null and compare all returned ids with the seeded set. Insert a newer record after page one and verify it does not shift forward traversal.
Twelve records share one timestamp with page size five. Three pages return all twelve ids exactly once.
I test the API as a consumer would: request a page, retain ids, follow the returned cursor, and stop at null. A set comparison catches missing rows while a duplicate assertion catches unstable tie handling.
[Fact]
public async Task Forward_cursor_is_stable_when_a_newer_row_arrives()
{
await using var factory = await ApiTestFactory.WithTiedEventsAsync(count: 12);
var client = factory.CreateAuthenticatedClient("user-1", "events.read");
var seededIds = factory.SeededEventIds.ToHashSet();
var seen = new List<Guid>();
var page = await ReadPageAsync(client, cursor: null);
seen.AddRange(page.Items.Select(item => item.Id));
var newerId = await factory.InsertEventAsync(
factory.SeededTimestamp.AddMinutes(1));
var cursor = page.NextCursor;
for (var pageNumber = 1; cursor is not null && pageNumber < 10; pageNumber++)
{
page = await ReadPageAsync(client, cursor);
seen.AddRange(page.Items.Select(item => item.Id));
cursor = page.NextCursor;
}
Assert.Null(cursor);
Assert.Equal(seededIds.Count, seen.Count);
Assert.Equal(seededIds.Order(), seen.Order());
Assert.Equal(seen.Count, seen.Distinct().Count());
Assert.DoesNotContain(newerId, seen);
}
private static async Task<EventPage> ReadPageAsync(
HttpClient client,
string? cursor)
{
var url = QueryHelpers.AddQueryString(
"/api/events",
new Dictionary<string, string?>
{
["pageSize"] = "5",
["cursor"] = cursor
});
return (await client.GetFromJsonAsync<EventPage>(url))!;
}
This test is cheap with deterministic isolated data. If provider translation determines cursor behavior, use a disposable relational database rather than the in-memory provider.
Next in Contracts & Reliability: Downstream Cancellation