Skip to main content

Transactions

Perform multiple mutations on one or more documents in an atomic manner for better performance at scale.

Why Use Transactions

You need to update multiple documents as a single atomic operation, where either all changes succeed or none do. Without transactions, a failure mid-way through a series of updates could leave your data in an inconsistent state. For example, transferring a balance between two accounts requires both the debit and credit to complete together.

Overview

To perform a transaction in Squid, use the runInTransaction method provided by the squid object. This method takes a callback function as a parameter, which will be executed within the context of a transaction.

Client code
await squid.runInTransaction(async (transactionId: string) => {
const user1 = squid.collection<User>('users').doc('user_1_id');
const user2 = squid.collection<User>('users').doc('user_2_id');
await user1.update({ name: 'Alice' }, transactionId);
await user2.update({ name: 'Bob' }, transactionId);
});

When applying changes inside a transaction, these changes will not reflect immediately, but they will reflect optimistically once the callback function completes.

This is in contrast to applying a mutation without a transaction, where changes are applied immediately and optimistically. Any mutation that is applied as part of a transaction will resolve immediately. In order to make sure the changes are applied on the server, you should wait for the promise returned from runInTransaction to resolve.

Core Concepts

Transactions and queries

Squid does not support queries from inside a transaction. For example, the following code will cause a deadlock in the transaction:

Client code
const user1 = squid.collection<User>('users').doc('user_1_id');

await squid.runInTransaction(async (transactionId: string) => {
// This query inside the transaction will cause a deadlock
const user1Data = await user1.snapshot();

await user1.update({ loginCount: user1Data.loginCount + 1 });
});

Instead, fetch any necessary data before the transaction begins:

Client code
const user1 = squid.collection<User>('users').doc('user_1_id');
const user1Data = await user1.snapshot();

await squid.runInTransaction(async (transactionId: string) => {
if (user1Data) {
await user1.update({ loginCount: user1Data.loginCount + 1 }, transactionId);
}
});

Cross-connector transactions

When applying a transaction on multiple connectors, each connector will be updated atomically. However, it is possible that one connector updates successfully while the other fails. There is no support for cross-connector atomic updates.

Note

Cross-connector transactions provide per-connector atomicity, not global atomicity. Design your data model accordingly.

Error Handling

ErrorCauseSolution
DeadlockPerforming a query inside the transaction callbackFetch all required data before calling runInTransaction
Transaction timeoutThe transaction callback took too long to completeKeep transactions small and fast; move heavy computation outside the callback
Partial failure (cross-connector)One connector succeeded but another failedDesign for per-connector atomicity; consider using a single connector for critical atomic operations
Security rule rejectionA mutation within the transaction was blocked by security rulesVerify the user has write permission for all collections involved

Best Practices

  1. Fetch data before the transaction to avoid deadlocks:

    Client code
    // Fetch first
    const accountA = await squid.collection<Account>('accounts').doc('a').snapshot();
    const accountB = await squid.collection<Account>('accounts').doc('b').snapshot();

    // Then transact
    if (accountA && accountB) {
    await squid.runInTransaction(async (txId: string) => {
    await squid
    .collection<Account>('accounts')
    .doc('a')
    .update({ balance: accountA.balance - 100 }, txId);
    await squid
    .collection<Account>('accounts')
    .doc('b')
    .update({ balance: accountB.balance + 100 }, txId);
    });
    }
  2. Keep transactions small by only including the mutations that must be atomic. Move computation and validation outside the callback.

  3. Pass the transactionId to every mutation inside the callback. Forgetting to pass it causes the mutation to execute outside the transaction.

  4. Avoid cross-connector transactions when you need guaranteed all-or-nothing atomicity. Use a single connector for critical atomic operations.