validates_overlap provides an ActiveRecord validator for resources that must not overlap, e.g. in datetime. Think rentals, meetings, bookings, work shifts, or assignments where the same resource cannot be assigned to multiple people or entities during overlapping time periods. But it also works for other domains than datetime (see below).
You specify two attributes defining a datetime range, such as starts_at and ends_at, and the validator checks with a single SQL query whether another record overlaps that range — no records are loaded for the comparison. If one does, the record receives a normal validation error.
It also supports scoped validation (per user, room, resource, etc.), open-ended ranges (a nil start or end counts as extending forever), ranges that may touch at their boundaries (exclude_edges), required gaps between ranges or a tolerated amount of overlap (start_shift / end_shift), associations, and retrieving the conflicting records.
Add to your gemfile
gem 'validates_overlap'In your model
validates :starts_at, :ends_at, :overlap => true
# or scoped, e.g. per user:
validates :starts_at, :ends_at, :overlap => {:scope => "user_id"}All options — scopes, edge handling, gaps and tolerated overlap, custom messages, associations, retrieving the conflicting records — are described in the Option Reference.
- Examples and Introduction
- Option Reference
- Range Types and Domains
- PostgreSQL: Exclusion Constraints
The range columns don't have to be dates or times: any linearly orderable column type works, such as integer ranges (ticket number blocks), decimal ranges (price bands), or string ranges (alphabetical partitions). :time columns outright. Range Types and Domains explains both halves.
Validation alone can not prevent double-booking under concurrent writes: two simultaneous requests can both pass the check and both save — the same limitation as validates_uniqueness_of. On PostgreSQL, the gem's migration helpers generate an exclusion constraint that closes this race at the database level:
add_overlap_constraint :meetings, :starts_at, :ends_at, scope: :user_idSee PostgreSQL: Exclusion Constraints for the helpers, the companion concern that turns the constraint violation into a normal validation error, and the equivalent hand-written SQL.
The validation runs one EXISTS query per save. Without a suitable index that query is a full table scan — invisible at 1,000 rows, painful at 1,000,000. Add a composite index with your scope columns first, then the range columns:
add_index :meetings, [:user_id, :starts_at, :ends_at]For unscoped validation, index the range columns alone ([:starts_at, :ends_at]). If you added the PostgreSQL exclusion constraint, you already have a suitable index — the constraint is backed by a GiST index that serves overlap queries. On large tables, verify with EXPLAIN that the query actually uses your index.
Every combination below is verified on every push by the CI matrix:
| Rails | Tested with Ruby |
|---|---|
| 8.1 | 3.2, 3.3, 3.4 |
| 8.0 | 3.2, 3.3, 3.4 |
| 7.2 | 3.1, 3.2, 3.3, 3.4 |
| 7.1 | 3.0, 3.1, 3.2, 3.3 |
| 7.0 | 3.0, 3.1, 3.2 |
| 6.1 | 3.0 |
The gemspec requires activerecord >= 6.0. Rails 6.0 is not part of the test matrix, but no incompatibilities are known. The previous version 0.8.6 was compatible with Rails 3, 4, and 5.
Note for MySQL users: use DATETIME (not TIMESTAMP) columns for your range attributes — MySQL's TIMESTAMP type cannot store dates after January 2038, which matters for long-running or far-future ranges. PostgreSQL and SQLite date/time types have no such limit.
validates_overlap was created by Robin Bortlik, who built and maintained it starting 2011.
Since August 2026 the gem is maintained by Tilo Sloboda.
A big thank you to Robin for creating this awesome gem and for the years of work he put into it. ❤️