How do I use higher-order functions for transaction management?
← Dependency Injection · Ref: Q335
Pass business logic as a function to a transaction wrapper. This creates a reusable transaction execution framework inspired by Rust/Diesel's closure pattern.
CALLBACK PATTERN
Define a function that accepts a business operation and wraps it in a transaction:
executeInTransaction()
-> operation as BusinessOperation
<- result as String?
try
-> txn <- Transaction()
result: operation("context")
txn.commit()
catch
-> ex as Exception
result: "Failed"
HOW IT WORKS
1. The transaction wrapper creates and manages the transaction
2. Business logic is passed as a function (Consumer, Function, etc.)
3. The wrapper calls the business logic within the transaction scope
4. Commit happens after successful execution, rollback on exception
ADVANTAGES
1. REUSABLE: one transaction wrapper serves all business operations
2. CONSISTENT: transaction management logic is centralized
3. TESTABLE: test business logic independently of transaction management
4. EXPLICIT: transaction scope is visible at the call site
SIMILAR TO RUST DIESEL
Rust's conn.transaction(|txn| { ... }) pattern. EK9 achieves the same with higher-order functions.
See Q56 for higher-order functions. See Q52 for dynamic functions. See Q333 for try-with-resources pattern. See Q336 for closure capture pattern.
Example
defines module qa.di.transaction.callback defines class SimpleTransaction with trait of Transaction label <- String() committed <- false SimpleTransaction() -> label as String this.label: label override commit() committed: true override rollback() committed: false override isCommitted() as pure <- rtn as Boolean: committed override operator close as pure stdout <- Stdout() if committed stdout.println($label + ": committed on close") else stdout.println($label + ": rolled back on close") override operator ? as pure <- rtn <- true defines function <?- Business operation type: accepts a label and returns a result. -?> BusinessOperation() as abstract -> context as String <- result as String? <?- Concrete operation: create user. -?> CreateUserOp() is BusinessOperation -> context as String <- result as String: "Created user in " + context <?- Concrete operation: update inventory. -?> UpdateInventoryOp() is BusinessOperation -> context as String <- result as String: "Updated inventory in " + context <?- Reusable transaction wrapper: executes any operation in a transaction. -?> executeInTransaction() -> operation as BusinessOperation <- result <- String() try -> txn <- SimpleTransaction("auto-txn") result: operation("within-transaction") txn.commit() catch -> ex as Exception result: "Transaction failed: " + $ex defines program TransactionCallbackDemo() stdout <- Stdout() // === CALLBACK PATTERN: pass business logic to wrapper === result1 <- executeInTransaction(CreateUserOp) stdout.println(result1) result2 <- executeInTransaction(UpdateInventoryOp) stdout.println(result2) stdout.println("Reusable wrapper executes any operation in a transaction")
Common mistakes
E08180 — Class fields must be initialized at declaration. Leaving a field without initialization and without the injection suffix '!' produces a field-not-initialized error. See ek9 -h E08180 for details.
Incorrect:
label as String
Correct:
label <- String()
Other ways to ask this
- Can I pass business logic to a transaction wrapper function?
- What is the callback pattern for transactions in EK9?
- How do I create a reusable transaction execution function?
Coming from another language?
Rust/Diesel: conn.transaction(|txn| { ... }) closure pattern. Kotlin/Exposed: transaction { ... } block. Go: custom withTransaction(func(tx *Tx) error) pattern. Java: Spring TransactionTemplate.execute(callback). C#: custom ExecuteInTransaction(Action) pattern. EK9: higher-order functions with dynamic function callbacks, captured closure state, try-with-resources inside wrapper.
Keywords: pattern, function, order, higher, migrate, execute, reusable, dependency, wrapper, inject, closure, consumer, callback