diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 5a1fc68a915e51d261ed40250bb6c4ac5849bbab..c2e8bfa1140086ffd37f0dc143775cdbc4bb4920 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1324,7 +1324,6 @@ Rails/SaveBang:
     - 'spec/support/shared_examples/models/members_notifications_shared_example.rb'
     - 'spec/support/shared_examples/models/mentionable_shared_examples.rb'
     - 'spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb'
-    - 'spec/support/shared_examples/models/relative_positioning_shared_examples.rb'
     - 'spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb'
     - 'spec/support/shared_examples/models/update_project_statistics_shared_examples.rb'
     - 'spec/support/shared_examples/models/with_uploads_shared_examples.rb'
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index ca64907f04bf35d52a7c2f30918722f729bc6d5b..afc818b28ab4de6e157add33f02053494ad5fb3c 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -3,11 +3,15 @@
 # This module makes it possible to handle items as a list, where the order of items can be easily altered
 # Requirements:
 #
-# - Only works for ActiveRecord models
-# - relative_position integer field must present on the model
-# - This module uses GROUP BY: the model should have a parent relation, example: project -> issues, project is the parent relation (issues table has a parent_id column)
+# The model must have the following named columns:
+#  - id: integer
+#  - relative_position: integer
 #
-# Setup like this in the body of your class:
+# The model must support a concept of siblings via a child->parent relationship,
+# to enable rebalancing and `GROUP BY` in queries.
+# - example: project -> issues, project is the parent relation (issues table has a parent_id column)
+#
+# Two class methods must be defined when including this concern:
 #
 #     include RelativePositioning
 #
@@ -24,66 +28,162 @@
 module RelativePositioning
   extend ActiveSupport::Concern
 
-  MIN_POSITION = 0
-  START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2
-  MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
-  IDEAL_DISTANCE = 500
+  STEPS = 10
+  IDEAL_DISTANCE = 2**(STEPS - 1) + 1
 
-  class_methods do
-    def move_nulls_to_end(objects)
-      objects = objects.reject(&:relative_position)
-      return if objects.empty?
+  MIN_POSITION = Gitlab::Database::MIN_INT_VALUE
+  START_POSITION = 0
+  MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
 
-      self.transaction do
-        max_relative_position = objects.first.max_relative_position
+  MAX_GAP = IDEAL_DISTANCE * 2
+  MIN_GAP = 2
 
-        objects.each do |object|
-          relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION)
-          object.update_column(:relative_position, relative_position)
+  NoSpaceLeft = Class.new(StandardError)
 
-          max_relative_position = relative_position
-        end
-      end
+  class_methods do
+    def move_nulls_to_end(objects)
+      move_nulls(objects, at_end: true)
     end
 
     def move_nulls_to_start(objects)
-      objects = objects.reject(&:relative_position)
-      return if objects.empty?
-
-      self.transaction do
-        min_relative_position = objects.first.min_relative_position
-
-        objects.reverse_each do |object|
-          relative_position = position_between(MIN_POSITION, min_relative_position || START_POSITION)
-          object.update_column(:relative_position, relative_position)
-
-          min_relative_position = relative_position
-        end
-      end
+      move_nulls(objects, at_end: false)
     end
 
     # This method takes two integer values (positions) and
     # calculates the position between them. The range is huge as
-    # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time
-    # when we have enough space. If distance is less than IDEAL_DISTANCE, we are calculating an average number.
+    # the maximum integer value is 2147483647.
+    #
+    # We avoid open ranges by clamping the range to [MIN_POSITION, MAX_POSITION].
+    #
+    # Then we handle one of three cases:
+    #  - If the gap is too small, we raise NoSpaceLeft
+    #  - If the gap is larger than MAX_GAP, we place the new position at most
+    #    IDEAL_DISTANCE from the edge of the gap.
+    #  - otherwise we place the new position at the midpoint.
+    #
+    # The new position will always satisfy: pos_before <= midpoint <= pos_after
+    #
+    # As a precondition, the gap between pos_before and pos_after MUST be >= 2.
+    # If the gap is too small, NoSpaceLeft is raised.
+    #
+    # This class method should only be called by instance methods of this module, which
+    # include handling for minimum gap size.
+    #
+    # @raises NoSpaceLeft
+    # @api private
     def position_between(pos_before, pos_after)
       pos_before ||= MIN_POSITION
       pos_after ||= MAX_POSITION
 
       pos_before, pos_after = [pos_before, pos_after].sort
 
-      halfway = (pos_after + pos_before) / 2
-      distance_to_halfway = pos_after - halfway
+      gap_width = pos_after - pos_before
+      midpoint = [pos_after - 1, pos_before + (gap_width / 2)].min
 
-      if distance_to_halfway < IDEAL_DISTANCE
-        halfway
-      else
+      if gap_width < MIN_GAP
+        raise NoSpaceLeft
+      elsif gap_width > MAX_GAP
         if pos_before == MIN_POSITION
           pos_after - IDEAL_DISTANCE
         elsif pos_after == MAX_POSITION
           pos_before + IDEAL_DISTANCE
         else
-          halfway
+          midpoint
+        end
+      else
+        midpoint
+      end
+    end
+
+    private
+
+    # @api private
+    def gap_size(object, gaps:, at_end:, starting_from:)
+      total_width = IDEAL_DISTANCE * gaps
+      size = if at_end && starting_from + total_width >= MAX_POSITION
+               (MAX_POSITION - starting_from) / gaps
+             elsif !at_end && starting_from - total_width <= MIN_POSITION
+               (starting_from - MIN_POSITION) / gaps
+             else
+               IDEAL_DISTANCE
+             end
+
+      # Shift max elements leftwards if there isn't enough space
+      return [size, starting_from] if size >= MIN_GAP
+
+      order = at_end ? :desc : :asc
+      terminus = object
+        .send(:relative_siblings) # rubocop:disable GitlabSecurity/PublicSend
+        .where('relative_position IS NOT NULL')
+        .order(relative_position: order)
+        .first
+
+      if at_end
+        terminus.move_sequence_before(true)
+        max_relative_position = terminus.reset.relative_position
+        [[(MAX_POSITION - max_relative_position) / gaps, IDEAL_DISTANCE].min, max_relative_position]
+      else
+        terminus.move_sequence_after(true)
+        min_relative_position = terminus.reset.relative_position
+        [[(min_relative_position - MIN_POSITION) / gaps, IDEAL_DISTANCE].min, min_relative_position]
+      end
+    end
+
+    # @api private
+    # @param [Array<RelativePositioning>] objects The objects to give positions to. The relative
+    #                                             order will be preserved (i.e. when this method returns,
+    #                                             objects.first.relative_position < objects.last.relative_position)
+    # @param [Boolean] at_end: The placement.
+    #                          If `true`, then all objects with `null` positions are placed _after_
+    #                          all siblings with positions. If `false`, all objects with `null`
+    #                          positions are placed _before_ all siblings with positions.
+    def move_nulls(objects, at_end:)
+      objects = objects.reject(&:relative_position)
+      return if objects.empty?
+
+      representative = objects.first
+      number_of_gaps = objects.size + 1 # 1 at left, one between each, and one at right
+      position = if at_end
+                   representative.max_relative_position
+                 else
+                   representative.min_relative_position
+                 end
+
+      position ||= START_POSITION # If there are no positioned siblings, start from START_POSITION
+
+      gap, position = gap_size(representative, gaps: number_of_gaps, at_end: at_end, starting_from: position)
+
+      # Raise if we could not make enough space
+      raise NoSpaceLeft if gap < MIN_GAP
+
+      indexed = objects.each_with_index.to_a
+      starting_from = at_end ? position : position - (gap * number_of_gaps)
+
+      # Some classes are polymorphic, and not all siblings are in the same table.
+      by_model = indexed.group_by { |pair| pair.first.class }
+
+      by_model.each do |model, pairs|
+        model.transaction do
+          pairs.each_slice(100) do |batch|
+            # These are known to be integers, one from the DB, and the other
+            # calculated by us, and thus safe to interpolate
+            values = batch.map do |obj, i|
+              pos = starting_from + gap * (i + 1)
+              obj.relative_position = pos
+              "(#{obj.id}, #{pos})"
+            end.join(', ')
+
+            model.connection.exec_query(<<~SQL, "UPDATE #{model.table_name} positions")
+              WITH cte(cte_id, new_pos) AS (
+               SELECT *
+               FROM (VALUES #{values}) as t (id, pos)
+              )
+              UPDATE #{model.table_name}
+              SET relative_position = cte.new_pos
+              FROM cte
+              WHERE cte_id = id
+            SQL
+          end
         end
       end
     end
@@ -97,11 +197,12 @@ def max_relative_position(&block)
     calculate_relative_position('MAX', &block)
   end
 
-  def prev_relative_position
+  def prev_relative_position(ignoring: nil)
     prev_pos = nil
 
     if self.relative_position
       prev_pos = max_relative_position do |relation|
+        relation = relation.id_not_in(ignoring.id) if ignoring.present?
         relation.where('relative_position < ?', self.relative_position)
       end
     end
@@ -109,11 +210,12 @@ def prev_relative_position
     prev_pos
   end
 
-  def next_relative_position
+  def next_relative_position(ignoring: nil)
     next_pos = nil
 
     if self.relative_position
       next_pos = min_relative_position do |relation|
+        relation = relation.id_not_in(ignoring.id) if ignoring.present?
         relation.where('relative_position > ?', self.relative_position)
       end
     end
@@ -125,24 +227,44 @@ def move_between(before, after)
     return move_after(before) unless after
     return move_before(after) unless before
 
-    # If there is no place to insert an item we need to create one by moving the item
-    # before this and all preceding items until there is a gap
     before, after = after, before if after.relative_position < before.relative_position
-    if (after.relative_position - before.relative_position) < 2
-      after.move_sequence_before
-      before.reset
+
+    pos_left = before.relative_position
+    pos_right = after.relative_position
+
+    if pos_right - pos_left < MIN_GAP
+      # Not enough room! Make space by shifting all previous elements to the left
+      # if there is enough space, else to the right
+      gap = after.send(:find_next_gap_before) # rubocop:disable GitlabSecurity/PublicSend
+
+      if gap.present?
+        after.move_sequence_before(next_gap: gap)
+        pos_left -= optimum_delta_for_gap(gap)
+      else
+        before.move_sequence_after
+        pos_right = after.reset.relative_position
+      end
     end
 
-    self.relative_position = self.class.position_between(before.relative_position, after.relative_position)
+    new_position = self.class.position_between(pos_left, pos_right)
+
+    self.relative_position = new_position
   end
 
   def move_after(before = self)
     pos_before = before.relative_position
-    pos_after = before.next_relative_position
+    pos_after = before.next_relative_position(ignoring: self)
+
+    if pos_before == MAX_POSITION || gap_too_small?(pos_after, pos_before)
+      gap = before.send(:find_next_gap_after) # rubocop:disable GitlabSecurity/PublicSend
 
-    if pos_after && (pos_after - pos_before) < 2
-      before.move_sequence_after
-      pos_after = before.next_relative_position
+      if gap.nil?
+        before.move_sequence_before(true)
+        pos_before = before.reset.relative_position
+      else
+        before.move_sequence_after(next_gap: gap)
+        pos_after += optimum_delta_for_gap(gap)
+      end
     end
 
     self.relative_position = self.class.position_between(pos_before, pos_after)
@@ -150,80 +272,168 @@ def move_after(before = self)
 
   def move_before(after = self)
     pos_after = after.relative_position
-    pos_before = after.prev_relative_position
+    pos_before = after.prev_relative_position(ignoring: self)
 
-    if pos_before && (pos_after - pos_before) < 2
-      after.move_sequence_before
-      pos_before = after.prev_relative_position
+    if pos_after == MIN_POSITION || gap_too_small?(pos_before, pos_after)
+      gap = after.send(:find_next_gap_before) # rubocop:disable GitlabSecurity/PublicSend
+
+      if gap.nil?
+        after.move_sequence_after(true)
+        pos_after = after.reset.relative_position
+      else
+        after.move_sequence_before(next_gap: gap)
+        pos_before -= optimum_delta_for_gap(gap)
+      end
     end
 
     self.relative_position = self.class.position_between(pos_before, pos_after)
   end
 
   def move_to_end
-    self.relative_position = self.class.position_between(max_relative_position || START_POSITION, MAX_POSITION)
+    max_pos = max_relative_position
+
+    self.relative_position = max_pos.nil? ? START_POSITION : self.class.position_between(max_pos, MAX_POSITION)
   end
 
   def move_to_start
-    self.relative_position = self.class.position_between(min_relative_position || START_POSITION, MIN_POSITION)
+    min_pos = max_relative_position
+
+    self.relative_position = min_pos.nil? ? START_POSITION : self.class.position_between(MIN_POSITION, min_pos)
   end
 
   # Moves the sequence before the current item to the middle of the next gap
-  # For example, we have 5 11 12 13 14 15 and the current item is 15
-  # This moves the sequence 11 12 13 14 to 8 9 10 11
-  def move_sequence_before
-    next_gap = find_next_gap_before
+  # For example, we have
+  #
+  #   5 . . . . . 11 12 13 14 [15] 16 . 17
+  #               -----------
+  #
+  # This moves the sequence [11 12 13 14] to [8 9 10 11], so we have:
+  #
+  #   5 . . 8 9 10 11 . . . [15] 16 . 17
+  #         ---------
+  #
+  # Creating a gap to the left of the current item. We can understand this as
+  # dividing the 5 spaces between 5 and 11 into two smaller gaps of 2 and 3.
+  #
+  # If `include_self` is true, the current item will also be moved, creating a
+  # gap to the right of the current item:
+  #
+  #   5 . . 8 9 10 11 [14] . . . 16 . 17
+  #         --------------
+  #
+  # As an optimization, the gap can be precalculated and passed to this method.
+  #
+  # @api private
+  # @raises NoSpaceLeft if the sequence cannot be moved
+  def move_sequence_before(include_self = false, next_gap: find_next_gap_before)
+    raise NoSpaceLeft unless next_gap.present?
+
     delta = optimum_delta_for_gap(next_gap)
 
-    move_sequence(next_gap[:start], relative_position, -delta)
+    move_sequence(next_gap[:start], relative_position, -delta, include_self)
   end
 
   # Moves the sequence after the current item to the middle of the next gap
-  # For example, we have 11 12 13 14 15 21 and the current item is 11
-  # This moves the sequence 12 13 14 15 to 15 16 17 18
-  def move_sequence_after
-    next_gap = find_next_gap_after
+  # For example, we have:
+  #
+  #   8 . 10 [11] 12 13 14 15 . . . . . 21
+  #               -----------
+  #
+  # This moves the sequence [12 13 14 15] to [15 16 17 18], so we have:
+  #
+  #   8 . 10 [11] . . . 15 16 17 18 . . 21
+  #                     -----------
+  #
+  # Creating a gap to the right of the current item. We can understand this as
+  # dividing the 5 spaces between 15 and 21 into two smaller gaps of 3 and 2.
+  #
+  # If `include_self` is true, the current item will also be moved, creating a
+  # gap to the left of the current item:
+  #
+  #   8 . 10 . . . [14] 15 16 17 18 . . 21
+  #                ----------------
+  #
+  # As an optimization, the gap can be precalculated and passed to this method.
+  #
+  # @api private
+  # @raises NoSpaceLeft if the sequence cannot be moved
+  def move_sequence_after(include_self = false, next_gap: find_next_gap_after)
+    raise NoSpaceLeft unless next_gap.present?
+
     delta = optimum_delta_for_gap(next_gap)
 
-    move_sequence(relative_position, next_gap[:start], delta)
+    move_sequence(relative_position, next_gap[:start], delta, include_self)
   end
 
   private
 
-  # Supposing that we have a sequence of items: 1 5 11 12 13 and the current item is 13
-  # This would return: `{ start: 11, end: 5 }`
+  def gap_too_small?(pos_a, pos_b)
+    return false unless pos_a && pos_b
+
+    (pos_a - pos_b).abs < MIN_GAP
+  end
+
+  # Find the first suitable gap to the left of the current position.
+  #
+  # Satisfies the relations:
+  #  - gap[:start] <= relative_position
+  #  - abs(gap[:start] - gap[:end]) >= MIN_GAP
+  #  - MIN_POSITION <= gap[:start] <= MAX_POSITION
+  #  - MIN_POSITION <= gap[:end] <= MAX_POSITION
+  #
+  # Supposing that the current item is 13, and we have a sequence of items:
+  #
+  #   1 . . . 5 . . . . 11 12 [13] 14 . . 17
+  #           ^---------^
+  #
+  # Then we return: `{ start: 11, end: 5 }`
+  #
+  # Here start refers to the end of the gap closest to the current item.
   def find_next_gap_before
     items_with_next_pos = scoped_items
                             .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position DESC) AS next_pos')
                             .where('relative_position <= ?', relative_position)
                             .order(relative_position: :desc)
 
-    find_next_gap(items_with_next_pos).tap do |gap|
-      gap[:end] ||= MIN_POSITION
-    end
+    find_next_gap(items_with_next_pos, MIN_POSITION)
   end
 
-  # Supposing that we have a sequence of items: 13 14 15 20 24 and the current item is 13
-  # This would return: `{ start: 15, end: 20 }`
+  # Find the first suitable gap to the right of the current position.
+  #
+  # Satisfies the relations:
+  #  - gap[:start] >= relative_position
+  #  - abs(gap[:start] - gap[:end]) >= MIN_GAP
+  #  - MIN_POSITION <= gap[:start] <= MAX_POSITION
+  #  - MIN_POSITION <= gap[:end] <= MAX_POSITION
+  #
+  # Supposing the current item is 13, and that we have a sequence of items:
+  #
+  #   9 . . . [13] 14 15 . . . . 20 . . . 24
+  #                    ^---------^
+  #
+  # Then we return: `{ start: 15, end: 20 }`
+  #
+  # Here start refers to the end of the gap closest to the current item.
   def find_next_gap_after
     items_with_next_pos = scoped_items
                             .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position ASC) AS next_pos')
                             .where('relative_position >= ?', relative_position)
                             .order(:relative_position)
 
-    find_next_gap(items_with_next_pos).tap do |gap|
-      gap[:end] ||= MAX_POSITION
-    end
+    find_next_gap(items_with_next_pos, MAX_POSITION)
   end
 
-  def find_next_gap(items_with_next_pos)
-    gap = self.class.from(items_with_next_pos, :items_with_next_pos)
-                    .where('ABS(pos - next_pos) > 1 OR next_pos IS NULL')
-                    .limit(1)
-                    .pluck(:pos, :next_pos)
-                    .first
+  def find_next_gap(items_with_next_pos, end_is_nil)
+    gap = self.class
+      .from(items_with_next_pos, :items)
+      .where('next_pos IS NULL OR ABS(pos::bigint - next_pos::bigint) >= ?', MIN_GAP)
+      .limit(1)
+      .pluck(:pos, :next_pos)
+      .first
 
-    { start: gap[0], end: gap[1] }
+    return if gap.nil? || gap.first == end_is_nil
+
+    { start: gap.first, end: gap.second || end_is_nil }
   end
 
   def optimum_delta_for_gap(gap)
@@ -232,9 +442,10 @@ def optimum_delta_for_gap(gap)
     [delta, IDEAL_DISTANCE].min
   end
 
-  def move_sequence(start_pos, end_pos, delta)
-    scoped_items
-      .where.not(id: self.id)
+  def move_sequence(start_pos, end_pos, delta, include_self = false)
+    relation = include_self ? scoped_items : relative_siblings
+
+    relation
       .where('relative_position BETWEEN ? AND ?', start_pos, end_pos)
       .update_all("relative_position = relative_position + #{delta}")
   end
@@ -255,6 +466,10 @@ def calculate_relative_position(calculation)
       .first&.last
   end
 
+  def relative_siblings(relation = scoped_items)
+    relation.id_not_in(id)
+  end
+
   def scoped_items
     self.class.relative_positioning_query_base(self)
   end
diff --git a/changelogs/unreleased/ajk-relative-positioning-improvements.yml b/changelogs/unreleased/ajk-relative-positioning-improvements.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c9de05a0403a353390554486d26fa08457cd6ea5
--- /dev/null
+++ b/changelogs/unreleased/ajk-relative-positioning-improvements.yml
@@ -0,0 +1,5 @@
+---
+title: Performance and robustness improvements for relative positioning
+merge_request: 37724
+author:
+type: performance
diff --git a/ee/app/models/concerns/epic_tree_sorting.rb b/ee/app/models/concerns/epic_tree_sorting.rb
index ae56137381cfa35824ff2b5c5278008326273b5f..4763462803661bcf9a2772e2372577b94a7b5f9a 100644
--- a/ee/app/models/concerns/epic_tree_sorting.rb
+++ b/ee/app/models/concerns/epic_tree_sorting.rb
@@ -8,8 +8,8 @@ module EpicTreeSorting
   class_methods do
     def relative_positioning_query_base(object)
       from_union([
-        EpicIssue.select("id, relative_position, epic_id, 'epic_issue' as object_type").in_epic(object.parent_ids),
-        Epic.select("id, relative_position, parent_id as epic_id, 'epic' as object_type").where(parent_id: object.parent_ids)
+        EpicIssue.select("id, relative_position, epic_id as parent_id, epic_id, 'epic_issue' as object_type").in_epic(object.parent_ids),
+        Epic.select("id, relative_position, parent_id, parent_id as epic_id, 'epic' as object_type").where(parent_id: object.parent_ids)
       ])
     end
 
@@ -19,11 +19,14 @@ def relative_positioning_parent_column
   end
 
   included do
-    def move_sequence(start_pos, end_pos, delta)
+    def move_sequence(start_pos, end_pos, delta, include_self = false)
       items_to_update = scoped_items
         .select(:id, :object_type)
         .where('relative_position BETWEEN ? AND ?', start_pos, end_pos)
-        .where.not('object_type = ? AND id = ?', self.class.table_name.singularize, self.id)
+
+      unless include_self
+        items_to_update = items_to_update.where.not('object_type = ? AND id = ?', self.class.table_name.singularize, self.id)
+      end
 
       items_to_update.group_by { |item| item.object_type }.each do |type, group_items|
         ids = group_items.map(&:id)
diff --git a/ee/spec/models/epic_issue_spec.rb b/ee/spec/models/epic_issue_spec.rb
index fca7dfa2e630376221a347466e4fe7ebda5a9dd2..84aa50377ad8abd6b1c684616037bd84a9a1dad2 100644
--- a/ee/spec/models/epic_issue_spec.rb
+++ b/ee/spec/models/epic_issue_spec.rb
@@ -28,9 +28,49 @@
 
   context "relative positioning" do
     it_behaves_like "a class that supports relative positioning" do
-      let(:epic) { create(:epic) }
+      let_it_be(:epic) { create(:epic) }
       let(:factory) { :epic_issue }
       let(:default_params) { { epic: epic } }
     end
+
+    context 'with a mixed tree level' do
+      let_it_be(:epic) { create(:epic) }
+      let_it_be_with_reload(:left) { create(:epic_issue, epic: epic, relative_position: 100) }
+      let_it_be_with_reload(:middle) { create(:epic, group: epic.group, parent: epic, relative_position: 101) }
+      let_it_be_with_reload(:right) { create(:epic_issue, epic: epic, relative_position: 102) }
+
+      it 'can create space by using move_sequence_after' do
+        left.move_sequence_after
+        [left, middle, right].each(&:reset)
+
+        expect(middle.relative_position - left.relative_position).to be > 1
+        expect(left.relative_position).to be < middle.relative_position
+        expect(middle.relative_position).to be < right.relative_position
+      end
+
+      it 'can create space by using move_sequence_before' do
+        right.move_sequence_before
+        [left, middle, right].each(&:reset)
+
+        expect(right.relative_position - middle.relative_position).to be > 1
+        expect(left.relative_position).to be < middle.relative_position
+        expect(middle.relative_position).to be < right.relative_position
+      end
+
+      it 'moves nulls to the end' do
+        leaves = create_list(:epic_issue, 2, epic: epic, relative_position: nil)
+        nested = create(:epic, group: epic.group, parent: epic, relative_position: nil)
+        moved = [*leaves, nested]
+        level = [nested, *leaves, right]
+
+        expect do
+          EpicIssue.move_nulls_to_end(level)
+        end.not_to change { right.reset.relative_position }
+
+        moved.each(&:reset)
+
+        expect(moved.map(&:relative_position)).to all(be > right.relative_position)
+      end
+    end
   end
 end
diff --git a/ee/spec/models/epic_spec.rb b/ee/spec/models/epic_spec.rb
index 508a70e4e39318d7f589e88712b3d81fa7ef4de6..b42b8f4abfc67f994acfb59dbb1921dcaf5ec025 100644
--- a/ee/spec/models/epic_spec.rb
+++ b/ee/spec/models/epic_spec.rb
@@ -616,9 +616,19 @@ def epics(order_by)
   end
 
   context "relative positioning" do
-    it_behaves_like "a class that supports relative positioning" do
-      let(:factory) { :epic }
-      let(:default_params) { {} }
+    context 'there is no parent' do
+      it_behaves_like "a class that supports relative positioning" do
+        let(:factory) { :epic }
+        let(:default_params) { {} }
+      end
+    end
+
+    context 'there is a parent' do
+      it_behaves_like "a class that supports relative positioning" do
+        let_it_be(:parent) { create(:epic) }
+        let(:factory) { :epic }
+        let(:default_params) { { parent: parent, group: parent.group } }
+      end
     end
   end
 
diff --git a/ee/spec/models/issue_spec.rb b/ee/spec/models/issue_spec.rb
index bad3dfde7432a8a2b79066d2ecca9914fa46d5b1..583527d93251d91fdb5ed0012c79cd833e7c0d57 100644
--- a/ee/spec/models/issue_spec.rb
+++ b/ee/spec/models/issue_spec.rb
@@ -454,20 +454,14 @@
   end
 
   describe 'relative positioning with group boards' do
-    let(:group) { create(:group) }
-    let!(:board) { create(:board, group: group) }
-    let(:project) { create(:project, namespace: group) }
-    let(:project1) { create(:project, namespace: group) }
-    let(:issue) { build(:issue, project: project) }
-    let(:issue1) { build(:issue, project: project1) }
+    let_it_be(:group) { create(:group) }
+    let_it_be(:board) { create(:board, group: group) }
+    let_it_be(:project) { create(:project, namespace: group) }
+    let_it_be(:project1) { create(:project, namespace: group) }
+    let_it_be_with_reload(:issue) { create(:issue, project: project) }
+    let_it_be_with_reload(:issue1) { create(:issue, project: project1, relative_position: issue.relative_position + RelativePositioning::IDEAL_DISTANCE) }
     let(:new_issue) { build(:issue, project: project1, relative_position: nil) }
 
-    before do
-      [issue, issue1].each do |issue|
-        issue.move_to_end && issue.save
-      end
-    end
-
     describe '#max_relative_position' do
       it 'returns maximum position' do
         expect(issue.max_relative_position).to eq issue1.relative_position
@@ -546,18 +540,20 @@
         issue1.update relative_position: issue.relative_position
 
         new_issue.move_between(issue, issue1)
+        [issue, issue1].each(&:reset)
 
-        expect(new_issue.relative_position).to be > issue.relative_position
-        expect(issue.relative_position).to be < issue1.relative_position
+        expect(new_issue.relative_position)
+          .to be_between(issue.relative_position, issue1.relative_position).exclusive
       end
 
       it 'positions issues between other two if distance is 1' do
         issue1.update relative_position: issue.relative_position + 1
 
         new_issue.move_between(issue, issue1)
+        [issue, issue1].each(&:reset)
 
-        expect(new_issue.relative_position).to be > issue.relative_position
-        expect(issue.relative_position).to be < issue1.relative_position
+        expect(new_issue.relative_position)
+          .to be_between(issue.relative_position, issue1.relative_position).exclusive
       end
 
       it 'positions issue in the middle of other two if distance is big enough' do
@@ -566,24 +562,20 @@
 
         new_issue.move_between(issue, issue1)
 
-        expect(new_issue.relative_position).to eq(8000)
+        expect(new_issue.relative_position)
+          .to be_between(issue.relative_position, issue1.relative_position).exclusive
       end
 
       it 'positions issue closer to the middle if we are at the very top' do
-        issue1.update relative_position: 6000
-
-        new_issue.move_between(nil, issue1)
+        new_issue.move_between(nil, issue)
 
-        expect(new_issue.relative_position).to eq(6000 - RelativePositioning::IDEAL_DISTANCE)
+        expect(new_issue.relative_position).to eq(issue.relative_position - RelativePositioning::IDEAL_DISTANCE)
       end
 
       it 'positions issue closer to the middle if we are at the very bottom' do
-        issue.update relative_position: 6000
-        issue1.update relative_position: nil
-
-        new_issue.move_between(issue, nil)
+        new_issue.move_between(issue1, nil)
 
-        expect(new_issue.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE)
+        expect(new_issue.relative_position).to eq(issue1.relative_position + RelativePositioning::IDEAL_DISTANCE)
       end
 
       it 'positions issue in the middle of other two if distance is not big enough' do
@@ -600,8 +592,10 @@
         issue1.update relative_position: 101
 
         new_issue.move_between(issue, issue1)
+        [issue, issue1].each(&:reset)
 
-        expect(new_issue.relative_position).to be_between(issue.relative_position, issue1.relative_position)
+        expect(new_issue.relative_position)
+          .to be_between(issue.relative_position, issue1.relative_position).exclusive
       end
 
       it 'uses rebalancing if there is no place' do
@@ -612,12 +606,15 @@
 
         new_issue.move_between(issue1, issue2)
         new_issue.save!
+        [issue, issue1, issue2].each(&:reset)
+
+        expect(new_issue.relative_position)
+          .to be_between(issue1.relative_position, issue2.relative_position).exclusive
 
-        expect(new_issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
-        expect(issue.reload.relative_position).not_to eq(100)
+        expect([issue, issue1, issue2, new_issue].map(&:relative_position).uniq).to have_attributes(size: 4)
       end
 
-      it 'positions issue right if we pass none-sequential parameters' do
+      it 'positions issue right if we pass non-sequential parameters' do
         issue.update relative_position: 99
         issue1.update relative_position: 101
         issue2 = create(:issue, relative_position: 102, project: project)
diff --git a/ee/spec/services/epic_issues/create_service_spec.rb b/ee/spec/services/epic_issues/create_service_spec.rb
index f3d42bbc4082c6231086767af701813ee002c751..12b661cd4f72b0d46b7f8218550c1af2b9163367 100644
--- a/ee/spec/services/epic_issues/create_service_spec.rb
+++ b/ee/spec/services/epic_issues/create_service_spec.rb
@@ -169,7 +169,9 @@ def assign_issue(references)
           end
 
           context 'when multiple valid issues are given' do
-            subject { assign_issue([issue.to_reference(full: true), issue2.to_reference(full: true)]) }
+            let(:references) { [issue, issue2].map { |i| i.to_reference(full: true) } }
+
+            subject { assign_issue(references) }
 
             let(:created_link1) { EpicIssue.find_by!(issue_id: issue.id) }
             let(:created_link2) { EpicIssue.find_by!(issue_id: issue2.id) }
@@ -181,13 +183,18 @@ def assign_issue(references)
               expect(created_link2).to have_attributes(epic: epic)
             end
 
+            it 'places each issue at the start' do
+              subject
+
+              expect(created_link2.relative_position).to be < created_link1.relative_position
+            end
             it 'orders the epic issues to the first place and moves the existing ones down' do
               existing_link = create(:epic_issue, epic: epic, issue: issue3)
 
               subject
 
-              expect(created_link2.relative_position).to be < created_link1.relative_position
-              expect(created_link1.relative_position).to be < existing_link.reload.relative_position
+              expect([created_link1, created_link2].map(&:relative_position))
+                .to all(be < existing_link.reset.relative_position)
             end
 
             it 'returns success status' do
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 859b53b9887d8f1e64d84e4268c003e109180690..e7df9fd27f01d6b6ceb8dbe0ccf541bf001ffbc2 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -22,6 +22,7 @@ module Database
 
     # https://www.postgresql.org/docs/9.2/static/datatype-numeric.html
     MAX_INT_VALUE = 2147483647
+    MIN_INT_VALUE = -2147483648
 
     # The max value between MySQL's TIMESTAMP and PostgreSQL's timestampz:
     # https://www.postgresql.org/docs/9.1/static/datatype-datetime.html
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index 03a76d9d3fd461e0cf69d66b6ff222291b153e57..87d29eed68d254470f143300e0b85342aee391b1 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -21,7 +21,7 @@
   end
 
   context 'un-ordered issues' do
-    let!(:issue4) { create(:labeled_issue, project: project, labels: [label]) }
+    let!(:issue4) { create(:labeled_issue, project: project, labels: [label], relative_position: nil) }
 
     before do
       visit project_board_path(project, board)
diff --git a/spec/models/analytics/cycle_analytics/project_stage_spec.rb b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
index 2e0240115533e0ef58e6d88f13413301b28ffa08..4675f0379575c661b823853d284074a685f07bd9 100644
--- a/spec/models/analytics/cycle_analytics/project_stage_spec.rb
+++ b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
@@ -23,7 +23,7 @@
 
   context 'relative positioning' do
     it_behaves_like 'a class that supports relative positioning' do
-      let(:project) { build(:project) }
+      let_it_be(:project) { create(:project) }
       let(:factory) { :cycle_analytics_project_stage }
       let(:default_params) { { project: project } }
     end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 586ef9da98ed4dda72e8d3455cadc4ecd454dc18..59634524e746a3ed8ff8e1aed2ceb38dbce69dbc 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -6,6 +6,7 @@
   include ExternalAuthorizationServiceHelpers
 
   let_it_be(:user) { create(:user) }
+  let_it_be(:reusable_project) { create(:project) }
 
   describe "Associations" do
     it { is_expected.to belong_to(:milestone) }
@@ -145,13 +146,13 @@
   end
 
   describe '#order_by_position_and_priority' do
-    let(:project) { create :project }
+    let(:project) { reusable_project }
     let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
     let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
     let!(:issue1) { create(:labeled_issue, project: project, labels: [p1]) }
     let!(:issue2) { create(:labeled_issue, project: project, labels: [p2]) }
-    let!(:issue3) { create(:issue, project: project, relative_position: 100) }
-    let!(:issue4) { create(:issue, project: project, relative_position: 200) }
+    let!(:issue3) { create(:issue, project: project, relative_position: -200) }
+    let!(:issue4) { create(:issue, project: project, relative_position: -100) }
 
     it 'returns ordered list' do
       expect(project.issues.order_by_position_and_priority)
@@ -160,10 +161,10 @@
   end
 
   describe '#sort' do
-    let(:project) { create(:project) }
+    let(:project) { reusable_project }
 
     context "by relative_position" do
-      let!(:issue)  { create(:issue, project: project) }
+      let!(:issue)  { create(:issue, project: project, relative_position: nil) }
       let!(:issue2) { create(:issue, project: project, relative_position: 2) }
       let!(:issue3) { create(:issue, project: project, relative_position: 1) }
 
@@ -1027,7 +1028,7 @@
 
   context "relative positioning" do
     it_behaves_like "a class that supports relative positioning" do
-      let(:project) { create(:project) }
+      let_it_be(:project) { create(:project) }
       let(:factory) { :issue }
       let(:default_params) { { project: project } }
     end
diff --git a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
index ab1467145a0433656786e3fe2695c4d9ddc53558..d09eb1fa14dcb1299d6f44237352a14e5bb75f38 100644
--- a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
+++ b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
@@ -25,7 +25,6 @@ def create_items_with_positions(positions)
 
       items = [item1, item2, item3]
       described_class.move_nulls_to_end(items)
-      items.map(&:reload)
 
       expect(items.sort_by(&:relative_position)).to eq(items)
       expect(item1.relative_position).to be(1000)
@@ -35,22 +34,57 @@ def create_items_with_positions(positions)
       expect(item3.next_relative_position).to be_nil
     end
 
+    it 'preserves relative position' do
+      item1.update!(relative_position: nil)
+      item2.update!(relative_position: nil)
+
+      described_class.move_nulls_to_end([item1, item2])
+
+      expect(item1.relative_position).to be < item2.relative_position
+    end
+
     it 'moves the item near the start position when there are no existing positions' do
       item1.update!(relative_position: nil)
 
       described_class.move_nulls_to_end([item1])
-      item1.reload
-
-      expect(item1.relative_position).to eq(described_class::START_POSITION + described_class::IDEAL_DISTANCE)
+      expect(item1.reset.relative_position).to eq(described_class::START_POSITION + described_class::IDEAL_DISTANCE)
     end
 
     it 'does not perform any moves if all items have their relative_position set' do
       item1.update!(relative_position: 1)
 
+      expect do
+        described_class.move_nulls_to_end([item1])
+      end.not_to change { item1.reset.relative_position }
+    end
+
+    it 'manages to move nulls to the end even if there is a sequence at the end' do
+      bunch = create_items_with_positions(run_at_end)
+      item1.update!(relative_position: nil)
+
       described_class.move_nulls_to_end([item1])
-      item1.reload
 
-      expect(item1.relative_position).to be(1)
+      items = [*bunch, item1]
+      items.each(&:reset)
+
+      expect(items.map(&:relative_position)).to all(be_valid_position)
+      expect(items.sort_by(&:relative_position)).to eq(items)
+    end
+
+    it 'does not have an N+1 issue' do
+      create_items_with_positions(10..12)
+
+      a, b, c, d, e, f = create_items_with_positions([nil, nil, nil, nil, nil, nil])
+
+      baseline = ActiveRecord::QueryRecorder.new do
+        described_class.move_nulls_to_end([a, e])
+      end
+
+      expect { described_class.move_nulls_to_end([b, c, d]) }
+        .not_to exceed_query_limit(baseline)
+
+      expect { described_class.move_nulls_to_end([f]) }
+        .not_to exceed_query_limit(baseline.count)
     end
   end
 
@@ -78,11 +112,19 @@ def create_items_with_positions(positions)
       item1.update!(relative_position: nil)
 
       described_class.move_nulls_to_start([item1])
-      item1.reload
 
       expect(item1.relative_position).to eq(described_class::START_POSITION - described_class::IDEAL_DISTANCE)
     end
 
+    it 'preserves relative position' do
+      item1.update!(relative_position: nil)
+      item2.update!(relative_position: nil)
+
+      described_class.move_nulls_to_end([item1, item2])
+
+      expect(item1.relative_position).to be < item2.relative_position
+    end
+
     it 'does not perform any moves if all items have their relative_position set' do
       item1.update!(relative_position: 1)
 
@@ -101,8 +143,8 @@ def create_items_with_positions(positions)
 
   describe '#prev_relative_position' do
     it 'returns previous position if there is an item above' do
-      item1.update(relative_position: 5)
-      item2.update(relative_position: 15)
+      item1.update!(relative_position: 5)
+      item2.update!(relative_position: 15)
 
       expect(item2.prev_relative_position).to eq item1.relative_position
     end
@@ -114,8 +156,8 @@ def create_items_with_positions(positions)
 
   describe '#next_relative_position' do
     it 'returns next position if there is an item below' do
-      item1.update(relative_position: 5)
-      item2.update(relative_position: 15)
+      item1.update!(relative_position: 5)
+      item2.update!(relative_position: 15)
 
       expect(item1.next_relative_position).to eq item2.relative_position
     end
@@ -125,9 +167,172 @@ def create_items_with_positions(positions)
     end
   end
 
+  describe '#find_next_gap_before' do
+    context 'there is no gap' do
+      let(:items) { create_items_with_positions(run_at_start) }
+
+      it 'returns nil' do
+        items.each do |item|
+          expect(item.send(:find_next_gap_before)).to be_nil
+        end
+      end
+    end
+
+    context 'there is a sequence ending at MAX_POSITION' do
+      let(:items) { create_items_with_positions(run_at_end) }
+
+      let(:gaps) do
+        items.map { |item| item.send(:find_next_gap_before) }
+      end
+
+      it 'can find the gap at the start for any item in the sequence' do
+        gap = { start: items.first.relative_position, end: RelativePositioning::MIN_POSITION }
+
+        expect(gaps).to all(eq(gap))
+      end
+
+      it 'respects lower bounds' do
+        gap = { start: items.first.relative_position, end: 10 }
+        new_item.update!(relative_position: 10)
+
+        expect(gaps).to all(eq(gap))
+      end
+    end
+
+    specify do
+      item1.update!(relative_position: 5)
+
+      (0..10).each do |pos|
+        item2.update!(relative_position: pos)
+
+        gap = item2.send(:find_next_gap_before)
+
+        expect(gap[:start]).to be <= item2.relative_position
+        expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP
+        expect(gap[:start]).to be_valid_position
+        expect(gap[:end]).to be_valid_position
+      end
+    end
+
+    it 'deals with there not being any items to the left' do
+      create_items_with_positions([1, 2, 3])
+      new_item.update!(relative_position: 0)
+
+      expect(new_item.send(:find_next_gap_before)).to eq(start: 0, end: RelativePositioning::MIN_POSITION)
+    end
+
+    it 'finds the next gap to the left, skipping adjacent values' do
+      create_items_with_positions([1, 9, 10])
+      new_item.update!(relative_position: 11)
+
+      expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 1)
+    end
+
+    it 'finds the next gap to the left' do
+      create_items_with_positions([2, 10])
+
+      new_item.update!(relative_position: 15)
+      expect(new_item.send(:find_next_gap_before)).to eq(start: 15, end: 10)
+
+      new_item.update!(relative_position: 11)
+      expect(new_item.send(:find_next_gap_before)).to eq(start: 10, end: 2)
+
+      new_item.update!(relative_position: 9)
+      expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 2)
+
+      new_item.update!(relative_position: 5)
+      expect(new_item.send(:find_next_gap_before)).to eq(start: 5, end: 2)
+    end
+  end
+
+  describe '#find_next_gap_after' do
+    context 'there is no gap' do
+      let(:items) { create_items_with_positions(run_at_end) }
+
+      it 'returns nil' do
+        items.each do |item|
+          expect(item.send(:find_next_gap_after)).to be_nil
+        end
+      end
+    end
+
+    context 'there is a sequence starting at MIN_POSITION' do
+      let(:items) { create_items_with_positions(run_at_start) }
+
+      let(:gaps) do
+        items.map { |item| item.send(:find_next_gap_after) }
+      end
+
+      it 'can find the gap at the end for any item in the sequence' do
+        gap = { start: items.last.relative_position, end: RelativePositioning::MAX_POSITION }
+
+        expect(gaps).to all(eq(gap))
+      end
+
+      it 'respects upper bounds' do
+        gap = { start: items.last.relative_position, end: 10 }
+        new_item.update!(relative_position: 10)
+
+        expect(gaps).to all(eq(gap))
+      end
+    end
+
+    specify do
+      item1.update!(relative_position: 5)
+
+      (0..10).each do |pos|
+        item2.update!(relative_position: pos)
+
+        gap = item2.send(:find_next_gap_after)
+
+        expect(gap[:start]).to be >= item2.relative_position
+        expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP
+        expect(gap[:start]).to be_valid_position
+        expect(gap[:end]).to be_valid_position
+      end
+    end
+
+    it 'deals with there not being any items to the right' do
+      create_items_with_positions([1, 2, 3])
+      new_item.update!(relative_position: 5)
+
+      expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: RelativePositioning::MAX_POSITION)
+    end
+
+    it 'finds the next gap to the right, skipping adjacent values' do
+      create_items_with_positions([1, 2, 10])
+      new_item.update!(relative_position: 0)
+
+      expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10)
+    end
+
+    it 'finds the next gap to the right' do
+      create_items_with_positions([2, 10])
+
+      new_item.update!(relative_position: 0)
+      expect(new_item.send(:find_next_gap_after)).to eq(start: 0, end: 2)
+
+      new_item.update!(relative_position: 1)
+      expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10)
+
+      new_item.update!(relative_position: 3)
+      expect(new_item.send(:find_next_gap_after)).to eq(start: 3, end: 10)
+
+      new_item.update!(relative_position: 5)
+      expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: 10)
+    end
+  end
+
   describe '#move_before' do
+    let(:item3) { create(factory, default_params) }
+
     it 'moves item before' do
-      [item2, item1].each(&:move_to_end)
+      [item2, item1].each do |item|
+        item.move_to_end
+        item.save!
+      end
+
+      expect(item1.relative_position).to be > item2.relative_position
 
       item1.move_before(item2)
 
@@ -135,12 +340,10 @@ def create_items_with_positions(positions)
     end
 
     context 'when there is no space' do
-      let(:item3) { create(factory, default_params) }
-
       before do
-        item1.update(relative_position: 1000)
-        item2.update(relative_position: 1001)
-        item3.update(relative_position: 1002)
+        item1.update!(relative_position: 1000)
+        item2.update!(relative_position: 1001)
+        item3.update!(relative_position: 1002)
       end
 
       it 'moves items correctly' do
@@ -149,6 +352,73 @@ def create_items_with_positions(positions)
         expect(item3.relative_position).to be_between(item1.reload.relative_position, item2.reload.relative_position).exclusive
       end
     end
+
+    it 'can move the item before an item at the start' do
+      item1.update!(relative_position: RelativePositioning::START_POSITION)
+
+      new_item.move_before(item1)
+
+      expect(new_item.relative_position).to be_valid_position
+      expect(new_item.relative_position).to be < item1.reload.relative_position
+    end
+
+    it 'can move the item before an item at MIN_POSITION' do
+      item1.update!(relative_position: RelativePositioning::MIN_POSITION)
+
+      new_item.move_before(item1)
+
+      expect(new_item.relative_position).to be >= RelativePositioning::MIN_POSITION
+      expect(new_item.relative_position).to be < item1.reload.relative_position
+    end
+
+    it 'can move the item before an item bunched up at MIN_POSITION' do
+      item1, item2, item3 = create_items_with_positions(run_at_start)
+
+      new_item.move_before(item3)
+      new_item.save!
+
+      items = [item1, item2, new_item, item3]
+
+      items.each do |item|
+        expect(item.reset.relative_position).to be_valid_position
+      end
+
+      expect(items.sort_by(&:relative_position)).to eq(items)
+    end
+
+    context 'leap-frogging to the left' do
+      before do
+        start = RelativePositioning::START_POSITION
+        item1.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 0)
+        item2.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 1)
+        item3.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 2)
+      end
+
+      let(:item3) { create(factory, default_params) }
+
+      def leap_frog(steps)
+        a = item1
+        b = item2
+
+        steps.times do |i|
+          a.move_before(b)
+          a.save!
+          a, b = b, a
+        end
+      end
+
+      it 'can leap-frog STEPS - 1 times before needing to rebalance' do
+        # This is less efficient than going right, due to the flooring of
+        # integer division
+        expect { leap_frog(RelativePositioning::STEPS - 1) }
+          .not_to change { item3.reload.relative_position }
+      end
+
+      it 'rebalances after leap-frogging STEPS times' do
+        expect { leap_frog(RelativePositioning::STEPS) }
+          .to change { item3.reload.relative_position }
+      end
+    end
   end
 
   describe '#move_after' do
@@ -164,9 +434,17 @@ def create_items_with_positions(positions)
       let(:item3) { create(factory, default_params) }
 
       before do
-        item1.update(relative_position: 1000)
-        item2.update(relative_position: 1001)
-        item3.update(relative_position: 1002)
+        item1.update!(relative_position: 1000)
+        item2.update!(relative_position: 1001)
+        item3.update!(relative_position: 1002)
+      end
+
+      it 'can move the item after an item at MAX_POSITION' do
+        item1.update!(relative_position: RelativePositioning::MAX_POSITION)
+
+        new_item.move_after(item1)
+        expect(new_item.relative_position).to be_valid_position
+        expect(new_item.relative_position).to be > item1.reset.relative_position
       end
 
       it 'moves items correctly' do
@@ -175,6 +453,53 @@ def create_items_with_positions(positions)
         expect(item1.relative_position).to be_between(item2.reload.relative_position, item3.reload.relative_position).exclusive
       end
     end
+
+    it 'can move the item after an item bunched up at MAX_POSITION' do
+      item1, item2, item3 = create_items_with_positions(run_at_end)
+
+      new_item.move_after(item1)
+      new_item.save!
+
+      items = [item1, new_item, item2, item3]
+
+      items.each do |item|
+        expect(item.reset.relative_position).to be_valid_position
+      end
+
+      expect(items.sort_by(&:relative_position)).to eq(items)
+    end
+
+    context 'leap-frogging' do
+      before do
+        start = RelativePositioning::START_POSITION
+        item1.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 0)
+        item2.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 1)
+        item3.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 2)
+      end
+
+      let(:item3) { create(factory, default_params) }
+
+      def leap_frog(steps)
+        a = item1
+        b = item2
+
+        steps.times do |i|
+          a.move_after(b)
+          a.save!
+          a, b = b, a
+        end
+      end
+
+      it 'can leap-frog STEPS times before needing to rebalance' do
+        expect { leap_frog(RelativePositioning::STEPS) }
+          .not_to change { item3.reload.relative_position }
+      end
+
+      it 'rebalances after leap-frogging STEPS+1 times' do
+        expect { leap_frog(RelativePositioning::STEPS + 1) }
+          .to change { item3.reload.relative_position }
+      end
+    end
   end
 
   describe '#move_to_end' do
@@ -193,8 +518,17 @@ def create_items_with_positions(positions)
 
   describe '#move_between' do
     before do
-      [item1, item2].each do |item1|
-        item1.move_to_end && item1.save
+      [item1, item2].each do |item|
+        item.move_to_end && item.save!
+      end
+    end
+
+    shared_examples 'moves item between' do
+      it 'moves the middle item to between left and right' do
+        expect do
+          middle.move_between(left, right)
+          middle.save!
+        end.to change { between_exclusive?(left, middle, right) }.from(false).to(true)
       end
     end
 
@@ -218,26 +552,26 @@ def create_items_with_positions(positions)
     end
 
     it 'positions items even when after and before positions are the same' do
-      item2.update relative_position: item1.relative_position
+      item2.update! relative_position: item1.relative_position
 
       new_item.move_between(item1, item2)
+      [item1, item2].each(&:reset)
 
       expect(new_item.relative_position).to be > item1.relative_position
       expect(item1.relative_position).to be < item2.relative_position
     end
 
-    it 'positions items between other two if distance is 1' do
-      item2.update relative_position: item1.relative_position + 1
+    context 'the two items are next to each other' do
+      let(:left) { item1 }
+      let(:middle) { new_item }
+      let(:right) { create_item(relative_position: item1.relative_position + 1) }
 
-      new_item.move_between(item1, item2)
-
-      expect(new_item.relative_position).to be > item1.relative_position
-      expect(item1.relative_position).to be < item2.relative_position
+      it_behaves_like 'moves item between'
     end
 
     it 'positions item in the middle of other two if distance is big enough' do
-      item1.update relative_position: 6000
-      item2.update relative_position: 10000
+      item1.update! relative_position: 6000
+      item2.update! relative_position: 10000
 
       new_item.move_between(item1, item2)
 
@@ -245,7 +579,8 @@ def create_items_with_positions(positions)
     end
 
     it 'positions item closer to the middle if we are at the very top' do
-      item2.update relative_position: 6000
+      item1.update!(relative_position: 6001)
+      item2.update!(relative_position: 6000)
 
       new_item.move_between(nil, item2)
 
@@ -253,51 +588,53 @@ def create_items_with_positions(positions)
     end
 
     it 'positions item closer to the middle if we are at the very bottom' do
-      new_item.update relative_position: 1
-      item1.update relative_position: 6000
-      item2.destroy
+      new_item.update!(relative_position: 1)
+      item1.update!(relative_position: 6000)
+      item2.update!(relative_position: 5999)
 
       new_item.move_between(item1, nil)
 
       expect(new_item.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE)
     end
 
-    it 'positions item in the middle of other two if distance is not big enough' do
-      item1.update relative_position: 100
-      item2.update relative_position: 400
+    it 'positions item in the middle of other two' do
+      item1.update! relative_position: 100
+      item2.update! relative_position: 400
 
       new_item.move_between(item1, item2)
 
       expect(new_item.relative_position).to eq(250)
     end
 
-    it 'positions item in the middle of other two is there is no place' do
-      item1.update relative_position: 100
-      item2.update relative_position: 101
-
-      new_item.move_between(item1, item2)
+    context 'there is no space' do
+      let(:middle) { new_item }
+      let(:left) { create_item(relative_position: 100) }
+      let(:right) { create_item(relative_position: 101) }
 
-      expect(new_item.relative_position).to be_between(item1.relative_position, item2.relative_position).exclusive
+      it_behaves_like 'moves item between'
     end
 
-    it 'uses rebalancing if there is no place' do
-      item1.update relative_position: 100
-      item2.update relative_position: 101
-      item3 = create_item(relative_position: 102)
-      new_item.update relative_position: 103
+    context 'there is a bunch of items' do
+      let(:items) { create_items_with_positions(100..104) }
+      let(:left) { items[1] }
+      let(:middle) { items[3] }
+      let(:right) { items[2] }
 
-      new_item.move_between(item2, item3)
-      new_item.save!
+      it_behaves_like 'moves item between'
+
+      it 'handles bunches correctly' do
+        middle.move_between(left, right)
+        middle.save!
 
-      expect(new_item.relative_position).to be_between(item2.relative_position, item3.relative_position).exclusive
-      expect(item1.reload.relative_position).not_to eq(100)
+        expect(items.first.reset.relative_position).to be < middle.relative_position
+      end
     end
 
-    it 'positions item right if we pass none-sequential parameters' do
-      item1.update relative_position: 99
-      item2.update relative_position: 101
+    it 'positions item right if we pass non-sequential parameters' do
+      item1.update! relative_position: 99
+      item2.update! relative_position: 101
       item3 = create_item(relative_position: 102)
-      new_item.update relative_position: 103
+      new_item.update! relative_position: 103
 
       new_item.move_between(item1, item3)
       new_item.save!
@@ -329,6 +666,12 @@ def create_items_with_positions(positions)
       expect(positions).to eq([90, 95, 96, 102])
     end
 
+    it 'raises an error if there is no space' do
+      items = create_items_with_positions(run_at_start)
+
+      expect { items.last.move_sequence_before }.to raise_error(RelativePositioning::NoSpaceLeft)
+    end
+
     it 'finds a gap if there are unused positions' do
       items = create_items_with_positions([100, 101, 102])
 
@@ -336,7 +679,8 @@ def create_items_with_positions(positions)
       items.last.save!
 
       positions = items.map { |item| item.reload.relative_position }
-      expect(positions).to eq([50, 51, 102])
+
+      expect(positions.last - positions.second).to be > RelativePositioning::MIN_GAP
     end
   end
 
@@ -358,7 +702,33 @@ def create_items_with_positions(positions)
       items.first.save!
 
       positions = items.map { |item| item.reload.relative_position }
-      expect(positions).to eq([100, 601, 602])
+      expect(positions.second - positions.first).to be > RelativePositioning::MIN_GAP
     end
+
+    it 'raises an error if there is no space' do
+      items = create_items_with_positions(run_at_end)
+
+      expect { items.first.move_sequence_after }.to raise_error(RelativePositioning::NoSpaceLeft)
+    end
+  end
+
+  def be_valid_position
+    be_between(RelativePositioning::MIN_POSITION, RelativePositioning::MAX_POSITION)
+  end
+
+  def between_exclusive?(left, middle, right)
+    a, b, c = [left, middle, right].map { |item| item.reset.relative_position }
+    return false if a.nil? || b.nil?
+    return a < b if c.nil?
+
+    a < b && b < c
+  end
+
+  def run_at_end(size = 3)
+    (RelativePositioning::MAX_POSITION - size)..RelativePositioning::MAX_POSITION
+  end
+
+  def run_at_start(size = 3)
+    (RelativePositioning::MIN_POSITION..).take(size)
   end
 end