> ## Documentation Index
> Fetch the complete documentation index at: https://cindel.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Transactions: Atomic Reads, Writes, and Rollback in Cindel

> Group multiple Cindel operations into atomic read or write transactions that automatically commit on success and roll back on error.

Transactions group database work into one consistent operation. Use `readTxn` when several reads must share a stable snapshot, and `writeTxn` when a set of writes must either all succeed or all be discarded together. Single calls such as a lone `put` or `delete` don't need an explicit transaction — reach for one only when a workflow involves more than one database step that must stay consistent.

<Tip>
  Single `put` and `delete` calls are already atomic on their own. You only need an explicit transaction when multiple operations must commit or roll back as a unit.
</Tip>

## Read Transactions

`readTxn` runs a callback inside a consistent read snapshot. Every query inside the callback sees the same database state — even if other writes land between them.

```dart theme={null}
final summary = await db.readTxn(() async {
  final open = await db.todos
      .filter()
      .completedEqualTo(false)
      .count();

  final done = await db.todos
      .filter()
      .completedEqualTo(true)
      .count();

  return (open: open, done: done);
});
```

Do not attempt any writes inside `readTxn`. Calling `put`, `delete`, or any other write operation inside a read transaction throws a `CindelTransactionError`.

```dart theme={null}
await db.readTxn(() async {
  await db.todos.put(todo); // Throws CindelTransactionError.
});
```

## Write Transactions

`writeTxn` runs writes atomically. If every operation in the callback completes without throwing, the transaction commits. If the callback throws for any reason, Cindel rolls the entire transaction back — no partial writes are ever persisted.

```dart theme={null}
// Simple write transaction — persist a single object alongside an audit event.
await db.writeTxn(() async {
  await db.todos.put(todo);
  await db.auditEvents.put(event);
});
```

Use a write transaction whenever a set of changes must commit as one logical unit:

```dart theme={null}
// Persist an order and all its line items together.
await db.writeTxn(() async {
  await db.orders.put(order);
  await db.orderLines.putAll(lines);
});
```

You can also read inside a `writeTxn` when a write depends on existing data:

```dart theme={null}
await db.writeTxn(() async {
  final todo = await db.todos.get(todoId);
  if (todo == null) {
    throw StateError('Todo not found.');
  }

  todo.completed = true;
  await db.todos.put(todo);
});
```

## Rollback Behavior

When the `writeTxn` callback throws, Cindel rolls the transaction back automatically. Rolled-back writes are never committed to storage, and watcher notifications for those writes are never emitted. The error is returned to the caller so you can retry, show an error, or abandon the operation.

```dart theme={null}
try {
  await db.writeTxn(() async {
    await db.todos.put(todo);
    throw StateError('Something went wrong.');
  });
} catch (e) {
  // The put was rolled back. Handle the error here.
}
```

## Multi-Step Example: Checkout

The following checkout example shows a read-then-write pattern inside a single `writeTxn`. The stock check and the inventory deduction belong to the same business operation. If stock is insufficient, the callback throws and nothing is committed.

```dart theme={null}
await db.writeTxn(() async {
  final product = await db.products.get(productId);

  if (product == null || product.stock < quantity) {
    throw StateError('Not enough stock.');
  }

  await db.products.put(
    product.copyWith(stock: product.stock - quantity),
  );

  await db.orders.put(order);
  await db.orderLines.putAll(lines);
});
```

## Common Mistakes

<Accordion title="Writing inside readTxn → CindelTransactionError">
  `readTxn` is for reads only. Any write call inside a read transaction throws `CindelTransactionError` immediately.

  ```dart theme={null}
  // ❌ Wrong — will throw.
  await db.readTxn(() async {
    await db.todos.put(todo);
  });

  // ✅ Correct — use writeTxn for writes.
  await db.writeTxn(() async {
    await db.todos.put(todo);
  });
  ```
</Accordion>

<Accordion title="Nesting explicit transactions → rejected">
  Cindel rejects nested explicit transactions. If you open a `writeTxn` inside another `writeTxn`, the inner call is rejected.

  ```dart theme={null}
  // ❌ Wrong — nested transaction is rejected.
  await db.writeTxn(() async {
    await db.writeTxn(() async {
      await db.todos.put(todo);
    });
  });
  ```

  Keep transaction boundaries at the outermost caller level. Helper methods should perform reads and writes without opening their own transactions, leaving that responsibility to the code that calls them.
</Accordion>

<Accordion title="Swallowing errors inside a transaction → rollback won't fire">
  Rollback depends on the callback throwing. If you catch an error inside the callback and don't rethrow it, Cindel treats the callback as successful and commits whatever ran before the catch.

  ```dart theme={null}
  // ❌ Wrong — error is swallowed; partial writes may commit.
  await db.writeTxn(() async {
    try {
      await db.todos.put(todo);
      throw StateError('Invalid follow-up step.');
    } catch (_) {
      // Silently ignoring the error — Cindel sees a clean return.
    }
  });

  // ✅ Correct — rethrow so the transaction rolls back.
  await db.writeTxn(() async {
    try {
      await db.todos.put(todo);
      throw StateError('Invalid follow-up step.');
    } catch (_) {
      rethrow;
    }
  });
  ```
</Accordion>

<Accordion title="Doing unrelated work inside a transaction → keep them DB-scoped">
  Transactions hold a database lock for their duration. Waiting on network calls, showing UI prompts, or running any long-running non-database work inside a transaction callback blocks other database operations and increases the risk of contention.

  Prepare all inputs **before** entering the transaction, then perform only the database reads and writes that must be atomic inside the callback.

  ```dart theme={null}
  // ❌ Wrong — network call inside the transaction.
  await db.writeTxn(() async {
    final data = await fetchFromServer(); // Don't do this.
    await db.todos.put(Todo.fromJson(data));
  });

  // ✅ Correct — fetch first, then write.
  final data = await fetchFromServer();
  await db.writeTxn(() async {
    await db.todos.put(Todo.fromJson(data));
  });
  ```
</Accordion>
