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

1
Database db = Database.Static.getDefault();
2
3
db.beginWrites();
4
try {
5
// Make changes
6
article1.save();
7
article2.save();
8
author.save();
9
10
// Commit changes
11
db.commitWrites();
12
13
} finally {
14
// Always clean up
15
db.endWrites();
16
}

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

1
// Without transaction - NOT atomic
2
article.save(); // Might succeed
3
author.save(); // Might fail
4
// Now you have inconsistent data!
5
6
// With transaction - atomic
7
db.beginWrites();
8
try {
9
article.save();
10
author.save();
11
db.commitWrites(); // Both succeed or both fail
12
} finally {
13
db.endWrites();
14
}

Nested Transactions

DB plugin supports nested transactions using a depth counter:

1
Database db = Database.Static.getDefault();
2
3
db.beginWrites(); // Depth 1
4
try {
5
article.save();
6
7
db.beginWrites(); // Depth 2 (nested)
8
try {
9
author.save();
10
db.commitWrites(); // Commits only at depth 1
11
} finally {
12
db.endWrites(); // Back to depth 1
13
}
14
15
db.commitWrites(); // Actually commits everything
16
} finally {
17
db.endWrites(); // Depth 0
18
}

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
1
public void saveArticleWithAuthor(Article article, Author author) {
2
// This method doesn't know if it's called within a transaction
3
db.beginWrites();
4
try {
5
article.save();
6
author.save();
7
db.commitWrites();
8
} finally {
9
db.endWrites();
10
}
11
}
12
13
// Called standalone - commits immediately
14
saveArticleWithAuthor(article, author);
15
16
// Called within transaction - commits with outer transaction
17
db.beginWrites();
18
try {
19
saveArticleWithAuthor(article1, author1);
20
saveArticleWithAuthor(article2, author2);
21
db.commitWrites(); // All 4 objects committed together
22
} finally {
23
db.endWrites();
24
}

Commit Strategies

commitWrites - Immediate Consistency

Commits changes immediately with strong consistency guarantees:

1
db.beginWrites();
2
try {
3
article.save();
4
db.commitWrites();
5
// Data is immediately visible to other transactions
6
} finally {
7
db.endWrites();
8
}

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:

1
db.beginWrites();
2
try {
3
article.save();
4
db.commitWritesEventually();
5
// Data will be visible soon, but maybe not immediately
6
} finally {
7
db.endWrites();
8
}

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:

1
// Main transaction
2
db.beginWrites();
3
try {
4
article.save();
5
6
// Isolated session
7
db.beginIsolatedWrites();
8
try {
9
// This saves independently
10
auditLog.save();
11
db.commitWrites();
12
} finally {
13
db.endWrites();
14
}
15
16
// Main transaction continues
17
db.commitWrites();
18
} finally {
19
db.endWrites();
20
}

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:

1
db.beginWrites();
2
try {
3
// These are buffered
4
article1.save();
5
article2.save();
6
article3.save();
7
8
// This triggers validation of ALL three
9
db.commitWrites();
10
11
// If any validation fails, NONE are written
12
} catch (ValidationException e) {
13
// Handle validation errors for all objects
14
Map<Object, Map<ObjectField, List<Throwable>>> errors = e.getAllErrors();
15
} finally {
16
db.endWrites();
17
}

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

1
public class Article extends Record {
2
3
@Required
4
private String title;
5
6
@Required
7
private String content;
8
9
@Override
10
protected void beforeSave() {
11
// Normalize data before validation
12
if (title != null) {
13
title = title.trim();
14
}
15
}
16
17
@Override
18
protected void onValidate() {
19
// Custom validation logic
20
if (content != null && content.length() < 100) {
21
getState().addError(
22
getState().getField("content"),
23
new IllegalArgumentException("Content must be at least 100 characters")
24
);
25
}
26
27
if (title != null && title.toLowerCase().contains("spam")) {
28
getState().addError(
29
getState().getField("title"),
30
new IllegalArgumentException("Title contains forbidden words")
31
);
32
}
33
}
34
35
@Override
36
protected void afterSave() {
37
// Post-save logic (cache invalidation, notifications, etc.)
38
System.out.println("Article saved: " + getLabel());
39
}
40
}

Error Handling

Handling ValidationException

1
db.beginWrites();
2
try {
3
article.save();
4
db.commitWrites();
5
6
} catch (ValidationException e) {
7
// Get all validation errors
8
Map<Object, Map<ObjectField, List<Throwable>>> allErrors = e.getAllErrors();
9
10
for (Map.Entry<Object, Map<ObjectField, List<Throwable>>> entry : allErrors.entrySet()) {
11
Object object = entry.getKey();
12
Map<ObjectField, List<Throwable>> fieldErrors = entry.getValue();
13
14
System.out.println("Errors in " + object.getClass().getSimpleName() + ":");
15
16
for (Map.Entry<ObjectField, List<Throwable>> fieldEntry : fieldErrors.entrySet()) {
17
String fieldName = fieldEntry.getKey().getInternalName();
18
for (Throwable error : fieldEntry.getValue()) {
19
System.out.println(" " + fieldName + ": " + error.getMessage());
20
}
21
}
22
}
23
24
} catch (DatabaseException e) {
25
// Handle database errors (connection, constraint violations, etc.)
26
logger.error("Database error", e);
27
throw e;
28
29
} finally {
30
db.endWrites();
31
}

Handling Database Errors

1
db.beginWrites();
2
try {
3
article.save();
4
db.commitWrites();
5
6
} catch (DatabaseException e) {
7
// Check for specific error types
8
if (e.getCause() instanceof SQLException) {
9
SQLException sqlEx = (SQLException) e.getCause();
10
11
// Unique constraint violation
12
if (sqlEx.getErrorCode() == 1062) { // MySQL duplicate entry
13
throw new DuplicateException("Article already exists", e);
14
}
15
16
// Deadlock
17
if (sqlEx.getErrorCode() == 1213) { // MySQL deadlock
18
// Retry logic
19
return retryOperation();
20
}
21
}
22
23
throw e;
24
25
} finally {
26
db.endWrites();
27
}

Automatic Retry

DB plugin automatically retries certain recoverable errors:

1
// This is handled automatically by AbstractDatabase
2
db.beginWrites();
3
try {
4
article.save();
5
db.commitWrites(); // Will retry on transient errors
6
} finally {
7
db.endWrites();
8
}

Automatically retried errors:

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

Uses exponential backoff with configurable max attempts.

Batch Operations

Efficient Batch Writes

1
public void importArticles(List<ArticleData> dataList) {
2
Database db = Database.Static.getDefault();
3
4
db.beginWrites();
5
try {
6
for (ArticleData data : dataList) {
7
Article article = new Article();
8
article.setTitle(data.getTitle());
9
article.setContent(data.getContent());
10
article.setAuthor(data.getAuthor());
11
article.save(); // Buffered, not written yet
12
}
13
14
// All articles validated and written in one batch
15
db.commitWrites();
16
17
System.out.println("Imported " + dataList.size() + " articles");
18
19
} catch (ValidationException e) {
20
System.err.println("Import failed due to validation errors");
21
// None of the articles are saved
22
throw e;
23
24
} finally {
25
db.endWrites();
26
}
27
}

Batch with Progress Tracking

1
public void importWithProgress(List<ArticleData> dataList) {
2
Database db = Database.Static.getDefault();
3
int batchSize = 100;
4
int processed = 0;
5
6
for (int i = 0; i < dataList.size(); i += batchSize) {
7
int end = Math.min(i + batchSize, dataList.size());
8
List<ArticleData> batch = dataList.subList(i, end);
9
10
db.beginWrites();
11
try {
12
for (ArticleData data : batch) {
13
Article article = new Article();
14
// ... populate article
15
article.save();
16
}
17
18
db.commitWrites();
19
processed += batch.size();
20
System.out.println("Processed " + processed + " / " + dataList.size());
21
22
} catch (Exception e) {
23
System.err.println("Batch failed at index " + i);
24
throw e;
25
26
} finally {
27
db.endWrites();
28
}
29
}
30
}

Parallel Batch Processing

1
public void parallelImport(List<ArticleData> dataList) throws InterruptedException {
2
int numThreads = Runtime.getRuntime().availableProcessors();
3
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
4
int batchSize = 100;
5
6
List<Future<?>> futures = new ArrayList<>();
7
8
for (int i = 0; i < dataList.size(); i += batchSize) {
9
int start = i;
10
int end = Math.min(i + batchSize, dataList.size());
11
12
Future<?> future = executor.submit(() -> {
13
List<ArticleData> batch = dataList.subList(start, end);
14
Database db = Database.Static.getDefault();
15
16
db.beginWrites();
17
try {
18
for (ArticleData data : batch) {
19
Article article = new Article();
20
// ... populate article
21
article.save();
22
}
23
db.commitWrites();
24
} finally {
25
db.endWrites();
26
}
27
});
28
29
futures.add(future);
30
}
31
32
// Wait for all batches to complete
33
for (Future<?> future : futures) {
34
try {
35
future.get();
36
} catch (ExecutionException e) {
37
System.err.println("Batch failed: " + e.getCause().getMessage());
38
}
39
}
40
41
executor.shutdown();
42
}

Distributed Locks

For unique constraint enforcement across distributed systems:

1
public class Article extends Record {
2
3
@Indexed
4
@Unique
5
private String slug;
6
7
@Override
8
protected void beforeSave() {
9
// DB plugin automatically acquires a distributed lock
10
// for unique fields during save
11
}
12
}

Transaction Patterns

Unit of Work Pattern

1
public class ArticleService {
2
3
public void publishArticle(UUID articleId) {
4
Database db = Database.Static.getDefault();
5
6
db.beginWrites();
7
try {
8
Article article = Query.from(Article.class)
9
.where("_id = ?", articleId)
10
.first();
11
12
if (article == null) {
13
throw new IllegalArgumentException("Article not found");
14
}
15
16
// Make multiple related changes
17
article.setPublishDate(db.now());
18
article.setStatus("published");
19
article.save();
20
21
// Update author stats
22
Author author = article.getAuthor();
23
author.incrementPublishedCount();
24
author.save();
25
26
// Create activity log
27
ActivityLog log = new ActivityLog();
28
log.setType("article_published");
29
log.setArticle(article);
30
log.setTimestamp(db.now());
31
log.save();
32
33
// All changes committed together
34
db.commitWrites();
35
36
} finally {
37
db.endWrites();
38
}
39
}
40
}

Saga Pattern (Compensating Transactions)

1
public class OrderService {
2
3
public void placeOrder(Order order) {
4
Database db = Database.Static.getDefault();
5
boolean inventoryReserved = false;
6
boolean paymentProcessed = false;
7
8
try {
9
db.beginWrites();
10
11
// Step 1: Reserve inventory
12
reserveInventory(order);
13
inventoryReserved = true;
14
15
// Step 2: Process payment
16
processPayment(order);
17
paymentProcessed = true;
18
19
// Step 3: Create order
20
order.setStatus("confirmed");
21
order.save();
22
23
db.commitWrites();
24
25
} catch (Exception e) {
26
// Compensating actions
27
if (paymentProcessed) {
28
refundPayment(order);
29
}
30
if (inventoryReserved) {
31
releaseInventory(order);
32
}
33
throw e;
34
35
} finally {
36
db.endWrites();
37
}
38
}
39
}

Optimistic Locking

1
public class Article extends Record {
2
3
@Indexed
4
private int version;
5
6
public void updateWithOptimisticLock(String newTitle) {
7
Database db = Database.Static.getDefault();
8
9
db.beginWrites();
10
try {
11
// Load current version
12
Article current = Query.from(Article.class)
13
.where("_id = ?", this.getId())
14
.first();
15
16
if (current.getVersion() != this.version) {
17
throw new ConcurrentModificationException(
18
"Article was modified by another transaction"
19
);
20
}
21
22
// Update with new version
23
this.setTitle(newTitle);
24
this.setVersion(this.version + 1);
25
this.save();
26
27
db.commitWrites();
28
29
} finally {
30
db.endWrites();
31
}
32
}
33
}

Performance Optimization

Batch vs. Individual Commits

1
// Slow - many small transactions
2
for (Article article : articles) {
3
db.beginWrites();
4
try {
5
article.save();
6
db.commitWrites();
7
} finally {
8
db.endWrites();
9
}
10
}
11
12
// Fast - one large transaction
13
db.beginWrites();
14
try {
15
for (Article article : articles) {
16
article.save();
17
}
18
db.commitWrites();
19
} finally {
20
db.endWrites();
21
}

Eventual Consistency for Bulk Operations

1
// Use eventual consistency for better performance
2
db.beginWrites();
3
try {
4
for (Article article : articles) {
5
article.save();
6
}
7
db.commitWritesEventually(); // Faster than commitWrites()
8
} finally {
9
db.endWrites();
10
}

Skip Validation for Trusted Data

1
// Import from trusted source - skip validation
2
db.beginWrites();
3
try {
4
for (ArticleData data : trustedData) {
5
Article article = convertToArticle(data);
6
db.saveUnsafely(article.getState()); // No validation
7
}
8
db.commitWrites();
9
} finally {
10
db.endWrites();
11
}

Best Practices

1. Always Use try-finally

1
// Good
2
db.beginWrites();
3
try {
4
article.save();
5
db.commitWrites();
6
} finally {
7
db.endWrites();
8
}
9
10
// Bad - endWrites() might not be called
11
db.beginWrites();
12
article.save();
13
db.commitWrites();
14
db.endWrites();

2. Keep Transactions Short

1
// Good - short transaction
2
db.beginWrites();
3
try {
4
article.save();
5
db.commitWrites();
6
} finally {
7
db.endWrites();
8
}
9
10
// Bad - long transaction holding locks
11
db.beginWrites();
12
try {
13
article.save();
14
sendEmail(article); // External I/O
15
updateSearchIndex(article); // Slow operation
16
db.commitWrites();
17
} finally {
18
db.endWrites();
19
}

3. Use Appropriate Commit Strategy

1
// Critical data - immediate consistency
2
db.beginWrites();
3
try {
4
payment.save();
5
db.commitWrites(); // Must be immediately visible
6
} finally {
7
db.endWrites();
8
}
9
10
// Non-critical data - eventual consistency
11
db.beginWrites();
12
try {
13
viewCount.save();
14
db.commitWritesEventually(); // Performance > consistency
15
} finally {
16
db.endWrites();
17
}

4. Don't Catch and Ignore Exceptions

1
// Bad - hides errors
2
db.beginWrites();
3
try {
4
article.save();
5
db.commitWrites();
6
} catch (Exception e) {
7
// Ignored!
8
} finally {
9
db.endWrites();
10
}
11
12
// Good - handle or propagate
13
db.beginWrites();
14
try {
15
article.save();
16
db.commitWrites();
17
} catch (ValidationException e) {
18
logger.error("Validation failed: {}", e.getMessage());
19
throw e;
20
} finally {
21
db.endWrites();
22
}