Which transaction management pattern should I use in EK9?

← Dependency Injection · Ref: Q339

EK9 offers five transaction patterns. Choose based on your specific requirements using this decision guide.

DEFAULT CHOICE: TRY-WITH-RESOURCES (Pattern 1)
Use for: most CRUD operations, single-function transaction scope
Why: most explicit, easiest to debug, AI generates correctly
Code: try -> txn <- Transaction() ... txn.commit()

CONTEXT PASSING (Pattern 2)
Use for: multiple functions need shared state beyond just the transaction
Why: typed context record carries transaction + audit log + correlation ID
When: call chains need rich context, not just the transaction

DELEGATION/DECORATOR (Pattern 3)
Use for: adding transactions to existing services without modification
Why: separation of concerns, composable, testable
When: layered architecture, multiple cross-cutting concerns

AOP ASPECT (Pattern 4)
Use for: truly uniform transaction behavior across many services
Caution: makes transactions invisible at call site
When: team understands AOP, behavior is identical for all advised methods

CALLBACK/HIGHER-ORDER (Pattern 5)
Use for: reusable transaction execution framework
Why: centralizes transaction management, any operation can use it
When: infrastructure code, many different operations need same wrapping

DYNAMIC FUNCTIONS (combine with any pattern)
Use for: preventing API pollution in deep call chains
Why: captures transaction in closure, keeps downstream APIs clean
When: transaction scope is wide but most functions do not need direct access

DECISION FLOWCHART

1. Is the transaction scope a single function? -> Pattern 1 (try-with-resources)
2. Do multiple functions need shared context? -> Pattern 2 (context record)
3. Are you wrapping existing services? -> Pattern 3 (delegation)
4. Is behavior truly uniform across all services? -> Pattern 4 (aspect, with caution)
5. Do you want a reusable framework? -> Pattern 5 (callback)
6. Is API pollution a concern? -> Add dynamic functions to any pattern

See Q333 for Pattern 1. See Q334 for Pattern 3. See Q335 for Pattern 5. See Q336 for dynamic functions. See Q337 for Pattern 4. See Q340 for AI-friendliness analysis.

Example

defines module qa.di.transaction.selection

  defines class

    MiniTransaction with trait of Transaction
      identifier <- String()
      committed <- false

      MiniTransaction()
        -> identifier as String
        this.identifier: identifier

      override commit()
        committed: true

      override isCommitted() as pure
        <- rtn as Boolean: committed

      override operator close as pure
        stdout <- Stdout()
        stdout.println(`${identifier} closed (committed=${committed})`)

      override operator ? as pure
        <- rtn <- true

  defines program

    PatternSelectionDemo()
      stdout <- Stdout()

      // === PATTERN 1: Try-with-resources (DEFAULT CHOICE) ===

      stdout.println("=== Pattern 1: Try-with-resources ===")
      try
        -> txn <- MiniTransaction("P1")
        stdout.println("Business logic here")
        txn.commit()
      catch
        -> ex as Exception
        stdout.println("Error: " + $ex)

      // Most use cases are covered by Pattern 1 above
      // Only use other patterns when Pattern 1 doesn't fit

      stdout.println("Default: try-with-resources")
      stdout.println("Context passing: when multiple functions need shared state")
      stdout.println("Delegation: when wrapping existing services")
      stdout.println("Aspect: when behavior is truly uniform (use with caution)")
      stdout.println("Callback: when building reusable transaction framework")

Common mistakes

E08180 — Class fields must be initialized at declaration. The MiniTransaction class initializes all its fields inline. Omitting initialization without the '!' injection suffix is a compile-time error. See ek9 -h E08180 for details.

Incorrect:

identifier as String

Correct:

identifier <- String()
Other ways to ask this
  • How do I choose between EK9 transaction patterns?
  • What is the best transaction pattern for my use case?
  • When should I use try-with-resources vs delegation vs aspects for transactions?

Coming from another language?

Spring: primarily @Transactional (AOP), TransactionTemplate (callback). Go: explicit transaction passing (context pattern). Rust: Diesel closure pattern (callback). C#: TransactionScope (ambient), middleware (delegation). Kotlin: transaction { } block (callback). EK9: five patterns from most-explicit to most-implicit, try-with-resources recommended as default, dynamic functions solve API pollution uniquely.

Keywords: inject, choose, guide, decision, selection, when, comparison, flowchart, dependency, recommendation, pattern, use