When you have a read-modify-write pattern like incrementing a counter, you’re vulnerable to race conditions:
# BAD: Race condition - two concurrent requests could both read likes=5,
# compute 6, and write 6, losing one like
post = Repo.get_by!(Post, slug: slug)
likes = post.likes + 1
changeset = Post.changeset(post, %{likes: likes})
Repo.update!(changeset)
Instead, use Repo.update_all with the inc option for atomic increment:
# GOOD: Atomic increment - PostgreSQL executes SET likes = likes + 1
from(p in Post, where: p.slug == ^slug, select: %{max_likes: p.max_likes})
|> Repo.update_all(
inc: [likes: 1],
set: [max_likes: fragment("GREATEST(likes + 1, max_likes)")]
)
This generates UPDATE ... SET likes = likes + 1 which PostgreSQL executes atomically, eliminating the race condition.
Note: You’ll need import Ecto.Query to use the from/2 macro and fragment/1 for raw SQL expressions.