Can I use aspects for transactions like Spring @Transactional?

← Dependency Injection · Ref: Q337

Yes, EK9 supports AOP-based transaction management through its aspect system. However, this pattern should be used with caution because it makes transaction boundaries invisible at the call site.

ASPECT-BASED TRANSACTIONS

Define a TransactionAspect that extends Aspect:

  TransactionAspect extends Aspect
    override beforeAdvice()
      -> joinPoint as JoinPoint
      <- rtn as PreparedMetaData: PreparedMetaData(joinPoint)
    override afterAdvice()
      -> preparedMetaData as PreparedMetaData

Register with: register Service() as ServiceType with aspect of TransactionAspect()

HOW IT WORKS

1. beforeAdvice() runs before the advised method (start transaction)
2. The advised method executes within the transaction context
3. afterAdvice() runs after the advised method (commit or rollback)
4. The aspect is registered in the application definition

CAUTION: INVISIBLE BEHAVIOUR

This pattern has the same problem as Spring @Transactional: the transaction is invisible at the call site. A developer reading service.process(order) cannot see that a transaction exists. This is acceptable for truly uniform cross-cutting concerns but dangerous for business-critical transaction logic.

WHEN TO USE ASPECTS

1. ALL methods on a service need identical transaction behavior
2. Transaction logic is truly uniform (no method-specific rollback rules)
3. The team understands that transactions are managed by the aspect
4. Auditing, logging, or metrics that are purely cross-cutting

WHEN TO PREFER EXPLICIT PATTERNS

1. Different methods need different transaction strategies
2. Transaction scope doesn't align with method boundaries
3. AI is generating the code (AI cannot see aspect behavior)
4. Debugging transaction issues is a concern

See Q114 for aspect basics. See Q266 for cross-cutting concerns. See Q333 for try-with-resources (recommended). See Q334 for delegation pattern.

Example

defines module qa.di.transaction.aspect

  defines component

    PaymentService as abstract
      processPayment() as abstract
        -> amount as String
        <- receipt as String?

      default operator ?

    BasicPaymentService is PaymentService
      override processPayment()
        -> amount as String
        <- receipt as String: "Payment processed: " + amount

      default operator ?

  defines class

    <?-
      Transaction aspect: wraps advised methods with transaction behavior.
    -?>
    TransactionAspect extends Aspect
      label <- String()

      TransactionAspect()
        -> label as String
        this.label: label

      override beforeAdvice()
        -> joinPoint as JoinPoint
        <- rtn as PreparedMetaData: PreparedMetaData(joinPoint)
        Stdout().println(`${label} BEGIN: ${joinPoint.componentName()}.${joinPoint.methodName()}`)

      override afterAdvice()
        -> preparedMetaData as PreparedMetaData
        joinPoint <- preparedMetaData.joinPoint()
        Stdout().println(`${label} COMMIT: ${joinPoint.componentName()}.${joinPoint.methodName()}`)

      default operator ?

  defines application

    TransactionalAspectApp
      register BasicPaymentService() as PaymentService with aspect of TransactionAspect("TXN")

  defines program

    TransactionAspectDemo() with application of TransactionalAspectApp
      stdout <- Stdout()

      // === AOP: transaction is invisible at this call site ===

      payment as PaymentService!

      receipt <- payment.processPayment("99.95")
      stdout.println(receipt)

      stdout.println("Aspect adds transaction, but behavior is invisible here")
      stdout.println("Prefer explicit patterns for critical business logic")

Common mistakes

E08150 — Injection fields must use abstract component types. The aspect is applied through the application registration, and the program injects the abstract type. See ek9 -h E08150 for details.

Incorrect:

payment as BasicPaymentService!

Correct:

payment as PaymentService!

E08180 — Class fields must be initialized at declaration. Without initialization and without the '!' injection suffix, the compiler reports a field-not-initialized error. See ek9 -h E08180 for details.

Incorrect:

label as String

Correct:

label <- String()
Other ways to ask this
  • Does EK9 support AOP-based transaction management?
  • How do I use aspects for cross-cutting transaction concerns?
  • What is the AOP approach to transactions in EK9?

Coming from another language?

Java Spring: @Transactional annotation, proxy-based AOP, seven silent failure modes. AspectJ: compile-time weaving for more reliable AOP. Go: no AOP support, explicit middleware only. Rust: no AOP support. C#: Castle DynamicProxy or middleware. EK9: Aspect class with beforeAdvice/afterAdvice, 'with aspect of' registration, compile-time wiring, but invisible behavior at call site.

Keywords: cross, cutting, before, cross-cutting, invisible, advice, caution, weave, transactional, aspect, inject, after, aop, dependency, migrate