FireProx Transactions Guide¶
This notebook demonstrates Firestore transactions in FireProx, enabling atomic read-modify-write operations for data consistency.
What are Transactions?¶
Transactions ensure ACID properties (Atomicity, Consistency, Isolation, Durability) for operations that read and write data. They prevent race conditions when multiple clients modify the same data concurrently.
Transaction Pattern¶
FireProx uses the native Firestore decorator pattern:
transaction = db.transaction()
@firestore.transactional
def my_transaction(transaction):
# Read documents
doc.fetch(transaction=transaction)
# Modify locally
doc.field += 1
# Write back
doc.save(transaction=transaction)
my_transaction(transaction)
Key Rules¶
- All reads must happen before writes within a transaction
- Automatic retry if concurrent modification detected
- Cannot create new documents (DETACHED → LOADED) in transactions
- Create transactions from any object: db, collection, or document
The demo is split into two sections:
- Synchronous API examples
- Asynchronous API examples
Setup¶
Import modules and prepare for demonstrations.
from google.cloud import firestore
from fire_prox import AsyncFireProx, FireProx
from fire_prox.testing import async_demo_client, demo_client
Initialize Client and Create Sample Data¶
We'll create a bank account system to demonstrate atomic transfers.
# Create sync client and collection
client = demo_client()
db = FireProx(client)
accounts = db.collection('transaction_demo_accounts')
# Create two bank accounts
alice = accounts.new()
alice.name = 'Alice'
alice.balance = 1000
alice.save(doc_id='alice')
bob = accounts.new()
bob.name = 'Bob'
bob.balance = 500
bob.save(doc_id='bob')
print("💰 Initial balances:")
print(f" Alice: ${alice.balance}")
print(f" Bob: ${bob.balance}")
💰 Initial balances: Alice: $1000 Bob: $500
Feature 1: Basic Transaction - Atomic Transfer¶
Transfer money between accounts atomically. Either both accounts are updated, or neither is.
# Create transaction object
transaction = db.transaction()
@firestore.transactional
def transfer_money(transaction, from_id, to_id, amount):
"""
Transfer money between accounts atomically.
This function reads both accounts, modifies them,
and writes them back within a single transaction.
"""
# Read both accounts (all reads must happen first)
from_account = accounts.doc(from_id)
to_account = accounts.doc(to_id)
from_account.fetch(transaction=transaction)
to_account.fetch(transaction=transaction)
# Check sufficient funds
if from_account.balance < amount:
raise ValueError(f"Insufficient funds: {from_account.balance} < {amount}")
# Modify locally
from_account.balance -= amount
to_account.balance += amount
# Write both updates (all writes must happen after reads)
from_account.save(transaction=transaction)
to_account.save(transaction=transaction)
return from_account.balance, to_account.balance
# Execute the transaction
alice_new, bob_new = transfer_money(transaction, 'alice', 'bob', 200)
print("💸 Transferred $200 from Alice to Bob")
print(f" Alice: ${alice_new}")
print(f" Bob: ${bob_new}")
print("\n✅ Transaction completed atomically!")
💸 Transferred $200 from Alice to Bob Alice: $800 Bob: $700 ✅ Transaction completed atomically!
Feature 2: Transaction with Validation¶
Transactions can include business logic and validation. If an error occurs, no changes are made.
transaction = db.transaction()
@firestore.transactional
def safe_transfer(transaction, from_id, to_id, amount):
"""
Transfer with validation and business rules.
"""
from_account = accounts.doc(from_id)
to_account = accounts.doc(to_id)
from_account.fetch(transaction=transaction)
to_account.fetch(transaction=transaction)
# Validation: minimum balance
MIN_BALANCE = 100
if from_account.balance - amount < MIN_BALANCE:
raise ValueError(
f"Transfer would leave balance below minimum (${MIN_BALANCE}). "
f"Current: ${from_account.balance}, Transfer: ${amount}"
)
# Validation: transfer limit
MAX_TRANSFER = 500
if amount > MAX_TRANSFER:
raise ValueError(f"Transfer amount ${amount} exceeds limit of ${MAX_TRANSFER}")
# Perform transfer
from_account.balance -= amount
to_account.balance += amount
from_account.save(transaction=transaction)
to_account.save(transaction=transaction)
# Try a valid transfer
try:
safe_transfer(transaction, 'alice', 'bob', 150)
print("✅ Valid transfer succeeded")
except ValueError as e:
print(f"❌ Transfer rejected: {e}")
# Try an invalid transfer (would leave balance too low)
transaction2 = db.transaction()
try:
safe_transfer(transaction2, 'alice', 'bob', 750) # Would leave Alice with $50
print("❌ This shouldn't happen")
except ValueError as e:
print(f"\n✅ Invalid transfer correctly rejected: {e}")
print(" No changes were made to either account!")
✅ Valid transfer succeeded ✅ Invalid transfer correctly rejected: Transfer would leave balance below minimum ($100). Current: $650, Transfer: $750 No changes were made to either account!
Feature 3: Creating Transactions from Different Objects¶
You can create transactions from db, collection, or document objects - whatever is convenient!
# Method 1: From db
transaction1 = db.transaction()
print("✅ Created transaction from db")
# Method 2: From collection
transaction2 = accounts.transaction()
print("✅ Created transaction from collection")
# Method 3: From document
alice_doc = accounts.doc('alice')
transaction3 = alice_doc.transaction()
print("✅ Created transaction from document")
print("\n💡 All methods create identical transaction objects")
print(" Choose whichever is most convenient for your code!")
✅ Created transaction from db ✅ Created transaction from collection ✅ Created transaction from document 💡 All methods create identical transaction objects Choose whichever is most convenient for your code!
Feature 4: Transactions with Atomic Operations¶
Combine transactions with atomic operations like ArrayUnion, ArrayRemove, and Increment.
# Create a user with transaction history
users = db.collection('transaction_demo_users')
user = users.new()
user.name = 'Charlie'
user.credits = 100
user.tags = ['customer']
user.login_count = 5
user.save(doc_id='charlie')
print("Initial state:")
print(f" Credits: {user.credits}")
print(f" Tags: {user.tags}")
print(f" Login count: {user.login_count}")
# Transaction with atomic operations
transaction = db.transaction()
@firestore.transactional
def upgrade_user(transaction):
user = users.doc('charlie')
user.fetch(transaction=transaction)
# Use atomic operations
user.increment('credits', 50) # Add bonus credits
user.array_union('tags', ['premium', 'verified']) # Add tags
user.increment('login_count', 1) # Track login
user.save(transaction=transaction)
upgrade_user(transaction)
# Verify results
user_after = users.doc('charlie')
user_after.fetch()
print("\nAfter upgrade:")
print(f" Credits: {user_after.credits}")
print(f" Tags: {user_after.tags}")
print(f" Login count: {user_after.login_count}")
print("\n✅ Transaction with atomic operations succeeded!")
Initial state: Credits: 100 Tags: ['customer'] Login count: 5 After upgrade: Credits: 150 Tags: ['customer', 'premium', 'verified'] Login count: 6 ✅ Transaction with atomic operations succeeded!
Feature 5: Multiple Document Updates¶
Transactions can update many documents atomically - all or nothing.
# Create a team of users
team = db.collection('transaction_demo_team')
members = ['alice', 'bob', 'charlie']
for member_id in members:
member = team.new()
member.name = member_id.capitalize()
member.points = 0
member.save(doc_id=member_id)
print("Initial team points:")
for member_id in members:
m = team.doc(member_id)
m.fetch()
print(f" {m.name}: {m.points} points")
# Award points to entire team atomically
transaction = db.transaction()
@firestore.transactional
def award_team_bonus(transaction, bonus_points):
"""
Award bonus points to all team members atomically.
"""
# Read all members
member_docs = [team.doc(member_id) for member_id in members]
for doc in member_docs:
doc.fetch(transaction=transaction)
# Award points to everyone
for doc in member_docs:
doc.increment('points', bonus_points)
doc.save(transaction=transaction)
award_team_bonus(transaction, 100)
print("\nAfter team bonus:")
for member_id in members:
m = team.doc(member_id)
m.fetch(force=True)
print(f" {m.name}: {m.points} points")
print("\n✅ All team members updated atomically!")
Initial team points: Alice: 0 points Bob: 0 points Charlie: 0 points After team bonus: Alice: 100 points Bob: 100 points Charlie: 100 points ✅ All team members updated atomically!
Feature 6: Error Handling - Cannot Create New Documents¶
Transactions can only update existing documents. Creating new documents (DETACHED → LOADED) is not allowed.
transaction = db.transaction()
@firestore.transactional
def try_create_document(transaction):
new_doc = accounts.new()
new_doc.name = 'David'
new_doc.balance = 1000
new_doc.save(doc_id='david', transaction=transaction) # This will fail!
try:
try_create_document(transaction)
print("❌ This shouldn't succeed")
except ValueError as e:
print(f"✅ Correctly rejected: {e}")
print("\n💡 Create documents outside transactions, then update them inside transactions")
# The correct way:
david = accounts.new()
david.name = 'David'
david.balance = 1000
david.save(doc_id='david') # Create OUTSIDE transaction
print("\n✅ Document created successfully outside transaction")
# Now we can update it in a transaction
transaction2 = db.transaction()
@firestore.transactional
def update_david(transaction):
david = accounts.doc('david')
david.fetch(transaction=transaction)
david.balance += 100
david.save(transaction=transaction)
update_david(transaction2)
print("✅ Document updated successfully in transaction")
✅ Correctly rejected: Cannot create new documents (DETACHED -> LOADED) within a transaction. Create the document first, then use transactions for updates. 💡 Create documents outside transactions, then update them inside transactions ✅ Document created successfully outside transaction ✅ Document updated successfully in transaction
Feature 7: Real-World Pattern - Inventory Management¶
A practical example: managing product inventory with transactions to prevent overselling.
# Create product inventory
inventory = db.collection('transaction_demo_inventory')
laptop = inventory.new()
laptop.name = 'Laptop Pro'
laptop.price = 1200
laptop.stock = 5
laptop.reserved = 0
laptop.save(doc_id='laptop_pro')
print(f"Product: {laptop.name}")
print(f" Price: ${laptop.price}")
print(f" Stock: {laptop.stock}")
print(f" Reserved: {laptop.reserved}")
# Function to reserve product
def reserve_product(product_id, quantity, customer_id):
"""
Reserve product inventory for a customer.
Prevents overselling by checking availability in a transaction.
"""
transaction = inventory.transaction()
@firestore.transactional
def execute_reservation(transaction):
product = inventory.doc(product_id)
product.fetch(transaction=transaction)
available = product.stock - product.reserved
if available < quantity:
raise ValueError(
f"Insufficient stock: {available} available, {quantity} requested"
)
# Reserve the items
product.reserved += quantity
product.save(transaction=transaction)
return available - quantity # Remaining after reservation
return execute_reservation(transaction)
# Successful reservations
try:
remaining = reserve_product('laptop_pro', 2, 'customer1')
print("\n✅ Reserved 2 laptops for customer1")
print(f" {remaining} available after reservation")
remaining = reserve_product('laptop_pro', 2, 'customer2')
print("\n✅ Reserved 2 laptops for customer2")
print(f" {remaining} available after reservation")
except ValueError as e:
print(f"❌ Reservation failed: {e}")
# Try to reserve more than available
try:
reserve_product('laptop_pro', 2, 'customer3') # Only 1 left!
print("❌ This shouldn't succeed")
except ValueError as e:
print(f"\n✅ Overselling prevented: {e}")
# Check final state
laptop_final = inventory.doc('laptop_pro')
laptop_final.fetch()
print("\nFinal inventory:")
print(f" Stock: {laptop_final.stock}")
print(f" Reserved: {laptop_final.reserved}")
print(f" Available: {laptop_final.stock - laptop_final.reserved}")
print("\n💡 Transactions prevent race conditions and overselling!")
Product: Laptop Pro Price: $1200 Stock: 5 Reserved: 0 ✅ Reserved 2 laptops for customer1 3 available after reservation ✅ Reserved 2 laptops for customer2 1 available after reservation ✅ Overselling prevented: Insufficient stock: 1 available, 2 requested Final inventory: Stock: 5 Reserved: 4 Available: 1 💡 Transactions prevent race conditions and overselling!
Part 2: Asynchronous Transactions¶
Examples using the asynchronous AsyncFireProx API with async/await.
Initialize Async Client and Create Sample Data¶
# Create async client and collection
async_client = async_demo_client()
async_db = AsyncFireProx(async_client)
async_accounts = async_db.collection('transaction_demo_accounts_async')
# Create two bank accounts
alice = async_accounts.new()
alice.name = 'Alice'
alice.balance = 1000
await alice.save(doc_id='alice')
bob = async_accounts.new()
bob.name = 'Bob'
bob.balance = 500
await bob.save(doc_id='bob')
print("💰 Initial async balances:")
print(f" Alice: ${alice.balance}")
print(f" Bob: ${bob.balance}")
💰 Initial async balances: Alice: $1000 Bob: $500
Feature 1: Basic Async Transaction¶
Async transactions use @firestore.async_transactional and await for all operations.
transaction = async_db.transaction()
@firestore.async_transactional
async def async_transfer_money(transaction, from_id, to_id, amount):
"""
Async transfer between accounts.
"""
from_account = async_accounts.doc(from_id)
to_account = async_accounts.doc(to_id)
# All reads with await
await from_account.fetch(transaction=transaction)
await to_account.fetch(transaction=transaction)
# Check funds
if from_account.balance < amount:
raise ValueError("Insufficient funds")
# Modify
from_account.balance -= amount
to_account.balance += amount
# All writes with await
await from_account.save(transaction=transaction)
await to_account.save(transaction=transaction)
return from_account.balance, to_account.balance
# Execute with await
alice_new, bob_new = await async_transfer_money(transaction, 'alice', 'bob', 200)
print("💸 Async transferred $200 from Alice to Bob")
print(f" Alice: ${alice_new}")
print(f" Bob: ${bob_new}")
print("\n✅ Async transaction completed!")
💸 Async transferred $200 from Alice to Bob Alice: $800 Bob: $700 ✅ Async transaction completed!
Feature 2: Async Transaction with Atomic Operations¶
# Create user
async_users = async_db.collection('transaction_demo_users_async')
user = async_users.new()
user.name = 'Charlie'
user.credits = 100
user.tags = ['customer']
user.visits = 10
await user.save(doc_id='charlie')
print(f"Initial: credits={user.credits}, tags={user.tags}, visits={user.visits}")
# Async transaction with atomic ops
transaction = async_db.transaction()
@firestore.async_transactional
async def async_upgrade_user(transaction):
user = async_users.doc('charlie')
await user.fetch(transaction=transaction)
user.increment('credits', 50)
user.array_union('tags', ['premium', 'verified'])
user.increment('visits', 1)
await user.save(transaction=transaction)
await async_upgrade_user(transaction)
# Verify
user_after = async_users.doc('charlie')
await user_after.fetch()
print(f"\nAfter: credits={user_after.credits}, tags={user_after.tags}, visits={user_after.visits}")
print("✅ Async transaction with atomic operations succeeded!")
Initial: credits=100, tags=['customer'], visits=10 After: credits=150, tags=['customer', 'premium', 'verified'], visits=11 ✅ Async transaction with atomic operations succeeded!
Feature 3: Async Multi-Document Transaction¶
# Create team
async_team = async_db.collection('transaction_demo_team_async')
members = ['alice', 'bob', 'charlie']
for member_id in members:
member = async_team.new()
member.name = member_id.capitalize()
member.points = 0
await member.save(doc_id=member_id)
print("Initial async team:")
for member_id in members:
m = async_team.doc(member_id)
await m.fetch()
print(f" {m.name}: {m.points} points")
# Award bonus to entire team
transaction = async_db.transaction()
@firestore.async_transactional
async def async_award_team_bonus(transaction, bonus):
member_docs = [async_team.doc(mid) for mid in members]
# Read all
for doc in member_docs:
await doc.fetch(transaction=transaction)
# Update all
for doc in member_docs:
doc.increment('points', bonus)
await doc.save(transaction=transaction)
await async_award_team_bonus(transaction, 100)
print("\nAfter async team bonus:")
for member_id in members:
m = async_team.doc(member_id)
await m.fetch(force=True)
print(f" {m.name}: {m.points} points")
print("\n✅ Async multi-document transaction succeeded!")
Initial async team: Alice: 0 points Bob: 0 points Charlie: 0 points After async team bonus: Alice: 100 points Bob: 100 points Charlie: 100 points ✅ Async multi-document transaction succeeded!
Summary¶
This demo showcased all transaction features:
✅ Transaction Pattern¶
Synchronous:
transaction = db.transaction()
@firestore.transactional
def my_transaction(transaction):
doc.fetch(transaction=transaction)
doc.field += 1
doc.save(transaction=transaction)
my_transaction(transaction)
Asynchronous:
transaction = db.transaction()
@firestore.async_transactional
async def my_transaction(transaction):
await doc.fetch(transaction=transaction)
doc.field += 1
await doc.save(transaction=transaction)
await my_transaction(transaction)
✅ Key Features¶
- Atomic Operations - All or nothing execution
- Automatic Retry - Handles concurrent modifications
- Multiple Documents - Update many documents atomically
- Business Logic - Include validation and checks
- Atomic Operations Support - ArrayUnion, ArrayRemove, Increment
- Convenient Creation - From db, collection, or document
⚠️ Important Rules¶
- All reads must happen before writes - Firestore requirement
- Cannot create new documents - Only update existing ones
- Use
transaction=parameter - For both fetch() and save() - Error handling - Exceptions rollback the entire transaction
💡 Best Practices¶
- Use transactions for financial operations (transfers, payments)
- Use transactions for inventory management (prevent overselling)
- Use transactions for counters (ensure accuracy)
- Use transactions for multi-step updates (maintain consistency)
- Keep transactions short (minimize contention)
- Create documents outside transactions, update them inside
🚀 Real-World Use Cases¶
- Banking - Money transfers, balance updates
- E-commerce - Inventory reservations, order processing
- Gaming - Score updates, resource management
- Social - Like counts, follower updates
- Bookings - Seat reservations, availability checks
📚 Learn More¶
- Tests: See
tests/test_integration_transactions.pyfor examples - Async Tests: See
tests/test_integration_transactions_async.py - Native API: Firestore Transactions Documentation