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()
    -> txnLabel as String
    <- processor as OrderProcessor?
    processor: (txnLabel) is OrderProcessor as function
      if validateOrder(orderId)
        result: `${txnLabel} | ${formatConfirmation(orderId)}`
      else
        result: `${txnLabel} | validation failed`

The capture list (txnLabel) is what makes the factory's parameter visible inside the dynamic function; an empty list () captures nothing. Downstream functions (validateOrder, formatConfirmation) 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

    <?-
      The factory: the dynamic function captures txnLabel in its closure, so the
      processor it returns carries the transaction context with it.
    -?>
    createProcessor()
      -> txnLabel as String
      <- processor as OrderProcessor?
      processor: (txnLabel) is OrderProcessor as function
        if validateOrder(orderId)
          result: `${txnLabel} | ${formatConfirmation(orderId)}`
        else
          result: `${txnLabel} | validation failed`

  defines program

    ClosureCaptureDemo()
      stdout <- Stdout()

      // === CLOSURE CAPTURE ===
      // The factory captures the transaction label once. Everything downstream
      // is called without a transaction parameter.

      process <- createProcessor("TXN-001")

      result1 <- process("ORD-100")
      stdout.println(result1)

      result2 <- process("ORD-200")
      stdout.println(result2)

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

Common mistakes

E50001 — Calling a name that was never declared in the program (processor instead of process) is not resolved, triggering E50001. See ek9 -h E50001 for details.

Incorrect:

result1 <- processor("ORD-100")

Correct:

result1 <- process("ORD-100")

E50001 — A misspelt factory name (createProcesser) is not resolved, triggering E50001. See ek9 -h E50001 for details.

Incorrect:

process <- createProcesser("TXN-001")

Correct:

process <- createProcessor("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: api, migrate, dependency, anonymous, dynamic, closure, clean, enclosing, scope, capture, function, inject, factory, pollution