NoSQL Transactions: Concurrency Control
The Busy Kitchen Story
Imagine a super busy restaurant kitchen. Many cooks are working at the same time. They all need to grab ingredients, use the stove, and plate dishes. But what happens when two cooks reach for the last tomato at the same time? Chaos!
That’s exactly what happens in databases when many users try to change the same data at once. We call this concurrency — multiple things happening together.
Today, we’ll learn how NoSQL databases keep the kitchen running smoothly, even when it’s packed with cooks!
1. Optimistic Concurrency
The “Hope for the Best” Approach
Think of it like this: You’re coloring a picture in a coloring book. Instead of asking your sister “Can I use this page?”, you just start coloring. You hope she won’t try to color the same page.
How it works:
- You read the data (pick up the coloring page)
- You make your changes (start coloring)
- Before saving, you check: “Did anyone else change this while I was working?”
- If yes → Start over. If no → Save your work!
graph TD A["Read Data + Version"] --> B["Make Changes"] B --> C{Version Still Same?} C -->|Yes| D["Save Changes!"] C -->|No| E["Someone Changed It!"] E --> F["Try Again"] F --> A
Real Example
// You want to update a user's score
let user = db.get("player123")
// user = {name: "Alex", score: 100, version: 5}
// You add 10 points
user.score = 110
// Try to save, but only if version is still 5
db.update("player123", user, {
ifVersion: 5
})
// If someone else changed it, version would be 6
// Your save would FAIL — and that's good!
Why use it? It’s fast! You don’t lock anything. Most of the time, nobody else is changing the same data anyway.
2. Conflict Resolution Strategies
When Two Cooks Grab the Same Tomato
Sometimes, despite our best hopes, two people DO change the same thing. Now what? We need rules to decide who wins!
Strategy 1: Last Write Wins (LWW)
The simplest rule: whoever saves last, wins.
Like two kids drawing on the same whiteboard. The last drawing is what stays.
// 10:00 AM - Alice sets price to $10
product.price = 10
// 10:01 AM - Bob sets price to $15
product.price = 15
// Result: Price is $15 (Bob wrote last)
Downside: Alice’s change is lost forever!
Strategy 2: Merge Changes
Instead of picking a winner, combine both changes!
// Alice adds "milk" to shopping list
// Bob adds "eggs" to shopping list
// MERGE result:
shoppingList = ["milk", "eggs"]
// Both items are kept!
Strategy 3: Ask the User
When the database can’t decide, ask the human!
"Oops! Someone else changed this too."
"Your version: $10"
"Their version: $15"
"Which one should we keep?"
Strategy 4: Custom Rules
You make your own rules based on your app’s needs.
// Rule: Always keep the HIGHER score
if (aliceScore > bobScore) {
keep(aliceScore)
} else {
keep(bobScore)
}
3. Data Versioning
The Magic Sticker System
Remember how we checked “is the version still the same?” Let’s understand versions better!
Think of it like putting a sticker number on every change. Each time something changes, you add a new sticker with a higher number.
Version 1: {name: "Sam", age: 8} // Created
Version 2: {name: "Sam", age: 9} // Birthday!
Version 3: {name: "Sammy", age: 9} // Nickname update
Types of Version Markers
Simple Number (Counter)
{
data: {name: "Luna", color: "purple"},
version: 42 // Just a number that goes up
}
Timestamp
{
data: {name: "Luna", color: "purple"},
updatedAt: "2024-01-15T10:30:00Z" // When it changed
}
Vector Clock (for distributed systems)
{
data: {name: "Luna"},
vectorClock: {
serverA: 3, // Server A made 3 changes
serverB: 2 // Server B made 2 changes
}
}
Why Versioning Matters
graph TD A["You Read: Version 5"] --> B["You Edit"] B --> C["Try to Save"] C --> D{Current Version?} D -->|Still 5| E["Save as Version 6"] D -->|Now 6| F["CONFLICT!"] F --> G["Re-read and Try Again"]
Without versions, you’d overwrite other people’s work without knowing!
4. Idempotent Operations
The “Safe to Repeat” Trick
Big word alert! Idempotent means: doing something twice gives the same result as doing it once.
Everyday Examples
Idempotent (Safe to repeat):
- Turning ON a light switch (already on? Still on!)
- Setting your age to 10 (do it 100 times, still 10)
- Deleting a file (delete twice? Still gone)
NOT Idempotent (Changes each time):
- Adding $5 to your piggy bank (do it twice = $10!)
- Liking a post (like twice = unlike!)
In Database World
// NOT idempotent - dangerous!
user.balance = user.balance + 10
// Run twice = added $20 instead of $10!
// IDEMPOTENT - safe!
user.balance = 110 // Set to exact value
// Run twice = still $110
Making Operations Safe
Bad (not idempotent):
db.update({
$increment: {views: 1} // Add 1 to views
})
// Network glitch? Might run twice!
Good (idempotent):
db.update({
$addToSet: {
viewedBy: "user123" // Add user to list
}
})
// Run 100 times, user only appears once!
The Request ID Trick
// Give each request a unique ID
const requestId = "abc-123-xyz"
// Before processing, check:
if (alreadyProcessed(requestId)) {
return previousResult // Don't do it again!
}
// Process and remember this ID
processRequest()
markAsProcessed(requestId)
5. Saga Pattern
The Adventure Story Approach
Imagine you’re planning a birthday party. You need to:
- Book the venue
- Order the cake
- Hire a magician
- Send invitations
What if the magician cancels? Do you cancel EVERYTHING? That’s expensive! Instead, you handle each problem step by step.
Saga is exactly this: a series of steps where each step can be undone if something goes wrong.
graph TD A["Book Venue"] -->|Success| B["Order Cake"] B -->|Success| C["Hire Magician"] C -->|FAILS!| D["Cancel Cake"] D --> E["Cancel Venue"] E --> F["Party Cancelled"] C -->|Success| G["Send Invites"] G --> H["Party Ready!"]
Real Example: Online Shopping
// The Saga for buying something online:
// Step 1: Reserve the item
reserveItem("toy-123")
// Step 2: Charge the credit card
try {
chargeCard("$25")
} catch (error) {
// UNDO Step 1!
unreserveItem("toy-123")
return "Payment failed"
}
// Step 3: Ship the package
try {
shipPackage()
} catch (error) {
// UNDO Step 2!
refundCard("$25")
// UNDO Step 1!
unreserveItem("toy-123")
return "Shipping failed"
}
return "Order complete!"
Two Types of Sagas
Choreography (Everyone knows their part)
Each service watches for events and reacts:
- Inventory sees "order placed" → reserves item
- Payment sees "item reserved" → charges card
- Shipping sees "payment done" → ships package
Orchestration (One boss gives orders)
A central controller tells everyone what to do:
- Controller: "Inventory, reserve item!"
- Controller: "Payment, charge card!"
- Controller: "Shipping, send package!"
Compensating Actions
Every forward step needs a “rewind” step:
| Forward Action | Compensating Action |
|---|---|
| Reserve item | Release item |
| Charge card | Refund card |
| Ship package | Request return |
| Send email | Send “oops” email |
Summary: Keeping the Kitchen Running
| Concept | Simple Explanation |
|---|---|
| Optimistic Concurrency | Hope for the best, check before saving |
| Conflict Resolution | Rules for when two changes collide |
| Data Versioning | Sticker numbers to track changes |
| Idempotent Operations | Safe to accidentally repeat |
| Saga Pattern | Step-by-step with undo buttons |
The Big Picture
graph TD A["Many Users"] --> B["Same Data"] B --> C{Conflict?} C -->|Prevent| D["Optimistic Concurrency"] C -->|Detect| E["Data Versioning"] C -->|Resolve| F["Conflict Resolution"] D --> G["Safe Updates"] E --> G F --> G G --> H["Idempotent Ops"] H --> I["Saga Pattern"] I --> J["Happy Database!"]
Now you’re ready to handle a busy database kitchen like a pro chef! Remember: it’s all about checking before you save, having rules for conflicts, tracking versions, making operations safe to repeat, and having an undo plan.
You’ve got this!
