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
π§ 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)
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
Fetching stats
RoundRobinAssignment.group_stats('support_team')
# => { last_assigned_user_id: 3, total_assignments: 120, last_assigned_at: "2025-10-20T14:12:00Z" }
π§ 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
:- It sorts the input list of assignee IDs.
- Finds or creates the DB record for that group.
- Calculates the next ID in order.
- 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
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
π‘ Design Principles
- Single Responsibility β does one thing: round-robin rotation
- Predictability β deterministic results for any input
- Persistence β works across deploys and multiple app servers
- Flexibility β no assumptions about your domain model
- 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'
Then run:
bundle install
rails g round_robin_assignment:install
rails db:migrate
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)