Firestore Transactions

A transaction in Firestore is a set of read and write operations that execute as a single unit. Either all operations succeed together, or none of them take effect. Transactions prevent data inconsistency when multiple users try to update the same data at the same time.

Why Transactions Matter

Imagine two customers both try to buy the last concert ticket at the same moment. Without a transaction:

Customer A reads stock: 1 ticket available
Customer B reads stock: 1 ticket available
Customer A writes: stock = 0, ticket sold to A
Customer B writes: stock = 0, ticket sold to B
Result: 2 tickets sold, stock shows 0, one customer gets nothing

With a transaction, Firestore detects the conflict and retries. Only one customer gets the ticket.

How Firestore Transactions Work

A Firestore transaction works in a loop:

1. Start transaction
2. Read the document(s) you need
3. Compute new values based on what you read
4. Write the new values
5. Firestore checks if the documents changed between step 2 and step 4
   - If no change: commit the writes
   - If changed: roll back and retry from step 2

Basic Transaction Example

import { doc, runTransaction } from "firebase/firestore";
import { db } from "./firebase";

async function purchaseTicket(eventId, userId) {
  const eventRef = doc(db, "events", eventId);

  try {
    await runTransaction(db, async (transaction) => {
      const eventDoc = await transaction.get(eventRef);

      if (!eventDoc.exists()) {
        throw new Error("Event not found.");
      }

      const availableTickets = eventDoc.data().availableTickets;

      if (availableTickets <= 0) {
        throw new Error("No tickets left.");
      }

      // Deduct one ticket and record the purchase
      transaction.update(eventRef, {
        availableTickets: availableTickets - 1
      });

      const purchaseRef = doc(db, "purchases", userId + "_" + eventId);
      transaction.set(purchaseRef, {
        userId,
        eventId,
        purchasedAt: new Date()
      });
    });

    console.log("Ticket purchased successfully.");
  } catch (error) {
    console.error("Purchase failed:", error.message);
  }
}

Rules Inside Transactions

Transactions have specific requirements:

  • All reads must happen before any writes in the transaction function
  • The transaction function may run multiple times if conflicts occur — keep it free of side effects like sending emails
  • Transactions time out after 30 seconds
  • A single transaction can read and write up to 500 documents

Transferring Value Between Documents

Transactions work well for transferring values between documents — like moving currency from one wallet to another:

async function transferCoins(fromUserId, toUserId, amount) {
  const fromRef = doc(db, "wallets", fromUserId);
  const toRef = doc(db, "wallets", toUserId);

  await runTransaction(db, async (transaction) => {
    const fromDoc = await transaction.get(fromRef);
    const toDoc = await transaction.get(toRef);

    const fromBalance = fromDoc.data().coins;
    const toBalance = toDoc.data().coins;

    if (fromBalance < amount) {
      throw new Error("Insufficient coins.");
    }

    transaction.update(fromRef, { coins: fromBalance - amount });
    transaction.update(toRef, { coins: toBalance + amount });
  });

  console.log("Transfer complete.");
}

When to Use Transactions vs increment()

For simple counter increments where you don't need to read the current value before writing, use increment() instead of a transaction. It is atomic and simpler:

// Simple: just add 1 to likesCount
await updateDoc(doc(db, "posts", "post_001"), {
  likesCount: increment(1)
});

// Complex: read value, apply business logic, write result
// Use a transaction

Use transactions when the new value depends on reading the current value AND applying conditional logic based on that value.

Key Takeaway

Firestore transactions guarantee that a group of reads and writes execute atomically — all succeed or all fail. They prevent data races when multiple users update shared data simultaneously. Always do all reads before any writes inside a transaction. Use increment() for simple counter updates and reserve transactions for operations that require reading a value, checking it, and writing a new value that depends on it.

Leave a Comment

Your email address will not be published. Required fields are marked *