Plugin histories

acts_as_list: ordered records in Rails, then and now

Plugin histories · · Published 2026-10-24

Rows of cardboard tubes on a shelf, each holding a bundle with a coloured numbered tag
Numbered samples stored in rows of tubes at a wool museum.

Tasks in a to-do list, chapters in a book, slides in a deck: many models need a user-defined order. acts_as_list has handled that since before Rails 2, and it is still maintained.

From Rails core to a gem

In Rails 1.x, acts_as_list was part of Active Record itself, alongside acts_as_tree and acts_as_nested_set. Rails 2.0 extracted these features into separate plugins to keep the core smaller. acts_as_list then lived as a plugin and, from the Rails 3 era onwards, as a gem. Like many plugins of the time, it went through a period around the Rails 3 release when users were unsure which fork worked with which version, before the main repository consolidated the fixes.

The project is now maintained in the brendon/acts_as_list repository. According to RubyGems.org, the latest release is 1.2.6, from October 2025, and it requires Active Record 6.1 or newer.

How it works

You add an integer position column and declare the list, optionally scoped to a parent:

class TodoItem < ApplicationRecord
  belongs_to :todo_list
  acts_as_list scope: :todo_list
end

class TodoList < ApplicationRecord
  has_many :todo_items, -> { order(position: :asc) }
end

New records are added at the bottom of their list by default (configurable with add_new_at: :top). The gem provides methods to reorder items, each of which updates the positions of the affected neighbours:

Scopes can be a single association, several columns, or a custom SQL condition, so one table can hold many independent lists. Destroying an item shifts the items below it up by one.

Things to watch

Concurrency. Reordering updates many rows. If two requests move items in the same list at the same time, positions can end up duplicated or with gaps. The project's README discusses this and the trade-offs of adding a unique index on the scope and position columns, which is harder than it sounds because the gem temporarily shifts positions during updates. For lists that many users edit at once, consider wrapping reorders in a lock on the parent record, or use one of the alternatives below.

Callbacks and bulk operations. acts_as_list maintains positions through Active Record callbacks. Methods that skip callbacks, such as update_all, delete_all or insert_all, leave the list inconsistent; you then need to renumber it yourself.

Large lists. Moving an item from the bottom to the top of a list of thousands updates thousands of rows. For long, frequently reordered lists, a sparse ranking scheme performs better.

Alternatives

Drag-and-drop reordering

The usual interface for ordered lists is drag and drop. With Hotwire, a small Stimulus controller around a sortable JavaScript library sends the new position of the moved item to a member route, for example PATCH /todo_items/:id/move with a position parameter, and the controller calls insert_at. Send the target position rather than the whole new order: it is smaller, and it lets the gem compute the other changes inside a single transaction.

Should you use it?

For most ordered lists in a Rails application, acts_as_list remains a sound, maintained choice with a long track record. Choose positioning or ranked-model if you expect heavy concurrent reordering or very long lists. As with any gem, check its CI against your Rails version and run your own tests around reordering before an upgrade; our guide to evaluating a gem explains what to look for.