Edit Page

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

Sets localhost:8080 with admin:secret
Values are saved in your browser

Stay consistent!

Enforce Atomicity, Consistency, Isolation and Durability with multi-document transactions.

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.

Tech talk on Transactions in RESTHeart at MongoDB Live 2020
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

IN

Transaction is in progress

COMMITTED

Transaction has been successfully committed

ABORTED

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:

  1. 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
  1. 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
  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
  1. 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
  1. 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
  1. 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

  1. Keep transactions short and simple

    Limit the number of operations in a transaction to reduce the chance of conflicts and timeouts.

  2. Implement proper error handling and retry logic

    Be prepared to handle transaction errors and retry when appropriate.

  3. Avoid operations that require talking to all shards

    In sharded clusters, transactions that span multiple shards have higher latency and risk of failures.

  4. Create indexes before running transactions

    Unindexed queries in transactions can cause performance issues.

  5. 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)