Spot the Tax · Card 1 of 20
Uniqueness lives in the database, not in valid?
Why validates :uniqueness can't actually prevent duplicates once your traffic gets concurrent.
The code
What will this cost you in six months?
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
end
# In the controller:
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render :new
end
end The problem
When two users try to sign up with the same email at the exact same moment, both requests will run valid? and ask the database whether that email already exists. Since neither request has actually inserted anything yet, both checks come back clean and both saves go through. You end up with two users sharing the same email, even though the validation did exactly what it was supposed to do — it just didn't have the right vantage point to actually prevent it.
Take a moment. Before revealing, try to work out the fix yourself. Where would you put the rule? What would it look like?
The solution
Move the uniqueness rule down into the database itself, by adding a unique index on the column. The database is the only thing in the stack that has visibility over every concurrent write at once, so it's the only thing that can actually enforce "no two rows can share this value". The validation can stay on the model for the friendly error message, but the database is what guarantees correctness.
- The rule holds under any amount of concurrent traffic
- The database raises a specific error you can catch
- The validation can stay for the friendly UI message
# In a migration:
add_index :users, :email, unique: true The principle at play — Database constraints
When two web requests come in at the same instant, Rails handles them in parallel on different processes. Each one is doing its own thing in isolation, and neither one can see what the other is currently working on. So when both run the uniqueness validation at almost the same moment, both get the same answer — even though only one of them is actually allowed to succeed.
The database, on the other hand, is the one place in your system that sees every write happening across every process at once. When you tell it "this column has to be unique", it's the only thing that can actually enforce that, because it's the only thing that can compare a new write against everything else happening at the same moment.
That's why uniqueness has to live in the database. It's not that the Ruby validation is wrong, it's that Ruby doesn't have the right vantage point to enforce uniqueness on its own. The database does, so that's where the rule belongs.