Most developers encounter concurrency as a problem they need to fix.
Two users edit the same record.
Two requests arrive at the same time.
One update overwrites another.
A transaction reads data that changes before it finishes.
Then someone says:
"We need locking."
And suddenly the code becomes full of mutexes, locks, retries, and complicated transaction logic.
But there is another way to think about concurrency.
Instead of asking:
"How do I stop everyone from touching the same data?"
we can ask:
"What if everyone gets a consistent view of the data while the database manages the conflicts?"
That is where MVCC — Multi-Version Concurrency Control — becomes interesting.
MVCC is one of those database concepts that looks like an implementation detail until you start building APIs around it.
Then you realize something strange:
MVCC can become part of the API's concurrency model.
Instead of exposing only:
GET /users/42
PATCH /users/42
we can build an API that understands:
versions
snapshots
concurrent writes
conflicts
optimistic concurrency
transactions
read consistency
The API stops pretending that data is static.
It starts acknowledging that multiple clients are changing reality simultaneously.
And that's where things get interesting.
1. The Problem MVCC Solves
Imagine two clients editing the same document.
Client A
|
| GET document
v
Version 10
Client B
|
| GET document
v
Version 10
Both clients see version 10.
Client A changes the title: ""
Title = "Distributed Systems"
Client B changes the author:
Author = "Derek"
Now both submit updates.
Without concurrency control:
Version 10
|
+---- Client A
|
+---- Client B
One update might overwrite the other.
This is called a lost update.
The database needs some way to reason about concurrent versions.
That's where MVCC enters.
2. What Is MVCC?
Multi-Version Concurrency Control means that the database can maintain multiple versions of data rather than treating every row as one mutable object.
Conceptually:
users.id = 42
Version 1
name = Derek
version = 1
Version 2
name = Derek Mwale
version = 2
Version 3
name = Derek Mwale
email = derek@example.com
version = 3
Different transactions can observe different versions depending on when they started and what isolation level they're using.
Instead of:
One row
|
v
Overwrite
we can think:
+--> Version 1
|
Record ------+--> Version 2
|
+--> Version 3
The database determines which version is visible to a transaction.
This is powerful because readers don't necessarily have to block writers.
And writers don't necessarily have to block readers.
That's one of the central ideas behind MVCC.
3. Why This Matters for APIs
An API is a concurrency boundary.
Imagine thousands of clients interacting with:
GET /products/42
PATCH /products/42
The API doesn't control when clients read the resource.
Client A could read at:
10:00:01
Client B could read at:
10:00:02
Then both could write at:
10:00:05
The API therefore needs a way to determine:
Is this client updating the version it actually read?
This is where MVCC and API design meet.
We can expose the database's notion of version to clients.
For example:
{
"id": 42,
"name": "Derek",
"version": 7
}
Now version becomes part of the API contract.
4. Optimistic Concurrency Control
Suppose the client retrieves:
{
"id": 42,
"name": "Derek",
"version": 7
}
The client edits the record.
It sends:
PATCH /users/42
If-Match: 7
with:
{
"name": "Derek Mwale"
}
The server effectively says to the database:
UPDATE users
SET name = 'Derek Mwale',
version = version + 1
WHERE id = 42
AND version = 7;
If one row is updated:
Success
If zero rows are updated:
Conflict
Why?
Because somebody changed the record first.
The API can return:
409 Conflict
This is optimistic concurrency control.
The API doesn't lock the record while the user is editing it.
It simply verifies that the version hasn't changed before accepting the update.
5. The API Becomes Version-Aware
A traditional API might return:
{
"id": 42,
"name": "Derek"
}
An MVCC-aware API might return:
{
"id": 42,
"name": "Derek",
"version": 17
}
That tiny field changes the semantics of the API.
Now the client has a snapshot identity.
The client isn't merely holding:
User 42
It is holding:
User 42 @ Version 17
That is a much more precise statement.
6. ETags Are Basically Version Tokens
HTTP already gives us an elegant mechanism for this.
The server could return:
ETag: "user-42-v17"
The client later sends:
If-Match: "user-42-v17"
The server checks whether the resource is still version 17.
If yes:
update
If not:
412 Precondition Failed
Conceptually:
Client
|
| GET
v
Resource v17
|
| edit
v
If-Match: v17
|
v
Server
|
+--> still v17 -> update
|
+--> now v18 -> reject
This is an incredibly clean bridge between HTTP semantics and database concurrency control.
7. MVCC Gives Us More Than Versions
The real power of MVCC isn't just:
version = 17
It is the idea of snapshots.
Imagine a transaction starts at time T1.
T1
|
v
Snapshot S1
While the transaction is running, another transaction modifies the database.
S1
|
+---- Transaction A
|
+---- Transaction B modifies data
Transaction A can continue seeing a consistent snapshot depending on the database's isolation semantics.
This is incredibly useful for APIs that perform complex reads.
Imagine:
GET /analytics/dashboard
The endpoint might query:
orders
payments
customers
inventory
If every query observes a different state, the dashboard could become internally inconsistent.
A consistent transaction snapshot can help.
8. Building a Snapshot API
Imagine exposing:
POST /snapshots
The server returns:
{
"snapshot_id": "snap-93821",
"created_at": "2026-08-21T08:30:00Z"
}
Then:
GET /users/42?snapshot=snap-93821
and:
GET /orders?snapshot=snap-93821
Both reads could conceptually refer to the same logical point in time.
Now the API supports:
"Show me the world as it looked at this snapshot."
This can be extremely powerful for reporting systems, debugging, auditing, and long-running reads.
9. But Don't Expose Database Internals Blindly
There is an important warning here.
MVCC implementation details differ between databases.
PostgreSQL, MySQL/InnoDB, SQL Server, and other systems don't implement MVCC identically.
Therefore an API shouldn't necessarily expose:
PostgreSQL transaction ID
or:
internal tuple version
Instead, create an application-level concurrency token.
For example:
{
"id": 42,
"version": 18
}
or:
ETag: "8f3a91..."
The database can internally use MVCC.
The API exposes a stable abstraction.
That's an important architectural boundary.
10. A Simple Architecture
We can build the system like this:
Client
|
v
+-----------+
| API Layer |
+-----------+
|
v
+-------------+
| Concurrency |
| Controller |
+-------------+
|
v
+-------------+
| Transaction |
+-------------+
|
v
+-------------+
| MVCC DB |
+-------------+
The concurrency controller translates API-level concepts into database-level operations.
For example:
If-Match: version-17
|
v
WHERE version = 17
|
v
UPDATE
This is where the API becomes interesting.
It isn't simply forwarding SQL.
It is translating distributed client behavior into transactional database semantics.
11. Implementing the Update
Suppose our table looks like:
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
version BIGINT NOT NULL DEFAULT 1,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
The client reads:
{
"id": 10,
"title": "Distributed Systems",
"content": "...",
"version": 4
}
The client updates:
PATCH /documents/10
If-Match: "4"
The server executes:
UPDATE documents
SET
title = $1,
content = $2,
version = version + 1,
updated_at = NOW()
WHERE
id = $3
AND version = $4;
Then inspect the affected rows.
1 row -> success
0 rows -> conflict
That is a very small amount of code.
But conceptually, we just created optimistic concurrency at the API boundary.
12. What Happens During a Conflict?
Suppose both clients read:
version = 4
Client A updates first.
4 -> 5
Client B then tries:
WHERE version = 4
No row matches.
The API returns:
409 Conflict
with something like:
{
"error": "VERSION_CONFLICT",
"message": "The resource has changed since you last read it.",
"current_version": 5
}
Now the client can react.
Maybe it fetches the latest version.
Maybe it merges changes.
Maybe it asks the user.
Maybe the agent automatically retries.
The API doesn't silently destroy data.
13. Automatic Merge
Here's where it gets more interesting.
Suppose the original document is:
{
"title": "Distributed Systems",
"author": "Derek"
}
Client A changes:
title
Client B changes:
author
A simplistic version check rejects B.
But a smarter API might detect that the fields don't conflict.
Conceptually:
Original
|
+---- Client A changes title
|
+---- Client B changes author
The server can merge:
{
"title": "Distributed Systems",
"author": "Derek Mwale"
}
Now MVCC becomes the foundation for more sophisticated conflict resolution.
This is particularly interesting for collaborative software.
14. MVCC and Collaborative Editing
Think about a document editor.
Ten users may be editing the same document.
A lock-based model might say:
User A editing
|
v
LOCK DOCUMENT
Everyone else waits.
MVCC gives us a different model.
Each user can work against a version.
Version 10
|
+---- User A
+---- User B
+---- User C
When users commit changes, the system compares versions.
This can be combined with:
operation logs
patches
conflict resolution
CRDTs
event sourcing
MVCC isn't enough by itself to build Google Docs.
But it provides a powerful foundation for reasoning about concurrent state.
15. API Reads Need a Consistency Model
Once we expose concurrent data, we should ask:
What does a GET actually guarantee?
There are many possibilities.
READ_COMMITTED
REPEATABLE_READ
SERIALIZABLE
Different APIs may choose different semantics.
For example:
GET /reports/monthly
Consistency: snapshot
might mean:
Every part of this response should represent a consistent view.
Meanwhile:
GET /notifications
may not require strict consistency.
This is important because consistency has a cost.
The API should not promise more than the underlying system needs to provide.
16. Long-Running APIs and MVCC
Imagine an API:
POST /reports/generate
The report takes five minutes.
The database changes during those five minutes.
What data should the report contain?
Data at:
start time?
Data at:
completion time?
Or a consistent snapshot?
A snapshot-based approach can provide:
Report execution
|
v
Snapshot at T1
|
+--> Query A
+--> Query B
+--> Query C
+--> Query D
Every query sees the same logical database state.
Now your API can provide reproducible reports.
That's a powerful property.
17. MVCC and Event-Driven APIs
We can combine MVCC with events.
Suppose:
UPDATE users
creates:
User version 18
The system emits:
{
"event": "user.updated",
"user_id": 42,
"version": 18
}
Consumers can track versions.
Consumer version: 17
Event version: 18
The consumer knows it is behind.
This can help with synchronization and recovery.
For example:
version 18
version 19
version 20
If the consumer misses version 19, it can detect the gap.
That gives us another important distributed systems concept:
versioned state is easier to synchronize.
18. MVCC and Idempotency
MVCC also interacts beautifully with idempotency.
Suppose a request carries:
operation_id = 93821
The API stores the operation result.
Then the client retries.
The server can check:
operation_id 93821
and determine whether the operation has already been processed.
This can be wrapped inside a transaction.
Conceptually:
BEGIN
check idempotency key
if already processed:
return stored result
perform operation
store result
COMMIT
Now retries become much safer.
Again, we're connecting:
API design
+
transactions
+
MVCC
+
distributed failure
19. MVCC Doesn't Eliminate Locks
This is an important misconception.
MVCC does not mean:
"There are no locks."
Databases still use locks internally for various operations.
MVCC mainly changes how reads and writes interact.
Readers can often see an appropriate version without blocking writers in the traditional way.
But there are still:
row locks
predicate locks
metadata locks
advisory locks
serialization conflicts
The correct mental model isn't:
MVCC = lock-free database
It's:
MVCC = version-based concurrency model
That distinction matters.
20. MVCC and Serialization Failures
At higher isolation levels, the database may detect that concurrent transactions cannot safely coexist.
For example:
Transaction A
|
+---- reads X
|
+---- writes Y
Transaction B
|
+---- reads Y
|
+---- writes X
The database may abort one transaction.
The API must be prepared for this.
It might receive an error such as:
serialization_failure
The correct response may be:
retry transaction
But only if the operation is safe to retry.
Again:
the database concurrency model leaks into API behavior.
21. Building a Version-Aware API in Rust
A simple Rust model could be:
struct Resource {
id: i64,
version: i64,
name: String,
}
An update request:
struct UpdateRequest {
id: i64,
expected_version: i64,
name: String,
}
The service logic:
async fn update_user(
db: &Db,
request: UpdateRequest,
) -> Result<User, ApiError> {
let result = sqlx::query!(
r#"
UPDATE users
SET name = $1,
version = version + 1
WHERE id = $2
AND version = $3
RETURNING id, name, version
"#,
request.name,
request.id,
request.expected_version
)
.fetch_optional(db)
.await?;
match result {
Some(user) => Ok(user.into()),
None => Err(ApiError::Conflict),
}
}
Notice what's happening.
The API doesn't lock the user while someone edits it.
It doesn't care how long the client took.
It simply says:
Update this record if the world is still the version I observed.
That is a beautiful concurrency primitive.
22. MVCC Turns Time Into Data
This is perhaps the most interesting philosophical aspect.
Traditional CRUD encourages us to think:
User 42
MVCC encourages us to think:
User 42 at version 10
User 42 at version 11
User 42 at version 12
The resource becomes temporal.
It has history.
It has lineage.
It has a relationship with previous states.
And APIs can use that.
For example:
GET /users/42/history
could expose:
{
"versions": [
{
"version": 10,
"name": "Derek"
},
{
"version": 11,
"name": "Derek Mwale"
},
{
"version": 12,
"name": "Derek Mwale",
"email": "derek@example.com"
}
]
}
Now the API becomes partially temporal.
23. A Better API Architecture
A serious MVCC-aware API might look like:
Client
|
v
+---------------+
| API Gateway |
+---------------+
|
v
+---------------+
| Version Check |
+---------------+
|
v
+---------------+
| Transaction |
| Manager |
+---------------+
|
v
+---------------+
| Service Layer |
+---------------+
|
v
+---------------+
| MVCC Database |
+---------------+
|
v
+---------------+
| Event Stream |
+---------------+
The API can expose:
GET resource
PATCH resource with version
GET resource history
GET snapshot
POST transaction
Not every system needs all of these.
But the architecture becomes much more explicit about concurrency.
24. The Strange Connection Between APIs and Databases
We often separate:
API design
from:
database design
But concurrency makes that separation blurry.
Consider:
PATCH /documents/42
The API needs to answer:
What happens if another client changes the document between read and write?
That is a database question.
But the API must communicate the answer.
Therefore:
Database concurrency
|
v
Service semantics
|
v
API contract
The API becomes the public face of the database's concurrency model.
That's why backend engineers need to understand both sides.
25. Don't Hide Concurrency
A common design mistake is pretending concurrency doesn't exist.
You expose:
PUT /users/42
and simply overwrite the row.
It works beautifully in development.
Then production arrives.
Two browser tabs.
Three mobile clients.
A background worker.
An admin dashboard.
An integration.
Suddenly five independent systems can modify the same object.
Now you discover that:
PUT
was secretly:
concurrent distributed write
The concurrency was always there.
You simply weren't modeling it.
26. MVCC Gives APIs a Better Vocabulary
Instead of saying:
"The update failed."
we can say:
"The resource changed since you last observed it."
Instead of:
"Something went wrong."
we can say:
"Your transaction conflicted with another committed transaction."
Instead of:
"Try again."
we can say:
"Retry using the latest version."
This is much more meaningful.
The API communicates the actual state of the distributed interaction.
27. The Real Lesson
Building an API on top of MVCC isn't primarily about writing SQL.
It's about changing your mental model.
A resource isn't necessarily:
an object
It can be:
an object evolving through time.
A request isn't necessarily:
an instruction
It can be:
an attempt to transition one version of reality into another.
A conflict isn't necessarily:
an error
It can be:
evidence that another actor changed the world first.
And a transaction isn't merely:
BEGIN
COMMIT
It is:
a temporary view of reality
+
a set of intended changes
+
a validation that those changes can safely become reality.
That's a much deeper way to think about APIs.
Conclusion: Your API Is Already Concurrency Control
The moment an API has more than one client, concurrency exists.
You can ignore it.
You can hide it.
Or you can design for it.
MVCC gives us a powerful foundation.
It allows databases to maintain multiple versions of data, provide consistent transactional views, and coordinate concurrent access without forcing every reader to wait for every writer.
When we expose the right abstractions at the API layer — version tokens, ETags, conditional updates, snapshots, conflicts, idempotency keys, and transaction semantics — we can build APIs that acknowledge the reality of distributed state.
The key idea is simple:
Client reads version 10
|
v
Client works
|
v
Client attempts update
|
v
Is version still 10?
/ \
YES NO
| |
v v
Commit Conflict
|
v
Version 11
This is more than database engineering.
It's a philosophy of API design.
Don't pretend the world is static.
The world changes while your client is thinking.
Other clients are writing.
Workers are running.
Events are arriving.
Transactions are committing.
Caches are expiring.
Replicas are catching up.
Your API exists in the middle of all of that motion.
MVCC gives you a way to reason about that motion.
And once you start designing APIs around versions instead of pretending every request happens in isolation, something clicks:
Concurrency stops being an annoying edge case and becomes part of the API's language.
That's the real power of building an API on top of MVCC.
Top comments (0)