🔒 Fractal Platform Transactions Guide

Master database transactions and data consistency

0. Sample Data

Throughout this guide, we'll work with two collections: "Users" and "Cars", each containing one document.

Users Collection - Document (ID: 0000000001)

{
    "Age": 20,
    "Name": "Bob",
    "Car": {
        "Engine": 2,
        "Model": "Subaru"
    },
    "Childs": [
        {
            "Age": 3,
            "Name": "Ted",
            "Numbers": [4, 5, 6]
        },
        {
            "Age": 5,
            "Name": "Mike",
            "Numbers": [7, 8, 9]
        }
    ],
    "Jobs": [
        {
            "Company": "Microsoft",
            "Position": "Engineer"
        },
        {
            "Company": "Google",
            "Position": "Engineer"
        },
        {
            "Company": "Amazon",
            "Position": "Manager"
        }
    ],
    "Numbers": [1, 2, 3]
}

Cars Collection - Document (ID: 0000000001)

{
    "Company": "VW",
    "Car": "Touran",
    "Engine": "2.0"
}
💡 Key Points:
  • Users collection: Contains Bob's complete profile with nested objects and arrays
  • Cars collection: Contains a VW Touran vehicle record
  • We'll use these documents to demonstrate transaction behavior

1. What Are Transactions?

Definition

A transaction is a sequence of database operations that are treated as a single unit of work. Think of it like a shopping cart - you add items, review them, and then either complete the purchase (commit) or cancel everything (rollback).

ACID Properties

Transactions follow the ACID principles:

Property Meaning Example
Atomicity All operations succeed or all fail If updating age fails, name update is also cancelled
Consistency Database remains in valid state Data relationships are preserved
Isolation Transactions don't interfere with each other Two users can update different records simultaneously
Durability Committed changes are permanent Data survives system crashes

Why Use Transactions?

💡 Real-World Example: When transferring money between bank accounts, you want to ensure that money is deducted from one account and added to another as a single operation. If either fails, both should be cancelled.

2. Transaction Isolation Levels

Fractal Platform supports three transaction isolation levels that control how transactions interact with each other:

Read Committed

The most basic isolation level. A transaction only sees data that has been committed by other transactions.

Client.BeginTran(TranType.ReadCommited);
💡 Use Case: General-purpose queries where you don't need strict consistency. Fast and allows good concurrency.
Pros Cons
✅ Prevents dirty reads
✅ Good performance
✅ High concurrency
❌ Non-repeatable reads possible
❌ Phantom reads possible

Repeatable Read

Ensures that if you read data twice in the same transaction, you'll get the same result. The data is locked for reading.

Client.BeginTran(TranType.RepeatableRead);
💡 Use Case: Financial calculations, reports where data must remain consistent throughout the transaction.
Pros Cons
✅ Prevents dirty reads
✅ Prevents non-repeatable reads
✅ Data consistency guaranteed
❌ Lower concurrency
❌ Possible lock conflicts
❌ Phantom reads possible

Snapshot

Each transaction works with a snapshot of the database as it existed at the start of the transaction. No locks are needed for reading.

Client.BeginTran(TranType.Snapshot);
💡 Use Case: Long-running reports, complex queries where you need consistent data without blocking other transactions.
Pros Cons
✅ No read locks needed
✅ Prevents all anomalies
✅ Best consistency
✅ Good for long transactions
❌ Higher memory usage
❌ Possible update conflicts
⚠️ Important: The isolation level affects how your transaction sees changes made by other transactions and how it locks data. Choose based on your specific needs for consistency vs. performance.

3. Basic Transaction Operations

Implicit Transactions

Every query in Fractal Platform runs in an implicit transaction. This means each operation is automatically committed:

// This operation runs in an implicit transaction
var count = DocsWhere("Users", 1)
                  .Count();
// Result: 1 (Bob's document exists)

Explicit Transactions

For complex operations, you can create explicit transactions with full control:

// 1. Begin transaction
Client.BeginTran(TranType.ReadCommited);

// 2. Perform operations
ModifyDocsWhere("Users", "{'Name':'Bob'}")
      .Update("{'Age':30}");

// 3. Commit or rollback
Client.CommitTran();  // Save changes
// OR
Client.RollbackTran(); // Cancel changes

Commit - Saving Changes

When you commit a transaction, all changes become permanent:

Client.BeginTran(TranType.ReadCommited);

// Update Bob's age
ModifyDocsWhere("Users", "{'Name':'Bob'}")
      .Update("{'Age':25}");

// Changes are visible inside transaction
var users = DocsWhere("Users", "{'Name':'Bob'}")
                  .Select<UserInfo>();
// users[0].Age == 25

Client.CommitTran();

// Changes are now permanent
users = DocsWhere("Users", "{'Name':'Bob'}")
              .Select<UserInfo>();
// users[0].Age == 25 (still 25 after commit)

Rollback - Cancelling Changes

Rollback undoes all changes made during the transaction:

Client.BeginTran(TranType.ReadCommited);

// Update Bob's age
ModifyDocsWhere("Users", "{'Name':'Bob'}")
      .Update("{'Age':30}");

// Changes are visible inside transaction
var users = DocsWhere("Users", "{'Name':'Bob'}")
                  .Select<UserInfo>();
// users[0].Age == 30

Client.RollbackTran();

// Changes are cancelled - original value restored
users = DocsWhere("Users", "{'Name':'Bob'}")
              .Select<UserInfo>();
// users[0].Age == 20 (original value)
✅ Best Practice: Always use try-catch blocks with transactions to ensure proper cleanup, and always either commit or rollback before the transaction ends.

4. Transactions with Single Document

Reading in a Transaction

All three isolation levels support reading documents within a transaction:

// Read Committed
Client.BeginTran(TranType.ReadCommited);

var count = DocsWhere("Users", 1)
                  .Count();
// Result: 1 (Bob exists)

count = DocsWhere("Users", 555)
              .Count();
// Result: 0 (document 555 doesn't exist)

Client.CommitTran();

Writing with Commit

Update data and save changes permanently:

Client.BeginTran(TranType.RepeatableRead);

// Update Bob's age
ModifyDocsWhere("Users", "{'Name':'Bob'}")
      .Update("{'Age':20}");

// Verify inside transaction
var users = DocsWhere("Users", "{'Name':'Bob'}")
                  .Select<UserInfo>();

Assert.AreEqual(20, users[0].Age.Value);

Client.CommitTran();

// Verify after commit - changes are permanent
users = DocsWhere("Users", "{'Name':'Bob'}")
              .Select<UserInfo>();

Assert.AreEqual(20, users[0].Age.Value);
// Age is still 20

Writing with Rollback

Update data but cancel the changes:

Client.BeginTran(TranType.Snapshot);

// Update Bob's age to 25
ModifyDocsWhere("Users", "{'Name':'Bob'}")
      .Update("{'Age':25}");

// Verify inside transaction - shows new value
var users = DocsWhere("Users", "{'Name':'Bob'}")
                  .Select<UserInfo>();

Assert.AreEqual(25, users[0].Age.Value);

Client.RollbackTran();

// Verify after rollback - original value restored
users = DocsWhere("Users", "{'Name':'Bob'}")
              .Select<UserInfo>();

Assert.AreEqual(20, users[0].Age.Value);
// Age is back to 20
💡 Key Insight: Inside a transaction, you always see your own changes. The isolation level determines what you see from other transactions and when locks are acquired.

5. Transactions with Multiple Documents

Adding Documents with Rollback

When you add a document in a transaction and rollback, the document is not created:

Client.BeginTran(TranType.ReadCommited);

// Add a new user
AddDoc("Users", "{'Name':'Tim'}");

// Count inside transaction
var count = DocsOf("Users")
                .Count();

Assert.AreEqual(2, count);
// We see 2 documents: Bob and Tim

Client.RollbackTran();

// Count after rollback
count = DocsOf("Users")
            .Count();

Assert.AreEqual(1, count);
// Back to 1 document: only Bob remains

Adding Documents with Commit

When you add a document and commit, it becomes permanent:

Client.BeginTran(TranType.RepeatableRead);

// Add a new user and save the ID
var newDocID = AddDoc("Users", "{'Name':'Tim'}");

// Count inside transaction
var count = DocsOf("Users")
                  .Count();

Assert.AreEqual(2, count);

Client.CommitTran();

// Count after commit
count = DocsOf("Users")
            .Count();

Assert.AreEqual(2, count);
// Still 2 documents - Tim was permanently added

// Clean up - delete the new document
DelDoc("Users", newDocID);
⚠️ Important: When adding multiple documents in a transaction, either all are created (on commit) or none are created (on rollback). This maintains atomicity.

Multiple Operations in One Transaction

You can perform multiple operations on different documents:

Client.BeginTran(TranType.Snapshot);

// Update Bob
ModifyDocsWhere("Users", "{'Name':'Bob'}")
      .Update("{'Age':25}");

// Add Tim
AddDoc("Users", "{'Name':'Tim','Age':30}");

// Both operations are in the same transaction
Client.CommitTran();
// Both changes are saved together

6. Transactions with Multiple Collections

Cross-Collection Rollback

Transactions can span multiple collections. If rolled back, changes in all collections are cancelled:

Client.BeginTran(TranType.ReadCommited);

// Add document to Users collection
AddDoc("Users", "{'Name':'Tim'}");

// Add document to Cars collection
AddDoc("Cars", "{'Model':'Tesla'}");

// Verify both collections inside transaction
var userCount = DocsOf("Users")
                      .Count();
Assert.AreEqual(2, userCount);
// Users: Bob + Tim = 2

var carCount = DocsOf("Cars")
                    .Count();
Assert.AreEqual(2, carCount);
// Cars: VW Touran + Tesla = 2

Client.RollbackTran();

// Verify after rollback - both additions cancelled
userCount = DocsOf("Users")
                .Count();
Assert.AreEqual(1, userCount);
// Back to 1: only Bob

carCount = DocsOf("Cars")
               .Count();
Assert.AreEqual(1, carCount);
// Back to 1: only VW Touran

Cross-Collection Commit

When committing, all changes across all collections are saved together:

Client.BeginTran(TranType.RepeatableRead);

// Add to Users
var newUserID = AddDoc("Users", "{'Name':'Tim'}");

// Add to Cars
var newCarID = AddDoc("Cars", "{'Model':'Tesla'}");

// Verify inside transaction
var userCount = DocsOf("Users")
                      .Count();
Assert.AreEqual(2, userCount);

var carCount = DocsOf("Cars")
                    .Count();
Assert.AreEqual(2, carCount);

Client.CommitTran();

// Verify after commit - both additions permanent
userCount = DocsOf("Users")
                .Count();
Assert.AreEqual(2, userCount);

carCount = DocsOf("Cars")
               .Count();
Assert.AreEqual(2, carCount);

// Clean up
DelDoc("Users", newUserID);
DelDoc("Cars", newCarID);
✅ Atomicity Across Collections: When working with multiple collections, transactions ensure that either all changes succeed or all fail together. This is crucial for maintaining data consistency across related collections.

7. Concurrent Transactions

Read Committed Isolation

With Read Committed, transactions see committed data from other transactions:

// Initial state: Bob's age is 25
DocsWhere("Users", "{'Name':'Bob'}")
      .Update("{'Age':25}");

// Client 1: Start transaction
Client.BeginTran(TranType.ReadCommited);

// Client 1: Update Bob's age to 20
DocsWhere("Users", "{'Name':'Bob'}")
      .Update("{'Age':20}");

// Client 1: Sees their own changes
var users = DocsWhere("Users", "{'Name':'Bob'}")
                  .Select<UserInfo>();
Assert.AreEqual(20, users[0].Age.Value);

// Client 2: Sees old committed data (25)
users = otherClient.DocsWhere("Users", "{'Name':'Bob'}")
                   .Select<UserInfo>();
Assert.AreEqual(25, users[0].Age.Value);

// Client 1: Rollback changes
Client.RollbackTran();

Repeatable Read with Locks

Repeatable Read locks data for reading. Other transactions cannot modify locked data:

// Set error handling for locks
TranManager.IsRaiseErrorOnLock = true;

// Client 1: Start Repeatable Read transaction
Client.BeginTran(TranType.RepeatableRead);

// Client 1: Read Bob's age (locks the data)
var age = DocsWhere("Users", "{'Name':'Bob'}")
                .Value("{'Age':$}");
Assert.AreEqual("25", age);

// Client 2: Try to update locked data - FAILS
otherClient.DocsWhere("Users", "{'Name':'Bob'}")
           .Update("{'Age':20}");

Assert.AreEqual(true, otherClient.Context.HasError);
// Update blocked because Client 1 has a read lock

otherClient.Context.ResetError();

// Client 1: Commit and release lock
Client.CommitTran();

// Client 2: Now update succeeds
otherClient.DocsWhere("Users", "{'Name':'Bob'}")
           .Update("{'Age':20}");

age = DocsWhere("Users", "{'Name':'Bob'}")
          .Value("{'Age':$}");
Assert.AreEqual("20", age);

Snapshot Isolation

Snapshot isolation prevents write conflicts but allows concurrent reads:

// Initial state: Bob's age is 25
DocsWhere("Users", "{'Name':'Bob'}")
      .Update("{'Age':25}");

// Client 1: Start Snapshot transaction
Client.BeginTran(TranType.Snapshot);

// Client 2: Try to update - FAILS (write lock)
otherClient.DocsWhere("Users", "{'Name':'Bob'}")
           .Update("{'Age':20}");

Assert.AreEqual(true, otherClient.Context.HasError);

otherClient.Context.ResetError();

// Client 1: Update data in snapshot
DocsWhere("Users", "{'Name':'Bob'}")
      .Update("{'Age':20}");

// Client 1: Commit
Client.CommitTran();

// Client 2: Can now read updated value
var age = otherClient.DocsWhere("Users", "{'Name':'Bob'}")
                       .Value("{'Age':$}");
Assert.AreEqual("20", age);
⚠️ Lock Conflicts: When TranManager.IsRaiseErrorOnLock is true, operations that encounter locks will fail with an error instead of waiting. This helps detect and handle concurrent access issues.

8. Best Practices

Choose the Right Isolation Level

Use Case Recommended Level Reason
Simple reads Read Committed Fast, good concurrency
Financial calculations Repeatable Read Prevents data changes during calculation
Long reports Snapshot Consistent view without blocking
Batch updates Read Committed Balance between consistency and performance

Keep Transactions Short

// ❌ BAD - Long transaction
Client.BeginTran(TranType.RepeatableRead);
var users = DocsOf("Users").Select<UserInfo>();
// ... lots of processing ...
// ... user interaction ...
ModifyDocsWhere("Users", "{'Name':'Bob'}").Update("{'Age':30}");
Client.CommitTran();

// ✅ GOOD - Short transaction
var users = DocsOf("Users").Select<UserInfo>();
// ... processing outside transaction ...
Client.BeginTran(TranType.RepeatableRead);
ModifyDocsWhere("Users", "{'Name':'Bob'}").Update("{'Age':30}");
Client.CommitTran();

Always Handle Errors

// ✅ GOOD - Proper error handling
try
{
    Client.BeginTran(TranType.RepeatableRead);
    
    ModifyDocsWhere("Users", "{'Name':'Bob'}")
          .Update("{'Age':30}");
    
    AddDoc("Cars", "{'Model':'Tesla'}");
    
    Client.CommitTran();
}
catch (Exception ex)
{
    Client.RollbackTran();
    // Log error and handle appropriately
}

Avoid Deadlocks

Test with Concurrent Access

Always test your application with multiple simultaneous users to identify potential concurrency issues:

// Create a second client for testing
var context = CreateUserContext();
var otherClient = CreateClient(context, Instance);

// Test concurrent operations
Client.BeginTran(TranType.RepeatableRead);
// ... Client 1 operations ...

// Attempt conflicting operation from Client 2
otherClient.DocsWhere("Users", "{'Name':'Bob'}")
           .Update("{'Age':40}");

// Check for conflicts
if (otherClient.Context.HasError)
{
    // Handle the error appropriately
}

Document Your Transaction Strategy

Always document which isolation level you're using and why:

// Using Repeatable Read to ensure consistent totals
// during financial report generation
Client.BeginTran(TranType.RepeatableRead);
var total = CalculateOrderTotal();
Client.CommitTran();
✅ Golden Rules:
  • Keep transactions as short as possible
  • Always use try-catch with proper rollback
  • Choose the appropriate isolation level for your use case
  • Test with concurrent access scenarios
  • Monitor for lock conflicts and deadlocks