How do dynamic functions solve transaction API pollution?

← Dependency Injection · Ref: Q336

Dynamic functions capture transaction state in closures, keeping downstream APIs clean. This solves Go's #1 complaint about explicit transaction management.

THE PROBLEM: API POLLUTION

In Go, if processOrder() needs a transaction, and it calls validateInventory(), which calls checkWarehouse(), ALL three functions must accept a transaction parameter. This creates massive API surface bloat.

EK9'S SOLUTION: CLOSURE CAPTURE

Create a dynamic function that captures the transaction:

  createProcessor()
    -> txn as Transaction
    <- processor as Processor
    processor: () is Processor as function ...

Downstream functions (doWork, etc.) have clean APIs with no transaction parameter.

HOW IT WORKS

1. Factory function receives the transaction context
2. Dynamic function captures the context in its closure
3. Downstream functions are called without transaction parameters
4. Transaction is managed by the factory caller, not the interior functions

WHY THIS IS TRANSFORMATIVE

1. EXPLICIT AT THE BOUNDARY: the factory clearly shows transaction capture
2. CLEAN INTERIOR: downstream APIs are unpolluted by transaction plumbing
3. TYPE SAFE: the captured state is typed, not a context value bag
4. TESTABLE: downstream functions can be tested without any transaction setup

COMBINES GO AND RUST ADVANTAGES

Go's explicitness (transaction is a real value) plus Rust's closure elegance (captured state is invisible to callers).

See Q53 for closure capture basics. See Q52 for dynamic functions. See Q333 for try-with-resources. See Q335 for callback pattern.

Example

defines module qa.di.transaction.closure

  defines function

    <?-
      Abstract processor with clean API: no transaction parameter.
    -?>
    OrderProcessor() as abstract
      -> orderId as String
      <- result as String?

    <?-
      Downstream function: no transaction parameter needed.
    -?>
    validateOrder()
      -> orderId as String
      <- isValid as Boolean: orderId?

    <?-
      Another downstream function: clean API.
    -?>
    formatConfirmation()
      -> orderId as String
      <- confirmation as String: "Confirmed: " + orderId

    <?-
      Downstream: builds final result with captured label.
    -?>
    buildResult()
      ->
        txnLabel as String
        orderId as String
      <- result <- String()
      valid <- validateOrder(orderId)
      if valid
        confirmation <- formatConfirmation(orderId)
        result: `${txnLabel} | ${confirmation}`
      else
        result: `${txnLabel} | validation failed`

  defines program

    ClosureCaptureDemo()
      stdout <- Stdout()

      // === CLOSURE CAPTURE CONCEPT ===
      // In full EK9, a factory function would return a dynamic function
      // that captures txnLabel in its closure.
      // Here we demonstrate the same concept with explicit function calls.

      txnLabel <- "TXN-001"

      // Downstream functions have clean APIs - no transaction parameter
      result1 <- buildResult(txnLabel, "ORD-100")
      stdout.println(result1)

      result2 <- buildResult(txnLabel, "ORD-200")
      stdout.println(result2)

      stdout.println("Transaction context at boundary, clean APIs inside")

Common mistakes

E50001 — Renaming the variable means later references to 'result1' become unresolved, triggering E50001. See ek9 -h E50001 for details.

Incorrect:

result1XYZ <- buildResult(txnLabel, "ORD-100")

Correct:

result1 <- buildResult(txnLabel, "ORD-100")

E50001 — Renaming the variable means later references to 'txnLabel' become unresolved, triggering E50001. See ek9 -h E50001 for details.

Incorrect:

txnLabelXYZ <- "TXN-001"

Correct:

txnLabel <- "TXN-001"
Other ways to ask this
  • How do I avoid passing transaction through every function?
  • What is the closure capture pattern for transactions?
  • How do dynamic functions prevent API pollution in EK9?

Coming from another language?

Go: context.Context + tx parameter pollution through every function in the chain. Rust: closures capture state naturally. Java: ThreadLocal for ambient state (fragile). C#: TransactionScope ambient (fragile). Python: contextvars for ambient state. EK9: dynamic functions capture transaction in closure, downstream APIs stay clean, explicit at boundary, invisible in interior.

Keywords: scope, enclosing, clean, closure, dynamic, anonymous, dependency, migrate, api, pollution, factory, inject, function, capture