How do I handle cross-cutting concerns in EK9?

← Design Patterns and Idioms · Ref: Q266

EK9 handles cross-cutting concerns using the decorator pattern via trait delegation. Each decorator wraps a service, adds behavior before or after, then delegates to the wrapped service.

SERVICE TRAIT

Define the service contract as a trait:

  Service
    execute() as abstract
      -> request as String
      <- rtn as String?

CORE IMPLEMENTATION

The base service handles the actual work:

  CoreService with trait of Service
    override execute()
      -> request as String
      <- rtn as String: "Processed: " + request

DECORATOR VIA DELEGATION

Use trait delegation with by to create decorators:

  LoggingService with trait of Service by delegate
    delegate as Service

Override execute() to add logging before and after delegation.

STACKING DECORATORS

Wrap decorators around each other:

  core <- CoreService()
  logged <- LoggingService(core)
  validated <- ValidatingService(logged)

Calling validated.execute() runs: validation then logging then core.

ORDER MATTERS

The outermost decorator runs first. Stack in the order you want execution:

  ValidatingService(LoggingService(CoreService()))

This validates first, then logs, then processes.

VS AOP FRAMEWORKS

No separate AOP framework needed. Trait delegation with selective override provides the same cross-cutting capability with explicit, debuggable code.

See Q106 for trait basics. See Q115 for dynamic classes as lightweight decorators. See Q210 for trait delegation with by. See Q213 for mutex lock as a cross-cutting thread safety pattern.

See Q337 for transaction aspects. See Q334 for transaction delegation.

Example

defines module qa.patterns.crosscutting

  defines trait

    Service
      execute() as abstract
        -> request as String
        <- rtn as String?

  defines class

    CoreService with trait of Service

      override execute()
        -> request as String
        <- rtn as String: "Processed: " + request

      default operator ?

    // Decorator: adds logging around the delegated call
    LoggingService with trait of Service by delegate
      delegate as Service: CoreService()

      LoggingService()
        -> svc as Service
        this.delegate: svc

      override execute()
        -> request as String
        <- rtn as String?
        Stdout().println("[LOG] Before: " + request)
        rtn: delegate.execute(request)
        Stdout().println("[LOG] After: " + $rtn)

      default operator ?

    // Decorator: adds validation before the delegated call
    ValidatingService with trait of Service by delegate
      delegate as Service: CoreService()

      ValidatingService()
        -> svc as Service
        this.delegate: svc

      override execute()
        -> request as String
        <- rtn as String: String()
        if request?
          rtn: delegate.execute(request)

      default operator ?

  defines program

    CrossCuttingDemo()
      stdout <- Stdout()

      // === CORE SERVICE ALONE ===

      core <- CoreService()
      stdout.println(core.execute("plain"))

      // === SINGLE DECORATOR ===

      logged <- LoggingService(core)
      stdout.println(logged.execute("with-logging"))

      // === STACKED DECORATORS ===
      // Validation runs first, then logging, then core

      stacked <- ValidatingService(LoggingService(CoreService()))
      stdout.println(stacked.execute("fully-decorated"))

      stdout.println("Cross-cutting via decorator stacking")

Common mistakes

E05120 — When overriding a trait method in a delegating class, the 'override' keyword is required. LoggingService and ValidatingService must use 'override execute()' to customize the delegated Service method. See ek9 -h E05120 for details.

Incorrect:

execute()

Correct:

override execute()

E08180 — Class fields must be initialized inline with a default value. Declaring a field without an initializer triggers E08180. Provide a default like 'CoreService()' even if a constructor will override it. See ek9 -h E08180 for details.

Incorrect:

delegate as Service

Correct:

delegate as Service: CoreService()
Other ways to ask this
  • How do I add logging without modifying existing code?
  • How do I implement the decorator pattern in EK9?
  • How do I stack multiple behaviors in EK9?

Coming from another language?

Java: AOP frameworks (Spring AOP, AspectJ), proxy-based decorators, annotation-driven aspects. Python: decorators (@decorator syntax), metaclasses, monkey patching. Rust: no built-in AOP, middleware pattern, tower crate for service layers. Go: middleware functions, handler wrapping. Kotlin: delegated properties, class delegation with by. EK9: trait delegation with by for decorator stacking, selective override for cross-cutting behavior, no AOP framework needed.

Keywords: advice, cross-cutting, middleware, delegate, logging, concern, design, idiom, pattern, wrap, stack, aspect, validation, decorator, weave