Skip to main content
Transactions in BinaryDB provide a way to group multiple operations together and ensure they either all succeed or all fail atomically. This is essential for maintaining data consistency.

Overview

BinaryDB implements a simple but correct transaction system using snapshot-based isolation:
  • begin() - Start a transaction and create a snapshot of current data
  • end() - Commit the transaction and persist changes to disk
  • rollback() - Discard all changes and restore the snapshot

How Transactions Work

Internal State

The database maintains transaction state using two internal attributes:
database.py:53-55
self._in_transaction: bool = False
self._tx_snapshot: dict[str, Any] | None = None
  • _in_transaction - Tracks whether a transaction is currently active
  • _tx_snapshot - Stores a complete copy of the data at transaction start

The Dirty Flag During Transactions

When inside a transaction, the dirty flag behaves differently:
database.py:72-74
def _mark_dirty(self) -> None:
    if not self._in_transaction:
        self._dirty = True
Changes made during a transaction do not set the dirty flag. This prevents auto-commit and ensures changes only persist when you call end().

Starting a Transaction

begin()

Begin a new transaction by creating a snapshot of the current database state.
database.py:201-213
def begin(self) -> None:
    """
    Begin a transaction.

    All changes are kept in memory until commit or rollback.
    """
    self._ensure_open()

    if self._in_transaction:
        raise TransactionError("Transaction already active")

    self._tx_snapshot = self._data.copy()
    self._in_transaction = True
Basic Usage:
from binarydb.database import Database

db = Database("mydata.pkl")
db.load()

db.begin()  # Start transaction
# ... perform operations ...
db.end()    # Commit transaction
You cannot start a nested transaction. Calling begin() while a transaction is already active raises TransactionError.
Error Example:
db.begin()
try:
    db.begin()  # Error!
except TransactionError as e:
    print(e)  # "Transaction already active"

Committing a Transaction

end()

Commit the active transaction and persist changes to disk.
database.py:230-241
def end(self) -> None:
    """
    Commit the active transaction.
    """
    self._ensure_open()

    if not self._in_transaction:
        raise TransactionError("No active transaction")

    self._tx_snapshot = None
    self._in_transaction = False
    self.commit()
What happens when you call end():
  1. Clears the snapshot (_tx_snapshot = None)
  2. Exits transaction mode (_in_transaction = False)
  3. Calls commit() to write changes to disk
Example:
db = Database("users.pkl")
db.load()

db.begin()
db.set("user:1", {"name": "Alice", "role": "admin"})
db.set("user:2", {"name": "Bob", "role": "user"})
db.end()  # Both users are saved atomically
end() automatically calls commit() to persist changes. You don’t need to call commit() separately.

Rolling Back a Transaction

rollback()

Discard all changes made during the transaction and restore the database to its state when begin() was called.
database.py:215-228
def rollback(self) -> None:
    """
    Roll back the active transaction.
    """
    self._ensure_open()

    if not self._in_transaction:
        raise TransactionError("No active transaction")

    if self._tx_snapshot is not None:
        self._data = self._tx_snapshot
    self._tx_snapshot = None
    self._in_transaction = False
    self._dirty = False
What happens during rollback:
  1. Restores _data from the snapshot
  2. Clears the snapshot
  3. Exits transaction mode
  4. Resets the dirty flag (no changes to persist)
Example:
db = Database("inventory.pkl")
db.load()

# Initial state
db.set("product:1", {"name": "Widget", "stock": 100})
db.commit()

db.begin()
db.update("product:1", {"stock": 0})  # Reduce stock to 0
db.set("product:2", {"name": "Gadget", "stock": 50})

# Something went wrong, rollback!
db.rollback()

# Data is restored to pre-transaction state
print(db.get("product:1"))  # {"name": "Widget", "stock": 100}
print(db.exists("product:2"))  # False - product:2 was never committed
Use rollback() when validation fails or an error occurs during transaction processing.

Transaction Scenarios

Scenario 1: Successful Transaction

from binarydb.database import Database

db = Database("bank.pkl")
db.load()

# Initial balances
db.set("account:alice", {"balance": 1000})
db.set("account:bob", {"balance": 500})
db.commit()

# Transfer money atomically
db.begin()
try:
    alice = db.get("account:alice")
    bob = db.get("account:bob")
    
    transfer_amount = 200
    
    if alice["balance"] >= transfer_amount:
        alice["balance"] -= transfer_amount
        bob["balance"] += transfer_amount
        
        db.set("account:alice", alice)
        db.set("account:bob", bob)
        
        db.end()  # Commit transaction
        print("Transfer successful")
    else:
        db.rollback()
        print("Insufficient funds")
        
except Exception as e:
    db.rollback()  # Ensure rollback on any error
    print(f"Transfer failed: {e}")

Scenario 2: Rollback on Validation Error

from binarydb.database import Database
from binarydb.errors import KeyValidationError

def create_user_with_preferences(db, user_id, name, preferences):
    """Create user and preferences atomically."""
    db.begin()
    try:
        # Create user record
        db.set(f"user:{user_id}", {"name": name, "created_at": "2026-03-04"})
        
        # Validate preferences
        if not isinstance(preferences, dict):
            raise ValueError("Preferences must be a dictionary")
        
        # Create preferences
        db.set(f"prefs:{user_id}", preferences)
        
        db.end()
        return True
        
    except (KeyValidationError, ValueError) as e:
        db.rollback()
        print(f"User creation failed: {e}")
        return False

db = Database("app.pkl")
db.load()

# This succeeds
create_user_with_preferences(db, "123", "Alice", {"theme": "dark"})

# This fails and rolls back
create_user_with_preferences(db, "456", "Bob", "not-a-dict")

print(db.exists("user:456"))  # False - rolled back

Scenario 3: Nested Transaction Error

db = Database("mydata.pkl")
db.load()

db.begin()
db.set("key1", "value1")

try:
    db.begin()  # Trying to start nested transaction
except TransactionError as e:
    print(f"Error: {e}")  # "Transaction already active"

db.end()  # Commit the first transaction

Transaction Without Active Transaction Errors

Calling end() or rollback() without an active transaction raises an error:
db = Database("mydata.pkl")
db.load()

try:
    db.end()  # No transaction started
except TransactionError as e:
    print(e)  # "No active transaction"

try:
    db.rollback()  # No transaction started
except TransactionError as e:
    print(e)  # "No active transaction"

Snapshot Isolation

BinaryDB uses snapshot isolation for transactions:
  1. When begin() is called, a complete copy of _data is stored in _tx_snapshot
  2. All operations during the transaction modify _data directly
  3. On rollback(), _data is replaced with the snapshot
  4. On end(), the snapshot is discarded and changes are committed
Visualization:
# Before transaction
_data = {"key1": "value1", "key2": "value2"}
_tx_snapshot = None

db.begin()
# After begin()
_data = {"key1": "value1", "key2": "value2"}
_tx_snapshot = {"key1": "value1", "key2": "value2"}  # Snapshot created

db.set("key3", "value3")
# During transaction
_data = {"key1": "value1", "key2": "value2", "key3": "value3"}
_tx_snapshot = {"key1": "value1", "key2": "value2"}  # Unchanged

db.rollback()
# After rollback()
_data = {"key1": "value1", "key2": "value2"}  # Restored from snapshot
_tx_snapshot = None
The snapshot is a shallow copy. If your data contains nested mutable objects, modifications to those objects affect both the snapshot and current data.

Best Practices

Wrap transaction code in try-except and rollback on errors:
db.begin()
try:
    # ... operations ...
    db.end()
except Exception as e:
    db.rollback()
    raise
Minimize the time spent in a transaction to reduce memory overhead and improve responsiveness:
# Good: Short transaction
db.begin()
db.set("key", value)
db.end()

# Bad: Long-running transaction
db.begin()
for i in range(1000000):
    db.set(f"key:{i}", expensive_computation())
db.end()
Always close your transaction with either end() or rollback(). Leaving a transaction open blocks the dirty flag mechanism:
db.begin()
db.set("key", "value")
# Forgot to call end() or rollback()!

# Later operations won't auto-commit
db.set("another_key", "value")  # _dirty stays False!
Perform all validation before calling end() to avoid partial commits:
db.begin()
try:
    # Perform all operations
    db.set("user", user_data)
    db.set("profile", profile_data)
    
    # Validate
    if not validate_data(user_data, profile_data):
        raise ValueError("Validation failed")
    
    db.end()
except Exception:
    db.rollback()
    raise

Complete Transaction Example

from binarydb.database import Database
from binarydb.errors import TransactionError, KeyValidationError
import logging

logging.basicConfig(level=logging.INFO)

def transfer_inventory(db, from_warehouse, to_warehouse, product, quantity):
    """
    Transfer inventory between warehouses atomically.
    """
    db.begin()
    try:
        # Get current inventory
        from_inv = db.get(f"inventory:{from_warehouse}", {})
        to_inv = db.get(f"inventory:{to_warehouse}", {})
        
        # Validate transfer
        if from_inv.get(product, 0) < quantity:
            raise ValueError(f"Insufficient stock of {product}")
        
        # Update inventory
        from_inv[product] = from_inv.get(product, 0) - quantity
        to_inv[product] = to_inv.get(product, 0) + quantity
        
        # Save changes
        db.set(f"inventory:{from_warehouse}", from_inv)
        db.set(f"inventory:{to_warehouse}", to_inv)
        
        # Log transfer
        db.set(f"transfer:{from_warehouse}:{to_warehouse}", {
            "product": product,
            "quantity": quantity,
            "timestamp": "2026-03-04T10:30:00"
        })
        
        db.end()
        logging.info(f"Transferred {quantity} {product} from {from_warehouse} to {to_warehouse}")
        return True
        
    except (ValueError, KeyValidationError, TransactionError) as e:
        db.rollback()
        logging.error(f"Transfer failed: {e}")
        return False

# Usage
db = Database("warehouse.pkl")
db.load()

# Setup initial inventory
db.set("inventory:warehouse_a", {"widget": 100, "gadget": 50})
db.set("inventory:warehouse_b", {"widget": 20, "gadget": 80})
db.commit()

# Transfer inventory
transfer_inventory(db, "warehouse_a", "warehouse_b", "widget", 30)

db.close()

Next Steps

Persistence

Learn how transactions are persisted to disk

Error Handling

Handle transaction errors gracefully

Build docs developers (and LLMs) love