Skip to main content

Transactions and Write Management

DB plugin provides a robust transaction system for managing write operations. Transactions ensure data consistency, enable batch operations, and provide validation before any data reaches the database.

Transaction Lifecycle

Basic Transaction Pattern

beginWrites / commitWrites / endWrites

Database db = Database.Static.getDefault();

db.beginWrites();
try {
// Make changes
article1.save();
article2.save();
author.save();

// Commit changes
db.commitWrites();

} finally {
// Always clean up
db.endWrites();
}

What Happens

  1. beginWrites() - Starts a transaction, increments depth counter
  2. save() - Buffers the write, doesn't touch the database yet
  3. commitWrites() - Validates all buffered writes, then writes them atomically
  4. endWrites() - Decrements depth counter, cleans up resources

Why Use Transactions

// Without transaction - NOT atomic
article.save(); // Might succeed
author.save(); // Might fail
// Now you have inconsistent data!

// With transaction - atomic
db.beginWrites();
try {
article.save();
author.save();
db.commitWrites(); // Both succeed or both fail
} finally {
db.endWrites();
}

Nested Transactions

DB plugin supports nested transactions using a depth counter:

Database db = Database.Static.getDefault();

db.beginWrites(); // Depth 1
try {
article.save();

db.beginWrites(); // Depth 2 (nested)
try {
author.save();
db.commitWrites(); // Commits only at depth 1
} finally {
db.endWrites(); // Back to depth 1
}

db.commitWrites(); // Actually commits everything
} finally {
db.endWrites(); // Depth 0
}

How Nesting Works

  • beginWrites() increments a counter
  • endWrites() decrements it
  • commitWrites() only writes when counter reaches 0
  • Allows method composition without worrying about transaction state
public void saveArticleWithAuthor(Article article, Author author) {
// This method doesn't know if it's called within a transaction
db.beginWrites();
try {
article.save();
author.save();
db.commitWrites();
} finally {
db.endWrites();
}
}

// Called standalone - commits immediately
saveArticleWithAuthor(article, author);

// Called within transaction - commits with outer transaction
db.beginWrites();
try {
saveArticleWithAuthor(article1, author1);
saveArticleWithAuthor(article2, author2);
db.commitWrites(); // All 4 objects committed together
} finally {
db.endWrites();
}

Commit Strategies

commitWrites - Immediate Consistency

Commits changes immediately with strong consistency guarantees:

db.beginWrites();
try {
article.save();
db.commitWrites();
// Data is immediately visible to other transactions
} finally {
db.endWrites();
}

Use when:

  • You need immediate read-after-write consistency
  • Other processes depend on this data immediately
  • Working with critical data (financial, inventory, etc.)

commitWritesEventually - Eventual Consistency

Commits changes for eventual consistency, allowing for optimization:

db.beginWrites();
try {
article.save();
db.commitWritesEventually();
// Data will be visible soon, but maybe not immediately
} finally {
db.endWrites();
}

Use when:

  • Eventual consistency is acceptable
  • Performance is critical
  • Working with caching or replication systems
  • Bulk imports or batch operations

The database implementation may:

  • Batch writes together
  • Delay replication
  • Optimize index updates
  • Trade immediate consistency for throughput

Isolated Writes

Create an isolated write session that doesn't interact with the main transaction:

// Main transaction
db.beginWrites();
try {
article.save();

// Isolated session
db.beginIsolatedWrites();
try {
// This saves independently
auditLog.save();
db.commitWrites();
} finally {
db.endWrites();
}

// Main transaction continues
db.commitWrites();
} finally {
db.endWrites();
}

Use cases:

  • Audit logging that should succeed even if main transaction fails
  • Separate concerns (e.g., metrics, analytics)
  • Writing to different databases

Write Buffering and Validation

The Validation Phase

All validation happens before any database writes:

db.beginWrites();
try {
// These are buffered
article1.save();
article2.save();
article3.save();

// This triggers validation of ALL three
db.commitWrites();

// If any validation fails, NONE are written
} catch (ValidationException e) {
// Handle validation errors for all objects
Map<Object, Map<ObjectField, List<Throwable>>> errors = e.getAllErrors();
} finally {
db.endWrites();
}

Validation Order

  1. Annotation validation - @Required, @Maximum, etc.
  2. beforeSave() hooks - Custom pre-save logic
  3. onValidate() hooks - Custom validation logic
  4. afterValidate() hooks - Post-validation logic
  5. Database write - Only if all validation passes
  6. afterSave() hooks - Post-save logic

Example: Custom Validation

public class Article extends Record {

@Required
private String title;

@Required
private String content;

@Override
protected void beforeSave() {
// Normalize data before validation
if (title != null) {
title = title.trim();
}
}

@Override
protected void onValidate() {
// Custom validation logic
if (content != null && content.length() < 100) {
getState().addError(
getState().getField("content"),
new IllegalArgumentException("Content must be at least 100 characters")
);
}

if (title != null && title.toLowerCase().contains("spam")) {
getState().addError(
getState().getField("title"),
new IllegalArgumentException("Title contains forbidden words")
);
}
}

@Override
protected void afterSave() {
// Post-save logic (cache invalidation, notifications, etc.)
System.out.println("Article saved: " + getLabel());
}
}

Error Handling

Handling ValidationException

db.beginWrites();
try {
article.save();
db.commitWrites();

} catch (ValidationException e) {
// Get all validation errors
Map<Object, Map<ObjectField, List<Throwable>>> allErrors = e.getAllErrors();

for (Map.Entry<Object, Map<ObjectField, List<Throwable>>> entry : allErrors.entrySet()) {
Object object = entry.getKey();
Map<ObjectField, List<Throwable>> fieldErrors = entry.getValue();

System.out.println("Errors in " + object.getClass().getSimpleName() + ":");

for (Map.Entry<ObjectField, List<Throwable>> fieldEntry : fieldErrors.entrySet()) {
String fieldName = fieldEntry.getKey().getInternalName();
for (Throwable error : fieldEntry.getValue()) {
System.out.println(" " + fieldName + ": " + error.getMessage());
}
}
}

} catch (DatabaseException e) {
// Handle database errors (connection, constraint violations, etc.)
logger.error("Database error", e);
throw e;

} finally {
db.endWrites();
}

Handling Database Errors

db.beginWrites();
try {
article.save();
db.commitWrites();

} catch (DatabaseException e) {
// Check for specific error types
if (e.getCause() instanceof SQLException) {
SQLException sqlEx = (SQLException) e.getCause();

// Unique constraint violation
if (sqlEx.getErrorCode() == 1062) { // MySQL duplicate entry
throw new DuplicateException("Article already exists", e);
}

// Deadlock
if (sqlEx.getErrorCode() == 1213) { // MySQL deadlock
// Retry logic
return retryOperation();
}
}

throw e;

} finally {
db.endWrites();
}

Automatic Retry

DB plugin automatically retries certain recoverable errors:

// This is handled automatically by AbstractDatabase
db.beginWrites();
try {
article.save();
db.commitWrites(); // Will retry on transient errors
} finally {
db.endWrites();
}

Automatically retried errors:

  • Connection timeouts
  • Deadlocks
  • Transient network failures
  • Lock wait timeouts

Uses exponential backoff with configurable max attempts.

Batch Operations

Efficient Batch Writes

public void importArticles(List<ArticleData> dataList) {
Database db = Database.Static.getDefault();

db.beginWrites();
try {
for (ArticleData data : dataList) {
Article article = new Article();
article.setTitle(data.getTitle());
article.setContent(data.getContent());
article.setAuthor(data.getAuthor());
article.save(); // Buffered, not written yet
}

// All articles validated and written in one batch
db.commitWrites();

System.out.println("Imported " + dataList.size() + " articles");

} catch (ValidationException e) {
System.err.println("Import failed due to validation errors");
// None of the articles are saved
throw e;

} finally {
db.endWrites();
}
}

Batch with Progress Tracking

public void importWithProgress(List<ArticleData> dataList) {
Database db = Database.Static.getDefault();
int batchSize = 100;
int processed = 0;

for (int i = 0; i < dataList.size(); i += batchSize) {
int end = Math.min(i + batchSize, dataList.size());
List<ArticleData> batch = dataList.subList(i, end);

db.beginWrites();
try {
for (ArticleData data : batch) {
Article article = new Article();
// ... populate article
article.save();
}

db.commitWrites();
processed += batch.size();
System.out.println("Processed " + processed + " / " + dataList.size());

} catch (Exception e) {
System.err.println("Batch failed at index " + i);
throw e;

} finally {
db.endWrites();
}
}
}

Parallel Batch Processing

public void parallelImport(List<ArticleData> dataList) throws InterruptedException {
int numThreads = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
int batchSize = 100;

List<Future<?>> futures = new ArrayList<>();

for (int i = 0; i < dataList.size(); i += batchSize) {
int start = i;
int end = Math.min(i + batchSize, dataList.size());

Future<?> future = executor.submit(() -> {
List<ArticleData> batch = dataList.subList(start, end);
Database db = Database.Static.getDefault();

db.beginWrites();
try {
for (ArticleData data : batch) {
Article article = new Article();
// ... populate article
article.save();
}
db.commitWrites();
} finally {
db.endWrites();
}
});

futures.add(future);
}

// Wait for all batches to complete
for (Future<?> future : futures) {
try {
future.get();
} catch (ExecutionException e) {
System.err.println("Batch failed: " + e.getCause().getMessage());
}
}

executor.shutdown();
}

Distributed Locks

For unique constraint enforcement across distributed systems:

public class Article extends Record {

@Indexed
@Unique
private String slug;

@Override
protected void beforeSave() {
// DB plugin automatically acquires a distributed lock
// for unique fields during save
}
}

Transaction Patterns

Unit of Work Pattern

public class ArticleService {

public void publishArticle(UUID articleId) {
Database db = Database.Static.getDefault();

db.beginWrites();
try {
Article article = Query.from(Article.class)
.where("_id = ?", articleId)
.first();

if (article == null) {
throw new IllegalArgumentException("Article not found");
}

// Make multiple related changes
article.setPublishDate(db.now());
article.setStatus("published");
article.save();

// Update author stats
Author author = article.getAuthor();
author.incrementPublishedCount();
author.save();

// Create activity log
ActivityLog log = new ActivityLog();
log.setType("article_published");
log.setArticle(article);
log.setTimestamp(db.now());
log.save();

// All changes committed together
db.commitWrites();

} finally {
db.endWrites();
}
}
}

Saga Pattern (Compensating Transactions)

public class OrderService {

public void placeOrder(Order order) {
Database db = Database.Static.getDefault();
boolean inventoryReserved = false;
boolean paymentProcessed = false;

try {
db.beginWrites();

// Step 1: Reserve inventory
reserveInventory(order);
inventoryReserved = true;

// Step 2: Process payment
processPayment(order);
paymentProcessed = true;

// Step 3: Create order
order.setStatus("confirmed");
order.save();

db.commitWrites();

} catch (Exception e) {
// Compensating actions
if (paymentProcessed) {
refundPayment(order);
}
if (inventoryReserved) {
releaseInventory(order);
}
throw e;

} finally {
db.endWrites();
}
}
}

Optimistic Locking

public class Article extends Record {

@Indexed
private int version;

public void updateWithOptimisticLock(String newTitle) {
Database db = Database.Static.getDefault();

db.beginWrites();
try {
// Load current version
Article current = Query.from(Article.class)
.where("_id = ?", this.getId())
.first();

if (current.getVersion() != this.version) {
throw new ConcurrentModificationException(
"Article was modified by another transaction"
);
}

// Update with new version
this.setTitle(newTitle);
this.setVersion(this.version + 1);
this.save();

db.commitWrites();

} finally {
db.endWrites();
}
}
}

Performance Optimization

Batch vs. Individual Commits

// Slow - many small transactions
for (Article article : articles) {
db.beginWrites();
try {
article.save();
db.commitWrites();
} finally {
db.endWrites();
}
}

// Fast - one large transaction
db.beginWrites();
try {
for (Article article : articles) {
article.save();
}
db.commitWrites();
} finally {
db.endWrites();
}

Eventual Consistency for Bulk Operations

// Use eventual consistency for better performance
db.beginWrites();
try {
for (Article article : articles) {
article.save();
}
db.commitWritesEventually(); // Faster than commitWrites()
} finally {
db.endWrites();
}

Skip Validation for Trusted Data

// Import from trusted source - skip validation
db.beginWrites();
try {
for (ArticleData data : trustedData) {
Article article = convertToArticle(data);
db.saveUnsafely(article.getState()); // No validation
}
db.commitWrites();
} finally {
db.endWrites();
}

Best Practices

1. Always Use try-finally

// Good
db.beginWrites();
try {
article.save();
db.commitWrites();
} finally {
db.endWrites();
}

// Bad - endWrites() might not be called
db.beginWrites();
article.save();
db.commitWrites();
db.endWrites();

2. Keep Transactions Short

// Good - short transaction
db.beginWrites();
try {
article.save();
db.commitWrites();
} finally {
db.endWrites();
}

// Bad - long transaction holding locks
db.beginWrites();
try {
article.save();
sendEmail(article); // External I/O
updateSearchIndex(article); // Slow operation
db.commitWrites();
} finally {
db.endWrites();
}

3. Use Appropriate Commit Strategy

// Critical data - immediate consistency
db.beginWrites();
try {
payment.save();
db.commitWrites(); // Must be immediately visible
} finally {
db.endWrites();
}

// Non-critical data - eventual consistency
db.beginWrites();
try {
viewCount.save();
db.commitWritesEventually(); // Performance > consistency
} finally {
db.endWrites();
}

4. Don't Catch and Ignore Exceptions

// Bad - hides errors
db.beginWrites();
try {
article.save();
db.commitWrites();
} catch (Exception e) {
// Ignored!
} finally {
db.endWrites();
}

// Good - handle or propagate
db.beginWrites();
try {
article.save();
db.commitWrites();
} catch (ValidationException e) {
logger.error("Validation failed: {}", e.getMessage());
throw e;
} finally {
db.endWrites();
}