Understanding Atomic Operations and Mutex Locks for Better Concurrency


Introduction
Concurrency is one of those topics that looks simple on paper but gets messy in real life. If you’ve ever worked on backend systems that deal with job queues, counters, or shared resources, you’ve likely stumbled upon issues where two processes try to modify the same value at the same time.
In my journey building GoQueue (a lightweight job queue library in Go), I faced this exact dilemma: How do I ensure jobs are processed safely without stepping on each other’s toes? That’s when I had to dig deeper into atomic operations vs. mutex locks.
But before diving into code, let’s bring this into a world we all understand.
The Analogy: One Coffee Machine, Many Engineers
Imagine an office with one coffee machine but 20 caffeine-hungry engineers.
Each engineer represents a thread.
The coffee machine represents a shared resource.
Now, let’s see what happens:
No Rules (Race Condition)
All engineers rush at once.
Some get coffee, some spill it, some double-fill, and some end up with none.
This is your race condition.
Atomic Operation (Fast Button Press)
The machine has a button that ensures only one cup is dispensed per press.
Engineers can still press quickly, one after another, but each press is guaranteed to pour exactly one coffee without overlap.
This is atomicity: a guarantee that the action happens as a single, indivisible step.
Mutex Lock (Taking Turns)
The office sets a rule: Only one engineer can use the coffee machine at a time.
An engineer locks the door, makes coffee peacefully, and then unlocks it.
This is a mutex lock — slower than atomic operations, but it ensures complete safety even if multiple steps are needed.
Where This Applies in Backend Systems
Now let’s map this analogy to the real world of backend development:
Atomic Operation Example (Counters, Job IDs)
Suppose you’re keeping track of the number of jobs processed in your queue:import ( "fmt" "sync/atomic" ) func main() { var counter int64 = 0 for i := 0; i < 1000; i++ { go func() { atomic.AddInt64(&counter, 1) }() } }
Here,
atomic.AddInt64
ensures every increment is safe. Even if 1000 goroutines run at once, you’ll never “lose a cup of coffee.”Perfect for: counters, flags, and operations that require just one step.
Mutex Lock Example (Job Queues, Shared Memory)
But what if making coffee isn’t just pressing a button? What if you need to:
Add sugar
Add milk
Stir
Now, multiple steps are required. If two engineers try this at the same time, the machine ends up a mess.
In Go, that looks like:
import (
"fmt"
"sync"
)
func main() {
var counter int64 = 0
var mu sync.Mutex
for i := 0; i < 1000; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
}
Here, the mutex ensures only one goroutine modifies the counter at a time — safe even if there are multiple steps inside the lock.
Perfect for: complex operations that can’t be made atomic in one shot.
Why Atomicity Matters in Real Life
Think of a banking system:
Atomic operation is like transferring money between two accounts with a single debit-credit instruction. It’s one indivisible step.
Mutex lock is like updating multiple tables (account balance, transaction history, notification queue). Since this needs multiple steps, you lock until all are complete.
Without these, you risk:
Double-spending
Missing transactions
Corrupted states
Exactly the kind of problems that can bring down production systems.
Which One Should You Use?
Use Atomic Operations when:
You need speed.
The operation can be done in a single step (e.g., counters, flags, indexes).
Use Mutex Locks when:
The operation involves multiple steps.
Safety is more important than raw speed.
Example: pushing jobs into a queue, updating multiple variables, or reading-modifying-writing shared state.
Conclusion
Concurrency isn’t about writing code that “runs faster.” It’s about writing code that runs correctly when multiple things happen at once.
Atomic operations are like pressing a coffee machine button — fast, single-step, and safe.
Mutex locks are like waiting your turn at the coffee machine — slower, but necessary when multiple steps are involved.
In my GoQueue project, I had to balance both. Some parts worked perfectly with atomics (like counters), while others needed mutex locks (like managing job state).
The key lesson? Choose the right tool for the right scenario. That’s what separates a working system from a broken one.
Subscribe to my newsletter
Read articles from Saravana Sai directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Saravana Sai
Saravana Sai
I am a self-taught web developer interested in building something that makes people's life awesome. Writing code for humans not for dump machine