Skip to content

Build1 publisher3 min readPublished

Freeze the pagination contract before the agent writes the list handler

Offset paging passes a static test suite and then skips rows the moment a merchant inserts a product mid-read. A dev.to case study answers that by freezing the sort key, tie-breaker and error codes in a spec file the agent cannot edit.

The Engineer · Build desk

Illustration accompanying Freeze the pagination contract before the agent writes the list handler

What happened

  • The first agent draft reached for ORDER BY name LIMIT 10 OFFSET n and stayed green against a happy-path test that seeded twenty static rows and walked page one and page two.
  • Page sizes above ten return 400 instead of being clamped, and a truncated cursor returns 400 with error code invalid_cursor rather than an empty page.
  • The author labels the published test suite a worked example, unexecuted against a database until the reader wires a test harness.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure The offset defect needs a concurrent writer to appear, so a suite of static seeds ships it green and the buyer paging the catalog is the one who finds the missing row.
  • constraint With sort key, tie-breaker, null order and error codes frozen, a prompt that widens page size or adds a count has to be argued as a spec change, and review is a diff against a table.
  • decision Anyone adopting this has to settle up front whether the client can live without a total count, because the study makes adding one a separate spec change.
  • cost All of it is written before any code exists: a decision table plus a test that fails on OFFSET under a concurrent insert, written by the humans who own the endpoint.

Offset paging recounts from the start of the ordering on every request. The case study on dev.to calls it a moving window over a live table, not a snapshot of a result set [1]. Insert a row before the current offset and everything after it moves down one position, so the next page starts one row late [2]. Delete a row and later rows move up, so the next page repeats one the reader already scanned [3]. The example failure is a merchant adding "Aardvark Adapter" while a buyer walks the name ordering [6].

The first draft in the study used ORDER BY name LIMIT 10 OFFSET n, which the author puts down to the pattern appearing in every tutorial [4]. That is a fair description of how a generator picks a default. The seed data in the passing test does not change between page one and page two [5].

The contract replaces that with keyset paging, seeking on (name ASC, id ASC), and it names id as the tie-breaker, required to be unique and immutable [8][9]. The endpoint is GET /products with a page size of ten and a stable next-page token [26]. The acceptance condition is written as an equality: two readers holding the same cursor, at the same committed table state, get the same next page [22]. The study's first test seeds two products called Clamp with different ids and asserts they come back in id order, not in insert order [25].

Doing this before generation matters because the handler is where the contract ends up living. Once the handler exists, the wrong contract is already encoded in it, and no later prompt removes it [7]. So the rules sit in a spec file the agent may not edit, and the tests import that file instead of restating policy in comments [18]. The agent may change queries, encodings and variable names; it may not change sort keys, tie-breakers, null order or error codes [19]. A later prompt asking for page=3 is rejected instead of extending the handler [23]. Generation is step four of five [21].

Every ambiguous input has one named answer. Ask for a page size above ten and the server returns 400 instead of clamping to ten [10]. A truncated cursor returns 400 with error code invalid_cursor, never an empty page [12]. When a well-formed cursor names an id that no longer exists, the handler skips forward from that sort position instead of returning 404 [13]. Null names sort last ascending and first descending [14]. The table lives next to the tests [29].

The cursor is described as opaque, and it is base64url of JSON carrying the name, the id and the direction [11]. Decode it and the sort position comes back in plain text [28].

The author labels the published suite a worked example, unexecuted against a database until the reader wires a harness [24]. Its assertions therefore state intended behaviour. The decision table covers inserts, deletes, equal names and bad cursors [20]; a rename that moves a product from after the cursor to before it is not among the listed cases [27].

Two things have to hold for this contract to transfer to another table. The tie-breaker has to be genuinely unique and immutable, because the cursor stores its value and the query looks for rows after it [9][11]. And the client has to accept no total count, since the study treats adding one as a separate spec change [16]. If your list view shows page 3 of 12 today, you will have to renegotiate the count row.

What to watch

  • Whether anyone runs the published suite against a real database and reports which assertions fail.
  • Whether the contract gains a row for renames, where a product's name moves it across an issued cursor position.
  • Whether the no-total-count rule survives the first client that wants a page-3-of-12 control.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories