Ordering Keys for Reorderable Lists: Fractional Indexing, LexoRank, and Sequence CRDTs
Date: 2026-04-12
Category: knowledge
Domain: software / data modeling / collaboration / editors / kanban / realtime systems
1) Why this topic matters more than it looks
A surprising number of products eventually need user-defined ordering:
- kanban cards and backlog priority
- playlists and queues
- CMS blocks and sections
- design-tool layers
- whiteboard objects
- document paragraphs or outline nodes
- database-backed drag-and-drop lists
The naive version looks trivial:
- item A has
position = 1 - item B has
position = 2 - item C has
position = 3
Then real life arrives:
- users drag items constantly
- only one item moved, but ten thousand rows need rewriting
- concurrent inserts happen in the same gap
- floats lose precision
- order keys grow over time
- collaborative editing interleaves two users’ runs into nonsense
- operators need a repair story when rank data gets weird
This is one of those “small schema choice, large system consequences” problems.
2) The short version
If you only need the decision fast:
- single-user or lightly concurrent admin UI -> integers with gaps or periodic rebalance can be enough
- server-authoritative drag-and-drop list / canvas / layers -> fractional indexing is usually the sweet spot
- very large operational backlog with explicit maintenance tooling -> LexoRank-style string ranks are battle-tested
- true local-first / peer-to-peer collaborative ordered text or blocks where adjacency matters -> sequence CRDT / tree-based indexing / RGA-family
- plain floating-point ranks for long-lived heavy reordering -> usually a trap
Pocket rule:
If you care mostly about cheap reordering, use fractional indexing.
If you care mostly about concurrent textual adjacency, use sequence CRDTs.
If you care mostly about operational backlog ranking at scale, LexoRank-style schemes are worth it.
3) What you are actually optimizing
Before picking a scheme, answer these five questions:
- How many rows may need rewriting per move?
- Can multiple users reorder concurrently?
- Must sequential inserts from the same user stay contiguous under concurrency?
- Will clients work offline / merge later?
- Do operators need repair / rebalance tooling for pathological cases?
Most ordering systems are trading among five things:
- write amplification
- key growth
- merge simplicity
- adjacency correctness under concurrency
- operational repairability
There is no universal winner.
4) Option A: dense integers
The model
Store positions as integers:
1, 2, 3, 4, 5
To insert in the middle, rewrite all subsequent rows.
Why people start here
- simple to understand
- easy DB sorting
- friendly for reports and debugging
- no weird key formats
Why it breaks down
Move one item near the top of a long list and you may need to rewrite a large suffix.
That means:
- more DB writes
- more lock contention
- more cache invalidation
- bigger sync payloads
- larger multiplayer diffs
For frequently reordered lists, this is the wrong default.
When it is still fine
- tiny lists
- rare reordering
- strong transactional DB, low contention
- internal tooling where simplicity beats elegance
5) Option B: integers with gaps
The model
Start with spaced positions instead of dense ones:
100, 200, 300, 400
Insert between 100 and 200 by assigning 150.
This reduces rewrites for a while.
Why it helps
- simple mental model
- cheap inserts until gaps close
- sortable with normal numeric indexing
Why it is temporary, not permanent
Eventually hotspots form. If users keep inserting between the same neighbors, you run out of room and need a rebalance.
Steve Dignam’s practical comparison makes the core point clearly: gap schemes reduce blast radius, but do not eliminate the need for occasional re-spacing.
Best fit
- low-to-medium reorder frequency
- centralized server
- you are okay with a background “repack ranks” job
Bad fit
- hot collaborative stacks
- long-lived lists with constant inserts near the same region
- systems where maintenance jobs are annoying or risky
6) Option C: floating-point / decimal fractional positions
The model
Store a position between neighbors by averaging:
- between
1and2->1.5 - between
1and1.5->1.25
This is the first form of fractional indexing most people discover.
Why it is attractive
- very cheap reordering
- only moving items change
- dead simple to prototype
- great for drag-and-drop demos
The problem: finite precision is real
With IEEE-754 doubles, repeated midpoint insertion eventually collapses.
You do not have infinite distinct values between two floats in practice.
Steve Ruiz shows the practical failure mode nicely: after enough repeated splits between two nearby values, the next “between” result stops being strictly between.
That means your ordering invariant can silently die.
Arbitrary precision decimals help, but...
Arbitrary precision numeric/decimal types extend the runway a lot. They are much better than plain doubles. But they are still fundamentally a precision budget system, not an actually unbounded identifier scheme.
For many apps that is enough. For long-lived heavy-reordering systems, it is usually not what I would choose.
Recommendation
- Do not use plain floats as long-lived canonical ordering keys for serious systems.
- Fine for prototypes, toys, or short-lived local state.
- If you want durable fractional ordering, move to string-based fractional keys or a more explicit ranking scheme.
7) Option D: string-based fractional indexing
The model
Instead of numbers, use sortable strings that can always generate a new key between two existing keys.
Figma’s write-up is the clean practical reference here:
- maintain child order by sorting on an index
- generate a new index between neighbors
- use arbitrary-precision fraction-like string manipulation instead of fixed-size floats
- keep values between lower and upper sentinels
In Figma’s implementation, order keys are stored as strings and midpoint generation is done with string operations, avoiding float precision exhaustion.
Why this is such a strong default
For server-authoritative ordered objects such as:
- layers
- blocks
- cards
- nodes
- whiteboard objects
string fractional indexing gets you most of what you want:
- cheap single-item reorder
- simple sort semantics
- no finite float collapse
- relatively easy implementation
- natural DB storage as text/varchar
Important caveat #1: key length grows
The cost you pay is identifier growth. If users keep inserting in the same tight gap, the generated strings can get longer.
In many real apps this is acceptable. But it is not “free forever.”
Operationally, you should expect one of these strategies:
- tolerate moderate growth
- periodically compact / rebalance
- reassign keys inside a transaction during maintenance windows
Important caveat #2: concurrent runs may interleave
Figma explicitly notes the trade-off:
- for design-layer ordering, interleaving concurrent inserts is usually acceptable
- for text editing, it usually is not
That is the key boundary.
If two users concurrently insert runs at the same location, simple fractional ordering may converge to a stable order that interleaves those runs. For design objects this is often fine. For paragraphs or text, it is ugly.
Important caveat #3: tie-breaking still matters
If two clients generate the same position between the same neighbors, you still need a deterministic uniqueness or server tie-break rule.
Figma’s article describes server-side tie handling for identical positions. Do not skip this in a multi-client system.
Best fit
- centralized collaboration
- server-authoritative data
- drag-and-drop lists
- layer stacks
- canvases
- block-based editors where occasional interleaving is acceptable or rare
Bad fit
- character-level collaborative text
- paragraph/block editors where preserving one user’s inserted run as a contiguous run is crucial
- fully offline peer-to-peer merge-heavy editing without a strong central tie-breaker
8) Option E: LexoRank-style schemes
The model
LexoRank is Atlassian’s ranking system used for Jira issue ordering. At a high level, it is a string rank approach designed for scalable sortable issue prioritization, along with maintenance and integrity tooling.
Even without depending on every internal detail, Atlassian’s documentation reveals the operational flavor:
- ranking data lives in buckets / ranges
- balancing can be required
- integrity checks matter
- operators may need to inspect marker rows, duplicates, out-of-bounds values, and balance state
That tells you something important:
LexoRank is not just an algorithm. It is an algorithm plus an operational maintenance model.
Why people use it
- good fit for very large ordered backlogs
- string ordering avoids plain numeric precision issues
- supports frequent inserts between neighbors
- comes with a mature “this may need balancing” worldview
What makes it different from generic fractional indexing
Generic fractional indexing says:
- generate sortable in-between keys
- maybe compact later if needed
LexoRank-style systems say:
- generate sortable in-between keys
- structure the rank space deliberately
- expose balancing and integrity tooling as first-class operator concerns
That is a meaningful difference in enterprise systems.
Trade-offs
Pros:
- battle-tested for backlog ranking
- operationally explicit
- string-based, not float-based
Cons:
- more complex than basic fractional indexing
- not the simplest thing to explain to every engineer
- you should expect maintenance procedures, not magical self-perfection
Best fit
- issue trackers
- ticket backlogs
- enterprise list ranking
- systems where operators want explicit repair visibility
My practical view
If you are not building “Jira-like ranked backlog at scale,” you probably do not need full LexoRank complexity. Basic string fractional indexing is often enough.
But if ranking is a core product primitive with years of operational life, LexoRank-style thinking is worth studying.
9) Option F: sequence CRDTs / tree-based indexing
The model
When collaboration is not just “many users talking to one server” but closer to:
- local-first
- peer-to-peer
- offline editing
- merge-later replication
- adjacency-sensitive text/block ordering
then the problem changes.
Now you are not just assigning sortable ranks. You are designing a merge semantics for ordered sequences.
This is where sequence CRDT families show up:
- RGA
- Logoot / LSEQ
- tree-based indexing variants
- text-buffer / block-sequence CRDT designs
Why simple fractional indexing stops being enough
Bartosz Sypytkowski’s write-up on non-interleaving LSeq explains the classic pain point:
- two peers insert text concurrently at the same cursor position
- deterministic fractional key generation plus peer tie-breaks can produce convergence
- but the characters or blocks become interleaved
That is “technically correct, user-visible nonsense.”
For text or paragraphs, adjacency is part of meaning. Interleaving destroys intent.
Evan Wallace’s tree-based indexing insight
Evan Wallace presents a useful middle ground for ordered objects under collaborative concurrency:
- insert new objects relative to the item before the insertion point
- maintain order via tree traversal + insertion counters
- this avoids interleaving of concurrently inserted runs when adjacency matters
The key benefit is subtle but big:
Concurrent runs inserted in the same place stay as runs, instead of becoming alternating soup.
That makes tree-based or adjacency-aware CRDT strategies appropriate for:
- paragraphs
- outline blocks
- rich-text structures
- any ordered content where one user’s contiguous insertion should remain contiguous
The cost
You do not get this for free. Common costs include:
- more metadata per element
- tombstones or historical references
- more complex merge logic
- harder garbage collection / compaction
- less pleasant storage model than a simple sortable rank column
Best fit
- offline-first collaborative editors
- peer-to-peer shared documents
- structured text/block editing where adjacency correctness matters more than schema simplicity
Bad fit
- simple kanban ordering
- design layers with centralized authority
- ordinary CRUD apps with drag-and-drop reorder
10) Decision table
Use dense integers when
- lists are small
- reorder frequency is low
- simplicity matters more than write amplification
Use gapped integers when
- you want a low-complexity improvement over dense integers
- occasional rebalance is acceptable
- concurrency is modest
Use string fractional indexing when
- you have frequent drag-and-drop reordering
- only moved items should change
- server authority exists
- interleaving of concurrent inserts is acceptable or rare
Use LexoRank-style ranking when
- ranked ordering is a core operational feature
- you want explicit maintenance / balance / integrity procedures
- backlog-scale ranking is the product primitive
Use sequence CRDT / tree-based indexing when
- concurrent insertion adjacency is semantically important
- offline / merge-heavy collaboration is real
- text / block / paragraph order is not allowed to turn into soup
11) Common mistakes
Mistake 1: using plain floats in production because the demo looked fine
This is one of the most common traps. The first week is smooth. Then a hotspot list gets hammered, and precision edge cases quietly arrive.
Mistake 2: over-engineering with CRDTs for a simple server-backed kanban board
If the server is authoritative and clients are online, CRDT complexity is often wasted. A good fractional index is easier to build and easier to operate.
Mistake 3: using simple fractional ordering for text blocks and discovering interleaving later
If adjacency matters, test concurrency early. A stable total order is not enough. You care about preserving runs, not just item uniqueness.
Mistake 4: ignoring the operator story
Any long-lived ranking system needs answers for:
- rebalance / compaction
- duplicate detection
- invalid rank repair
- tie-break determinism
- migration between schemes
Jira’s LexoRank docs are a nice reminder that ranking systems eventually become operational systems.
Mistake 5: confusing “sortable key” with “semantic merge model”
A sortable rank solves ordering. It does not automatically solve collaborative intent preservation.
That distinction matters a lot.
12) Practical recommendations by product type
Kanban / issue tracker
Default pick:
- fractional indexing
- or LexoRank-style if ranking is central and long-lived at scale
Why:
- cheap reordering matters
- adjacency of concurrently inserted runs usually does not matter much
- operational repairability does matter
Design tool layers / whiteboard z-order
Default pick:
- string fractional indexing
Why:
- reordering is frequent
- only moved objects should update
- occasional interleaving is usually tolerable
- server-authoritative collaboration is common
This is exactly the type of use case Figma’s approach is excellent for.
CMS blocks / outline sections with ordinary collaboration
Default pick:
- start with fractional indexing if server-authoritative
- graduate to CRDT-style structure only if offline/local-first or adjacency bugs become real
Rich-text / paragraph collaborative editor
Default pick:
- sequence CRDT / tree-based indexing / RGA-like strategy
Why:
- preserving contiguous insertions matters
- merge semantics matter more than easy SQL sorting
Small internal admin app
Default pick:
- probably just integers with gaps
Why:
- simple beats clever
- you may never hit the pathological cases
13) A simple selection rule I would actually use
If I were choosing quickly:
Case A — ordinary product list ordering
I would choose string fractional indexing.
Reason:
- best simplicity-to-power ratio
- easy DB model
- cheap reordering
- avoids float precision nonsense
Case B — Jira-like ranked backlog as a core business object
I would study LexoRank-style operations and maintenance from day one.
Reason:
- ranking becomes long-lived infrastructure
- balancing/integrity are not side notes
Case C — collaborative text/block structure with real offline merge
I would choose a sequence CRDT family.
Reason:
- preserving insertion intent matters more than schema neatness
Case D — tiny CRUD admin tool
I would choose gapped integers and move on with my life.
Reason:
- done is better than academically optimal
14) Schema and ops notes
For fractional indexing
Store at least:
idparent_idif hierarchicalorder_keytextupdated_at- optional
moved_by,moved_atfor audit/debugging
Operationally:
- define deterministic tie-breaks
- log pathological key growth
- optionally add a compaction job for extreme hotspots
- test repeated midpoint insertion and concurrent insert races
For LexoRank-style systems
Also plan for:
- integrity checks
- duplicate / out-of-bounds detection
- balancing procedure
- admin visibility into rank health
For sequence CRDTs
Also plan for:
- tombstone / history retention policy
- compaction and GC rules
- merge test corpus with concurrency fixtures
- serialization format stability
- migration path if metadata grows too much
15) TL;DR
- Dense integer ranks are simple but cause too many rewrites.
- Gapped integers delay the pain but do not eliminate rebalance.
- Plain floats are convenient and eventually betray you.
- String fractional indexing is the best general default for server-authoritative reorderable lists.
- LexoRank-style systems are worth it when ranked ordering is a long-lived operational primitive.
- Sequence CRDTs are for when preserving concurrent insertion intent matters more than keeping the schema cute.
The core distinction is this:
Ordering keys solve cheap reordering.
Sequence CRDTs solve collaborative intent under concurrency.
Choose based on which problem you actually have.
References / starting points
Evan Wallace, Realtime Editing of Ordered Sequences — clear explanation of Figma’s fractional indexing trade-offs, including why it is great for design-layer ordering and why interleaving makes it a bad fit for text.
https://www.figma.com/blog/realtime-editing-of-ordered-sequences/Atlassian Support, Understanding and Managing LexoRank in Jira Server / Data Center — useful for the operational worldview around balancing, integrity checks, marker rows, duplicates, and out-of-bounds conditions.
https://support.atlassian.com/jira/kb/understanding-and-managing-lexorank-in-jira-server/Steve Dignam, User Defined Ordering Made Easy — pragmatic comparison of integers, floats, decimals, rationals, and lexicographically sorted strings for database-backed ordering.
https://steve.dignam.xyz/2020/03/31/practical-ordering/Steve Ruiz, Reordering Part 2: Tables and Fractional Indexing — very practical explanation of why fractional positions reduce rewrite blast radius in UI reorder flows and where naive float implementations fail.
https://www.steveruiz.me/posts/reordering-fractional-indicesEvan Wallace, CRDT: Tree-Based Indexing — strong explanation of adjacency-preserving ordering for collaborative sequences where concurrent runs should not interleave.
https://madebyevan.com/algos/crdt-tree-based-indexing/Bartosz Sypytkowski, Non-interleaving Linear Sequence (LSeq) CRDT — practical discussion of the interleaving problem in LSeq-like sequence CRDTs and one way to mitigate it.
https://www.bartoszsypytkowski.com/non-interleaving-lseq/