image

24 Feb 2026

9K

35K

Flutter & Firebase Firestore: Using Transactions for Consistent Data

In modern application development, especially when dealing with real-time data and multiple users, ensuring data consistency is paramount. Firebase Firestore is a powerful NoSQL database that offers robust solutions for this, and one of its key features for maintaining data integrity is transactions. This article will explore how to leverage Firestore transactions with Flutter to achieve consistent data updates.

The Need for Transactions

Consider a scenario where multiple users might try to update the same piece of data concurrently. For example, updating a product's stock count, transferring funds between accounts, or incrementing a vote counter. Without proper mechanisms, these concurrent operations can lead to what's known as a "race condition," resulting in inconsistent or incorrect data.

Let's say a product has 10 items in stock. Two users simultaneously try to buy 1 item each:

  1. User A reads stock: 10
  2. User B reads stock: 10
  3. User A calculates new stock: 10 - 1 = 9
  4. User B calculates new stock: 10 - 1 = 9
  5. User A writes new stock: 9
  6. User B writes new stock: 9

The expected stock should be 8, but because both users read the same initial value and wrote independently, the final stock is 9. This is where transactions come in.

How Firestore Transactions Work

Firestore transactions provide a way to execute a set of read and write operations atomically. This means either all operations within the transaction succeed, or none of them do. Firestore achieves this through optimistic concurrency control:

  • Reads within a Transaction: When you read documents within a transaction, Firestore tracks the version of those documents.
  • Writes within a Transaction: When you attempt to commit the transaction, Firestore checks if any of the documents read during the transaction have been modified by another operation since they were read.
  • Automatic Retries: If any document has been modified, the entire transaction is automatically retried by Firestore. This process can repeat up to five times until it successfully commits or fails after all retries.

This mechanism guarantees that your operations are performed on the most up-to-date data, preventing race conditions and ensuring data consistency.

Implementing a Transaction in Flutter

Firestore provides the runTransaction method to execute a transaction. You pass it a callback function where you define your read and write operations. All operations within this callback are part of the atomic transaction.

Scenario: Updating a Product Stock

Let's revisit our product stock example and see how to correctly implement it using a Firestore transaction in Flutter.

Without a Transaction (Illustrating the Problem)

Here's how you might incorrectly update stock, leading to potential inconsistencies:


Future<void> updateStockWithoutTransaction(String productId, int quantityChange) async {
  final docRef = FirebaseFirestore.instance.collection('products').doc(productId);
  
  // Read the current stock
  final docSnapshot = await docRef.get();
  
  if (docSnapshot.exists) {
    final currentStock = docSnapshot.data()?['stock'] ?? 0;
    
    // Calculate new stock
    final newStock = currentStock + quantityChange;
    
    // Write the new stock
    await docRef.update({'stock': newStock});
    print('Stock updated without transaction: $newStock');
  } else {
    print('Product does not exist.');
  }
}

As explained, if two users run updateStockWithoutTransaction concurrently, they might both read the same currentStock before either has written the newStock, leading to an incorrect final value.

With a Transaction (The Solution)

Now, let's use a transaction to ensure atomic updates:


import 'package:cloud_firestore/cloud_firestore.dart';

Future<void> updateStockWithTransaction(String productId, int quantityChange) async {
  final docRef = FirebaseFirestore.instance.collection('products').doc(productId);

  try {
    await FirebaseFirestore.instance.runTransaction((transaction) async {
      // 1. Read the document within the transaction
      final docSnapshot = await transaction.get(docRef);

      if (!docSnapshot.exists) {
        throw Exception('Product does not exist!');
      }

      final currentStock = docSnapshot.data()?['stock'] ?? 0;
      final newStock = currentStock + quantityChange;

      // Optional: Add business logic validation
      if (newStock < 0) {
        throw Exception('Stock cannot be negative!');
      }

      // 2. Update the document within the transaction
      transaction.update(docRef, {'stock': newStock});
    });
    print('Stock updated successfully with transaction.');
  } on FirebaseException catch (e) {
    print('Firebase error during transaction: ${e.message}');
  } catch (e) {
    print('Failed to update stock with transaction: $e');
  }
}

In this transactional example:

  • The transaction.get(docRef) operation reads the product document. Firestore monitors this document for changes.
  • The transaction.update(docRef, {'stock': newStock}) operation prepares the update.
  • When the transaction attempts to commit, if the product document was modified by another operation between our read and the commit attempt, Firestore will automatically retry the entire runTransaction callback. This ensures that the currentStock we read is always the latest available at the time of a successful commit.
  • Any exceptions thrown within the runTransaction callback (like 'Product does not exist!' or 'Stock cannot be negative!') will cause the transaction to abort, and any changes will not be committed.

Key Considerations and Best Practices

  • Idempotency & Retries: Your transaction logic should be idempotent. Since transactions can be retried, ensure that running the code multiple times doesn't have unintended side effects.
  • Keep Transactions Short: Transactions hold locks on documents (optimistically), and retries consume resources. Keep the logic inside your transactions as simple and quick as possible. Avoid complex computations or external API calls within a transaction.
  • Read-Modify-Write: Always follow the pattern of reading documents, modifying them based on the read data, and then writing them back within the same transaction. Directly writing without prior reading might bypass the consistency checks you need.
  • Error Handling: Catch exceptions outside the runTransaction block to handle cases where the transaction fails after all retries (e.g., due to network issues, permission errors, or persistent conflicts).
  • Transaction Limitations: A single transaction can read up to 10 documents and write up to 500 documents. The total size of all documents written cannot exceed 10 MB.
  • Offline Behavior: Firestore transactions require an active network connection. They do not work offline, unlike regular document reads and writes which benefit from Firestore's offline persistence.

Conclusion

Firebase Firestore transactions are a crucial tool for any Flutter developer building robust, multi-user applications that demand high data consistency. By understanding and correctly implementing runTransaction, you can safeguard your application against race conditions and ensure that your data remains accurate and reliable, even under heavy concurrent load. Embrace transactions to build more resilient and trustworthy Flutter applications with Firestore.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is