DEV Community

Mustafa Duranovic
Mustafa Duranovic

Posted on

πŸ’Ž Introducing round_robin_assignment: A Reliable Round-Robin Assignment Gem for Rails

In many web applications β€” support systems, lead management, code reviews, or on-call rotations β€” there’s a recurring need to assign work evenly across a group of people.

I built round_robin_assignment to solve that problem in a clean, persistent, and concurrency-safe way for Ruby on Rails projects.

This gem encapsulates a database-backed round-robin assignment algorithm that works across multiple app instances and survives restarts β€” no more reinventing the wheel for fair task rotation.


πŸš€ Why I Built It

I’ve seen many Rails apps try to β€œjust rotate” assignments in memory, or store a pointer somewhere ad hoc β€” until concurrency, scaling, or team changes break it.

round_robin_assignment is meant to be the opposite:

βœ… Simple API

βœ… Persistent in the database

βœ… Handles multiple groups

βœ… Adapts to changing members

βœ… Thread-safe and well-tested

Whether you’re distributing tickets, leads, or PRs, you can rely on a consistent next-in-line mechanism.


βš™οΈ What the Gem Does

✳️ Core Features

  • Persistent state β€” assignment state lives in your DB, not in memory
  • Multiple groups β€” each queue (support, sales, reviews, etc.) is tracked separately
  • Dynamic membership β€” new members join or leave seamlessly
  • Concurrency safety β€” database transactions prevent race conditions
  • Statistics & reset β€” inspect or reset rotation state
  • RSpec coverage β€” the gem ships with a complete test suite

πŸ‘‰ View it on GitHub β†’


🧭 Common Use Cases

  • πŸ§‘β€πŸ’» Support ticket rotation
  • πŸ’Ό Sales lead distribution
  • πŸ” Code review assignment
  • πŸ• On-call scheduling
  • βš™οΈ Load balancing any repeated task

Wherever tasks need to be distributed fairly β€” this gem fits.


🧩 Example Usage

Basic rotation

# Suppose your support agents have IDs [1, 2, 3]
RoundRobinAssignment.get_next_assignee('support_team', [1, 2, 3])
# => 1
RoundRobinAssignment.get_next_assignee('support_team', [1, 2, 3])
# => 2
RoundRobinAssignment.get_next_assignee('support_team', [1, 2, 3])
# => 3
RoundRobinAssignment.get_next_assignee('support_team', [1, 2, 3])
# => 1  (cycles again)
Enter fullscreen mode Exit fullscreen mode

Dynamic member updates

# Remove or add users without breaking the rotation
RoundRobinAssignment.get_next_assignee('team', [1, 2, 3]) # => 1
RoundRobinAssignment.get_next_assignee('team', [1, 2, 3]) # => 2

# User 2 leaves
RoundRobinAssignment.get_next_assignee('team', [1, 3])    # => 3
RoundRobinAssignment.get_next_assignee('team', [1, 3])    # => 1

# User 4 joins
RoundRobinAssignment.get_next_assignee('team', [1, 3, 4]) # => 3
RoundRobinAssignment.get_next_assignee('team', [1, 3, 4]) # => 4
Enter fullscreen mode Exit fullscreen mode

Fetching stats

RoundRobinAssignment.group_stats('support_team')
# => { last_assigned_user_id: 3, total_assignments: 120, last_assigned_at: "2025-10-20T14:12:00Z" }
Enter fullscreen mode Exit fullscreen mode

🧠 How It Works Under the Hood

  • The gem defines a table round_robin_assignments in your database.
  • Each group (like "support_team") has a single record.
  • When you call get_next_assignee:
    1. It sorts the input list of assignee IDs.
    2. Finds or creates the DB record for that group.
    3. Calculates the next ID in order.
    4. Locks and updates the record transactionally to ensure safe concurrent writes.

The logic is simple, deterministic, and reliable β€” perfect for production use.


πŸ” Example Integration

Here’s how you might wire it into your Rails app:

class TicketAssignmentService
  def self.assign(ticket)
    agent_ids = User.support_agents.pluck(:id)
    next_agent = RoundRobinAssignment.get_next_assignee('support_tickets', agent_ids)
    ticket.update!(assignee_id: next_agent)
  end
end
Enter fullscreen mode Exit fullscreen mode

Or for code reviews:

class PullRequestService
  def assign_reviewer(pr)
    reviewers = pr.eligible_reviewers.pluck(:id)
    reviewer_id = RoundRobinAssignment.get_next_assignee("reviews_#{pr.repo_id}", reviewers)
    pr.update!(reviewer_id: reviewer_id)
  end
end
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Design Principles

  1. Single Responsibility β€” does one thing: round-robin rotation
  2. Predictability β€” deterministic results for any input
  3. Persistence β€” works across deploys and multiple app servers
  4. Flexibility β€” no assumptions about your domain model
  5. Extendable β€” easy to add weights, schedules, or history later

πŸ›£οΈ Future Ideas

  • 🎯 Weighted round-robin β€” prefer some users more often
  • ⏰ Time-based skipping β€” ignore users off-duty or on vacation
  • πŸ“ˆ Assignment analytics β€” visualize who’s getting how many tasks
  • 🧩 Admin dashboard β€” manage and reset rotations
  • ⚑ Redis cache layer β€” optimize for extremely high concurrency

If any of those sound interesting, feel free to open an issue or contribute!


🧰 Installation

Add to your Gemfile:

gem 'round_robin_assignment'
Enter fullscreen mode Exit fullscreen mode

Then run:

bundle install
rails g round_robin_assignment:install
rails db:migrate
Enter fullscreen mode Exit fullscreen mode

Now you’re ready to assign fairly and predictably.


🀝 Contribute or Get Involved

The gem is open source at:

πŸ‘‰ github.com/mustafa90/round_robin_assignment

I’d love feedback, ideas, and PRs β€” especially around edge cases or advanced features like weighted logic.


✨ Final Thoughts

Fair task distribution sounds simple β€” until it’s not.

When concurrency, persistence, and fairness matter, round_robin_assignment gives you a clean, battle-tested solution.

Use it to power your next assignment workflow β€” and focus on your business logic, not rotation logic.


✍️ Written by Mustafa DuranoviΔ‡

πŸ’Ž Open source contributor & Rails developer

Top comments (0)