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