How does EK9 detect circular component injection at compile time?

← DI Validation · Ref: Q797

EK9 detects circular dependency chains in component injection at compile time (E08190). When Component A depends on Component B, and Component B depends on Component A (directly or transitively), neither can be constructed first.

CIRCULAR CHAIN

  ComponentA -> needs AbstractB -> provided by ComponentB
  ComponentB -> needs AbstractA -> provided by ComponentA
  DEADLOCK: neither can be created first

COMPILE-TIME DETECTION

Unlike Spring (runtime BeanCurrentlyInCreationException), EK9 catches circular dependencies before any code runs. The compiler analyses the full injection graph and rejects cycles.

BREAKING CYCLES

1. One-way dependency: only one component injects the other
2. Extract shared logic into a third component
3. Use an event or callback pattern instead of direct injection

See Q671 for the general circular dependency explanation. See Q673 for missing registration. See Q674 for duplicate registration.

Example

defines module qa.divalidation.circularcompiletime

  defines component

    Handler as abstract

      handleEvent() as abstract
        -> eventName as String
        <- handled as Boolean?

      default operator ?

    NotificationHandler extends Handler

      override handleEvent()
        -> eventName as String
        <- handled as Boolean: true

      default operator ?

    Dispatcher as abstract

      dispatch() as abstract
        -> eventName as String
        <- dispatched as Boolean?

      default operator ?

    EventDispatcher extends Dispatcher

      handler as Handler!

      override dispatch()
        -> eventName as String
        <- dispatched as Boolean?

        dispatched: handler.handleEvent(eventName)

      default operator ?

  defines application

    OneWayApp
      register NotificationHandler() as Handler
      register EventDispatcher() as Dispatcher

  defines program

    CircularDiDemo() with application of OneWayApp
      stdout <- Stdout()

      dispatcher as Dispatcher!
      sent <- dispatcher.dispatch("user-login")
      stdout.println(`Dispatched: ${sent}`)

Common mistakes

E08190 — Injecting Dispatcher into NotificationHandler creates a circular dependency (Handler -> Dispatcher -> Handler) that the compiler rejects at compile time. See ek9 -h E08190 for details.

Incorrect:

    NotificationHandler extends Handler

      dispatcher as Dispatcher!

      override handleEvent()

Correct:

    NotificationHandler extends Handler

      override handleEvent()
Other ways to ask this
  • What triggers E08190 DI circular dependency?
  • Why does mutual component injection fail to compile?
  • How do I break a circular dependency chain in EK9 DI?

Coming from another language?

Java: Spring throws BeanCurrentlyInCreationException at runtime. Python: no DI framework detection. Go: Wire detects at code generation time. Rust: no standard DI. Kotlin: Koin throws at runtime. EK9: compile-time E08190 rejects circular chains before deployment.

Keywords: injection, cycle, deadlock, DI, circular, compile-time, dependency, component, E08190