MongoDB Transactions
Multi-document transactions allow you to execute multiple operations as a single atomic unit. This enforces the ACID properties (Atomicity, Consistency, Isolation, Durability) across multiple documents and collections.
🔧 Configuration
Why Use Transactions?
In MongoDB, operations on a single document are always atomic. For many applications, this is sufficient because data can be structured with embedded documents and arrays to keep related data in a single document.
However, there are scenarios where you need to update multiple documents atomically:
-
Financial operations affecting multiple accounts
-
Inventory and order management
-
User activity tracking across different collections
-
Data migrations that need to maintain referential integrity
Important
|
Multi-document transactions require MongoDB v4.0 or later configured as a Replica Set. |
Tip
|
The demo application shown during the talk is available on GitHub at https://github.com/softInstigate/restheart-txn-demo |
Sessions
Transactions in MongoDB are built on top of sessions. A session represents a sequence of related operations executed by an application.
Creating a Session
To start a session:
cURL
curl -X POST [INSTANCE-URL]/_sessions \
-H "Authorization: Basic [BASIC-AUTH]" \
-H "Content-Type: application/json" \
-d '{"causallyConsistent": true}'
HTTPie
http POST [INSTANCE-URL]/_sessions \
Authorization:"Basic [BASIC-AUTH]" \
causallyConsistent:=true
JavaScript
fetch('[INSTANCE-URL]/_sessions', {
method: 'POST',
headers: {
'Authorization': 'Basic [BASIC-AUTH]',
'Content-Type': 'application/json'
},
body: JSON.stringify({
causallyConsistent: true
})
})
.then(response => {
if (response.ok) {
console.log('Resource created successfully');
console.log('Location:', response.headers.get('Location'));
} else {
console.error('Failed to create resource:', response.status);
}
})
.catch(error => console.error('Error:', error));
Response:
HTTP/1.1 201 Created
Location: http://localhost:8080/_sessions/11c3ceb6-7b97-4f34-ba3f-689ea22ce6e0
The causallyConsistent
property is optional and defaults to true
. This ensures that operations within the session are causally ordered.
The session ID is returned in the Location
header. Extract the last part of the URL path to get the session ID:
sid=11c3ceb6-7b97-4f34-ba3f-689ea22ce6e0
Using a Session
To execute requests within a session, add the sid
query parameter:
cURL
curl -X POST "[INSTANCE-URL]/collection?sid=[SESSION-ID]" \
-H "Authorization: Basic [BASIC-AUTH]" \
-H "Content-Type: application/json" \
-d '{"name": "Session example", "value": 42}'
HTTPie
http POST "[INSTANCE-URL]/collection?sid=[SESSION-ID]" \
Authorization:"Basic [BASIC-AUTH]" \
name="Session example" \
value:=42
JavaScript
fetch('[INSTANCE-URL]/collection?sid=[SESSION-ID]', {
method: 'POST',
headers: {
'Authorization': 'Basic [BASIC-AUTH]',
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: "Session example",
value: 42
})
})
.then(response => {
if (response.ok) {
console.log('Resource created successfully');
console.log('Location:', response.headers.get('Location'));
} else {
console.error('Failed to create resource:', response.status);
}
})
.catch(error => console.error('Error:', error));
cURL
curl -X GET "[INSTANCE-URL]/collection?sid=[SESSION-ID]" \
-H "Authorization: Basic [BASIC-AUTH]"
HTTPie
http GET "[INSTANCE-URL]/collection?sid=[SESSION-ID]" \
Authorization:"Basic [BASIC-AUTH]"
JavaScript
fetch('[INSTANCE-URL]/collection?sid=[SESSION-ID]', {
method: 'GET',
headers: {
'Authorization': 'Basic [BASIC-AUTH]'
}
})
.then(response => response.json())
.then(data => {
console.log('Retrieved data:', data);
})
.catch(error => console.error('Error:', error));
Transaction Lifecycle
Transactions provide an all-or-nothing execution model. Either all operations in the transaction succeed, or none of them take effect.
Transaction Status
A transaction can be in one of the following states:
Status | Description |
---|---|
|
Transaction is in progress |
|
Transaction has been successfully committed |
|
Transaction has been aborted (explicitly or due to error/timeout) |
Starting a Transaction
To start a transaction within a session:
cURL
curl -X POST "[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns" \
-H "Authorization: Basic [BASIC-AUTH]"
HTTPie
http POST "[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns" \
Authorization:"Basic [BASIC-AUTH]"
JavaScript
fetch('[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns', {
method: 'POST',
headers: {
'Authorization': 'Basic [BASIC-AUTH]'
}
})
.then(response => {
if (response.ok) {
console.log('Resource created successfully');
console.log('Location:', response.headers.get('Location'));
} else {
console.error('Failed to create resource:', response.status);
}
})
.catch(error => console.error('Error:', error));
Response:
HTTP/1.1 201 Created
Location: http://localhost:8080/_sessions/11c3ceb6-7b97-4f34-ba3f-689ea22ce6e0/_txns/1
The transaction ID is the last part of the Location header (in this case, 1
).
Checking Transaction Status
To check the current status of a transaction:
cURL
curl -X GET "[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns" \
-H "Authorization: Basic [BASIC-AUTH]"
HTTPie
http GET "[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns" \
Authorization:"Basic [BASIC-AUTH]"
JavaScript
fetch('[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns', {
method: 'GET',
headers: {
'Authorization': 'Basic [BASIC-AUTH]'
}
})
.then(response => response.json())
.then(data => {
console.log('Retrieved data:', data);
})
.catch(error => console.error('Error:', error));
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"currentTxn": {
"id": 1,
"status": "IN"
}
}
Executing Operations in a Transaction
To include operations in a transaction, use both the sid
and txn
query parameters:
cURL
curl -X POST "[INSTANCE-URL]/accounts?sid=[SESSION-ID]&txn=[TRANSACTION-ID]" \
-H "Authorization: Basic [BASIC-AUTH]" \
-H "Content-Type: application/json" \
-d '{"owner": "Alice", "balance": 1000}'
HTTPie
http POST "[INSTANCE-URL]/accounts?sid=[SESSION-ID]&txn=[TRANSACTION-ID]" \
Authorization:"Basic [BASIC-AUTH]" \
owner="Alice" \
balance:=1000
JavaScript
fetch('[INSTANCE-URL]/accounts?sid=[SESSION-ID]&txn=[TRANSACTION-ID]', {
method: 'POST',
headers: {
'Authorization': 'Basic [BASIC-AUTH]',
'Content-Type': 'application/json'
},
body: JSON.stringify({
owner: "Alice",
balance: 1000
})
})
.then(response => {
if (response.ok) {
console.log('Resource created successfully');
console.log('Location:', response.headers.get('Location'));
} else {
console.error('Failed to create resource:', response.status);
}
})
.catch(error => console.error('Error:', error));
cURL
curl -X PATCH "[INSTANCE-URL]/accounts/bob?sid=[SESSION-ID]&txn=[TRANSACTION-ID]" \
-H "Authorization: Basic [BASIC-AUTH]" \
-H "Content-Type: application/json" \
-d '{"$inc": {"balance": -100}}'
HTTPie
http PATCH "[INSTANCE-URL]/accounts/bob?sid=[SESSION-ID]&txn=[TRANSACTION-ID]" \
Authorization:"Basic [BASIC-AUTH]" \
'$inc[balance]':=-100
JavaScript
fetch('[INSTANCE-URL]/accounts/bob?sid=[SESSION-ID]&txn=[TRANSACTION-ID]', {
method: 'PATCH',
headers: {
'Authorization': 'Basic [BASIC-AUTH]',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"$inc": { "balance": -100 }
})
})
.then(response => {
if (response.ok) {
console.log('Write request executed successfully');
} else {
console.error('Write request failed:', response.status);
}
})
.catch(error => console.error('Error:', error));
cURL
curl -X PATCH "[INSTANCE-URL]/accounts/alice?sid=[SESSION-ID]&txn=[TRANSACTION-ID]" \
-H "Authorization: Basic [BASIC-AUTH]" \
-H "Content-Type: application/json" \
-d '{"$inc": {"balance": 100}}'
HTTPie
http PATCH "[INSTANCE-URL]/accounts/alice?sid=[SESSION-ID]&txn=[TRANSACTION-ID]" \
Authorization:"Basic [BASIC-AUTH]" \
'$inc[balance]':=100
JavaScript
fetch('[INSTANCE-URL]/accounts/alice?sid=[SESSION-ID]&txn=[TRANSACTION-ID]', {
method: 'PATCH',
headers: {
'Authorization': 'Basic [BASIC-AUTH]',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"$inc": { "balance": 100 }
})
})
.then(response => {
if (response.ok) {
console.log('Write request executed successfully');
} else {
console.error('Write request failed:', response.status);
}
})
.catch(error => console.error('Error:', error));
Committing a Transaction
When all operations have been executed successfully, commit the transaction:
cURL
curl -X PATCH "[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns/[TRANSACTION-ID]" \
-H "Authorization: Basic [BASIC-AUTH]"
HTTPie
http PATCH "[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns/[TRANSACTION-ID]" \
Authorization:"Basic [BASIC-AUTH]"
JavaScript
fetch('[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns/[TRANSACTION-ID]', {
method: 'PATCH',
headers: {
'Authorization': 'Basic [BASIC-AUTH]'
}
})
.then(response => {
if (response.ok) {
console.log('Write request executed successfully');
} else {
console.error('Write request failed:', response.status);
}
})
.catch(error => console.error('Error:', error));
Response:
HTTP/1.1 200 OK
Aborting a Transaction
If you need to cancel a transaction:
cURL
curl -X DELETE "[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns/[TRANSACTION-ID]" \
-H "Authorization: Basic [BASIC-AUTH]"
HTTPie
http DELETE "[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns/[TRANSACTION-ID]" \
Authorization:"Basic [BASIC-AUTH]"
JavaScript
fetch('[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns/[TRANSACTION-ID]', {
method: 'DELETE',
headers: {
'Authorization': 'Basic [BASIC-AUTH]'
}
})
.then(response => {
if (response.ok) {
console.log('Write request executed successfully');
} else {
console.error('Write request failed:', response.status);
}
})
.catch(error => console.error('Error:', error));
Response:
HTTP/1.1 204 No Content
Error Handling
Warning
|
The client application is responsible for handling transaction errors and implementing appropriate retry logic. |
Common error scenarios:
Error | Status Code | Description |
---|---|---|
Transaction not in progress |
406 |
An operation was attempted in a transaction that’s not in the "IN" state |
Write conflict |
409 |
Another transaction committed changes to the same documents |
Transaction expired |
500 |
Transaction exceeded the maximum runtime (default: 60 seconds) |
Transaction Timeouts
By default, transactions must complete within 60 seconds. If this time limit is exceeded, MongoDB automatically aborts the transaction.
For more information on transaction limits, see the MongoDB documentation.
Complete Example
The following example demonstrates a transfer between two bank accounts:
-
Create a session
==== cURL
curl -X POST "[INSTANCE-URL]/_sessions" \ -H "Authorization: Basic [BASIC-AUTH]"
HTTPie
http POST "[INSTANCE-URL]/_sessions" \
Authorization:"Basic [BASIC-AUTH]"
JavaScript
fetch('[INSTANCE-URL]/_sessions', {
method: 'POST',
headers: {
'Authorization': 'Basic [BASIC-AUTH]'
}
})
.then(response => {
if (response.ok) {
console.log('Resource created successfully');
console.log('Location:', response.headers.get('Location'));
} else {
console.error('Failed to create resource:', response.status);
}
})
.catch(error => console.error('Error:', error));
+
HTTP/1.1 201 Created
Location: http://localhost:8080/_sessions/session123
-
Start a transaction
==== cURL
curl -X POST "[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns" \ -H "Authorization: Basic [BASIC-AUTH]"
HTTPie
http POST "[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns" \
Authorization:"Basic [BASIC-AUTH]"
JavaScript
fetch('[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns', {
method: 'POST',
headers: {
'Authorization': 'Basic [BASIC-AUTH]'
}
})
.then(response => {
if (response.ok) {
console.log('Resource created successfully');
console.log('Location:', response.headers.get('Location'));
} else {
console.error('Failed to create resource:', response.status);
}
})
.catch(error => console.error('Error:', error));
+
HTTP/1.1 201 Created
Location: http://localhost:8080/_sessions/session123/_txns/1
-
Debit from account A
==== cURL
curl -X PATCH "[INSTANCE-URL]/accounts/accountA?sid=[SESSION-ID]&txn=[TRANSACTION-ID]" \ -H "Authorization: Basic [BASIC-AUTH]" \ -H "Content-Type: application/json" \ -d '{"$inc": {"balance": -100}}'
HTTPie
http PATCH "[INSTANCE-URL]/accounts/accountA?sid=[SESSION-ID]&txn=[TRANSACTION-ID]" \
Authorization:"Basic [BASIC-AUTH]" \
'$inc[balance]':=-100
JavaScript
fetch('[INSTANCE-URL]/accounts/accountA?sid=[SESSION-ID]&txn=[TRANSACTION-ID]', {
method: 'PATCH',
headers: {
'Authorization': 'Basic [BASIC-AUTH]',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"$inc": { "balance": -100 }
})
})
.then(response => {
if (response.ok) {
console.log('Write request executed successfully');
} else {
console.error('Write request failed:', response.status);
}
})
.catch(error => console.error('Error:', error));
+
HTTP/1.1 200 OK
-
Credit to account B
==== cURL
curl -X PATCH "[INSTANCE-URL]/accounts/accountB?sid=[SESSION-ID]&txn=[TRANSACTION-ID]" \ -H "Authorization: Basic [BASIC-AUTH]" \ -H "Content-Type: application/json" \ -d '{"$inc": {"balance": 100}}'
HTTPie
http PATCH "[INSTANCE-URL]/accounts/accountB?sid=[SESSION-ID]&txn=[TRANSACTION-ID]" \
Authorization:"Basic [BASIC-AUTH]" \
'$inc[balance]':=100
JavaScript
fetch('[INSTANCE-URL]/accounts/accountB?sid=[SESSION-ID]&txn=[TRANSACTION-ID]', {
method: 'PATCH',
headers: {
'Authorization': 'Basic [BASIC-AUTH]',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"$inc": { "balance": 100 }
})
})
.then(response => {
if (response.ok) {
console.log('Write request executed successfully');
} else {
console.error('Write request failed:', response.status);
}
})
.catch(error => console.error('Error:', error));
+
HTTP/1.1 200 OK
-
Add transaction record
==== cURL
curl -X POST "[INSTANCE-URL]/transactions?sid=[SESSION-ID]&txn=[TRANSACTION-ID]" \ -H "Authorization: Basic [BASIC-AUTH]" \ -H "Content-Type: application/json" \ -d '{"from": "accountA", "to": "accountB", "amount": 100, "timestamp": {"$date": 1623408052123}}'
HTTPie
http POST "[INSTANCE-URL]/transactions?sid=[SESSION-ID]&txn=[TRANSACTION-ID]" \
Authorization:"Basic [BASIC-AUTH]" \
from="accountA" \
to="accountB" \
amount:=100 \
'timestamp[$date]':=1623408052123
JavaScript
fetch('[INSTANCE-URL]/transactions?sid=[SESSION-ID]&txn=[TRANSACTION-ID]', {
method: 'POST',
headers: {
'Authorization': 'Basic [BASIC-AUTH]',
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: "accountA",
to: "accountB",
amount: 100,
timestamp: { "$date": 1623408052123 }
})
})
.then(response => {
if (response.ok) {
console.log('Resource created successfully');
console.log('Location:', response.headers.get('Location'));
} else {
console.error('Failed to create resource:', response.status);
}
})
.catch(error => console.error('Error:', error));
+
HTTP/1.1 201 Created
-
Commit the transaction
==== cURL
curl -X PATCH "[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns/[TRANSACTION-ID]" \ -H "Authorization: Basic [BASIC-AUTH]"
HTTPie
http PATCH "[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns/[TRANSACTION-ID]" \
Authorization:"Basic [BASIC-AUTH]"
JavaScript
fetch('[INSTANCE-URL]/_sessions/[SESSION-ID]/_txns/[TRANSACTION-ID]', {
method: 'PATCH',
headers: {
'Authorization': 'Basic [BASIC-AUTH]'
}
})
.then(response => {
if (response.ok) {
console.log('Write request executed successfully');
} else {
console.error('Write request failed:', response.status);
}
})
.catch(error => console.error('Error:', error));
+
HTTP/1.1 200 OK
Best Practices
-
Keep transactions short and simple
Limit the number of operations in a transaction to reduce the chance of conflicts and timeouts.
-
Implement proper error handling and retry logic
Be prepared to handle transaction errors and retry when appropriate.
-
Avoid operations that require talking to all shards
In sharded clusters, transactions that span multiple shards have higher latency and risk of failures.
-
Create indexes before running transactions
Unindexed queries in transactions can cause performance issues.
-
Consider increasing the default transaction timeout
For complex operations, you may need to configure MongoDB to allow longer transactions.
Limitations
-
Multi-document transactions have some performance overhead
-
Transactions in sharded clusters have additional constraints
-
Some operations are not allowed in transactions (e.g., creating collections or indexes)
-
Default 60-second runtime limit (can be configured)