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_assignmentsin 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