What This Post Covers
This note collects two things I learned in a situation where I wanted to move an already-built feature to another project but did not want to copy the code verbatim. The feature was "a virtualized infinite-scroll list (drawing only the rows visible on screen) + position that is restored even after going back or reloading".
The first is how to move it — the clean-room approach of writing down "how does this feature behave from the outside" as a contract rather than the implementation code. The second is the core data problem of infinite scroll that I reconfirmed along the way — whether you slice the list with offset or with keyset (cursor). There is no talk of any specific company or product, only general concepts that apply anywhere.
Day Summary
| Task | What I wanted to do | What I did | Result |
|---|---|---|---|
| Clean-room contract | Move a feature to another stack without copying code | Left out the implementation code, documented only "observable behavior + failure guarantees" as a contract | The receiving side isn't tied to library choices |
| Keyset pagination | Keep infinite scroll from breaking on insert/delete | Laid out the problems with offset and replaced it with a bidirectional cursor contract | Duplicates & gaps removed, consistent performance even on deep pages |
1. Move the Behavior, Not the Code — the Clean-Room Approach
The situation. I wanted to move a well-working feature to another project, but that project might use different libraries than we do. Copy-pasting the code as-is forces our tool choices (a specific data-fetching library, a specific virtualization library) on them.
The core concept. A clean-room migration is an approach where you copy not a single line of the original code and instead hand over only a document of "how this feature behaves from the outside", so the other side reimplements it from scratch. The distinction matters here: a capability is the observable behavior of "it scrolls infinitely and restores position when you go back", while tooling is the specific libraries that achieve it. If you write down only the capability, the tool choice is up to the receiving side. As a cooking analogy, "a warm soup dish" is the capability and "this specific pot" is the tooling — you hand over the recipe (the capability) and do not carry off someone else's kitchen tools.
What I did. I decomposed the feature into a behavior contract across four layers: (1) UI/virtualization — keep only the visible rows in the DOM and decide row height in the order measured value → stored height → average estimate, (2) domain/model — a bidirectional page array and a memory cap, (3) transport/API — query assembly and throw-on-failure, (4) restoration — the snapshot schema and the restore decision. Each contract item got a "verified directly / observed / inferred / unknown" label so the reader can tell verified facts apart from guesses. I put in not a single block of implementation code, and instead spelled out "what does it guarantee if it fails" (failure scenarios) and "this it does not do" (non-goals).
The lesson. The heart of a feature-migration document is not the code we wrote but the observable behavior + failure guarantees. Attaching a confidence label to each piece of evidence lets the receiving side manage risk.
2. Infinite-Scroll Stability Comes From Pagination
The situation. You keep appending to a list downward (or upward), and when a new post is added or one is deleted in the meantime, the list gets out of sync. The last post you just saw shows up again (a duplicate), or a post that was there gets skipped (a gap).
The core concept. The cause is usually the pagination method. Offset pagination requests a page by a count-to-skip, like "10 starting from the 11th" (LIMIT 10 OFFSET 10). It slows down the further back you go because it counts all the preceding rows (O(offset)), and when the list changes in real time, items shift and get duplicated or skipped. Keyset (cursor) pagination uses the last-seen value as a coordinate, like "give me everything after the sort key of the last post I saw". It finds that point directly via the index so it is fast (O(log n)), and even with inserts and deletes in between, "after this value" does not change, so it is stable. As a book analogy, offset is the page number (if someone slips a page in the middle, it goes out of sync); keyset is the bookmark (you keep reading from the last sentence you read).
What I did. I laid out the infinite-scroll API as a cursor-based bidirectional contract. Scroll up uses the before (newer) cursor, scroll down uses the after (older) cursor, and the first request starts with no cursor. A bad cursor / out-of-range limit does not silently fall back to the first page but fails immediately with 400, so the client learns it lost its position. Ever-accumulating pages get a cap on retained count so memory stays bounded.
The lesson. Most of an infinite list's stability comes down to offset vs keyset. The way to keep from hiding a lost position is to fail fast on bad input rather than swallowing it silently.