Lesson 22 / 25
Transactions and Batched Writes
All-or-nothing changes.
When to use each
A batched write (writeBatch) groups set, update and delete operations that commit atomically: all succeed or none do. Use it when you do not need to read first. A transaction (runTransaction) reads documents and then writes based on what it read; if another client changes those documents before commit, the SDK retries the function, so it must not have side effects such as sending emails. All reads must come before writes. Client transactions fail when offline. Both have limits on operations per commit (check the docs). In Security Rules, getAfter() lets rules check the state a batch will produce, which helps keep related documents consistent.
Correct, affordable, repeatable
Production Firebase apps need consistent writes, a cost model, automated testing and deploys, and a clear view of trade-offs.
Transfer credits and a batched fan-out
TypeScript.
import { runTransaction, writeBatch, doc, serverTimestamp } from "firebase/firestore";
export async function transfer(fromId: string, toId: string, amount: number) {
await runTransaction(db, async (tx) => {
const fromRef = doc(db, "wallets", fromId);
const toRef = doc(db, "wallets", toId);
const [from, to] = [await tx.get(fromRef), await tx.get(toRef)]; // reads first
const balance = from.data()?.balance ?? 0;
if (balance < amount) throw new Error("Insufficient balance");
tx.update(fromRef, { balance: balance - amount });
tx.update(toRef, { balance: (to.data()?.balance ?? 0) + amount });
});
}
export async function renameAuthor(uid: string, name: string, postIds: string[]) {
const batch = writeBatch(db);
batch.update(doc(db, "profiles", uid), { displayName: name });
postIds.forEach((id) => batch.update(doc(db, "posts", id), { authorName: name }));
batch.set(doc(db, "audit", `${uid}-${Date.now()}`), { action: "rename", at: serverTimestamp() });
await batch.commit(); // atomic
}Keep money moves on the server
For balances and payments, run the transaction in a Cloud Function with the Admin SDK and lock the fields down in rules, so clients cannot write balances at all.
Quick check: Why must a transaction function avoid side effects like sending an email?
- Transactions run only on the emulator
- Transactions cannot call functions
- Emails are blocked by rules
- It may be retried several times if data changes before commit
Answer
It may be retried several times if data changes before commit — Retries would repeat the side effect.