Avoiding index collisions in sortable lists
The limits of integer indices
If you have ever built a drag-and-drop list, you have probably stored the order like this.
[
{ "id": "a", "order": 1 },
{ "id": "b", "order": 2 },
{ "id": "c", "order": 3 }
]
What happens if you move b to the front? b becomes 0, and a is still 1, so at first glance it seems fine. But if you later want to insert a new item between a and b, you have to shift a to 2 and c to 3. In other words, changing one item often forces you to update several others too.
In collaborative tools where multiple users can reorder items at the same time, that structure tends to create collisions. If two people modify the same part of the list concurrently, the final order can become inconsistent or trigger large update conflicts.
What is fractional-indexing?
David Greenspan introduced this approach in Implementing Fractional Indexing. The core idea is simple: instead of using integers for order, use sortable string keys.
[
{ "id": "a", "order": "a0" },
{ "id": "b", "order": "a1" },
{ "id": "c", "order": "a2" }
]
Want to insert an item between a1 and a2? You can generate a middle key like a1V. Everything else stays unchanged.
Figma uses this idea in its multiplayer editing system. It manages child-node ordering with fractional indexing, which means reordering typically updates only the moved node.
Using the library
In JavaScript, you can use the fractional-indexing package.
npm install fractional-indexing
import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing';
// First key
const first = generateKeyBetween(null, null);
// → 'a0'
// Insert at the beginning
const zeroth = generateKeyBetween(null, first);
// → 'Zz'
// Insert at the end
const second = generateKeyBetween(first, null);
// → 'a1'
// Generate a key between two existing keys
const third = generateKeyBetween(second, null); // 'a2'
const mid = generateKeyBetween(second, third);
// → 'a1V'
// Generate multiple keys at once
const keys = generateNKeysBetween('a0', 'a2', 2);
// → ['a0G', 'a0V']
You store the key as a string in the database and sort it with lexicographic order using ORDER BY. The scheme is designed so alphabetical order matches the intended item order.
Other ways to manage ordering
fractional-indexing is not the only option. There are a few common alternatives, and each comes with tradeoffs.
Gap strategy with integers
This is the simplest approach. You start with generous spacing.
[
{ "id": "a", "order": 1000 },
{ "id": "b", "order": 2000 },
{ "id": "c", "order": 3000 }
]
To insert between a and b, you assign order: 1500. It is simple and fast. The downside is that once the gaps are exhausted, you eventually need to reindex everything. If inserts keep happening in the same region, you end up with values like 1500 → 1250 → 1375 → ..., and a full rebalance becomes unavoidable.
Timestamp-based ordering
Another approach is to use insertion time as the order value.
const item = {
id: 'a',
order: Date.now(), // 1700000001000
};
This is the easiest implementation. The problem is that if two clients insert at nearly the same time, ordering becomes ambiguous. For a single-user app, that may be fine. In collaborative environments, it is usually not reliable enough.
Linked list ordering
In this model, each item points to the next item.
[
{ "id": "a", "next": "b" },
{ "id": "b", "next": "c" },
{ "id": "c", "next": null }
]
The nice part is that insertion only touches nearby nodes, so the update scope stays small. The downside is that reading the full order requires traversal, and you lose the convenience of a simple database ORDER BY. If your service reads ordered lists frequently, query complexity can become a real cost.
How to choose
| Approach | Implementation complexity | Collaboration safety | Long-term operation |
|---|---|---|---|
| fractional-indexing | Medium | High | Rebalancing needed |
| linked list | Medium | Medium | More complex queries |
| integer gaps | Low | Low | Reindexing needed |
| timestamps | Low | Low | Collision risk |
If your product involves frequent reordering or multiple users interacting with the same list, fractional-indexing is close to a practical default. For simpler single-user apps, a gap strategy with integers can still be perfectly sufficient.
Things to watch out for
Keys can grow longer over time. If you keep generating new keys inside the same narrow interval, the string length increases. That is why long-running systems often need a rebalancing step that periodically rewrites the ordering keys.
Another important detail is consistency in string comparison. Your database, server, and client should all treat ordering the same way. If different layers compare keys differently, the rendered order can drift from the intended one.
Closing thoughts
If you manage ordering with plain integers, you eventually run into friction. fractional-indexing is a fairly elegant way to avoid that problem. It is especially worth considering when you need realtime collaboration, optimistic updates, or frequent drag-and-drop reordering.


Top comments (0)