How does EK9 prevent circular dependencies in DI?

← DI Validation · Ref: Q671

EK9 detects circular dependencies at compile time (E08190). When Component A injects Component B, and Component B injects Component A (directly or transitively), the compiler rejects the code.

WHY CIRCULAR IS FORBIDDEN

Circular dependencies make object creation impossible. To create A, you need B. To create B, you need A. This deadlock is detected before runtime.

BREAKING CYCLES

1. Introduce an intermediary:

   A -> C <- B (C mediates between A and B)

2. Use event-based communication
3. Restructure to remove the cycle

CORRECT HIERARCHICAL PATTERN

  Repository -> (no dependencies)
  Service -> Repository
  Controller -> Service

Dependencies flow in ONE direction.

See Q667 for abstract injection. See Q670 for field initialization. See Q673 for missing registration.

Example

defines module qa.divalidation.circulardep

  defines component

    <?-
      Abstract storage contract.
      Bottom of the dependency hierarchy (no injections).
    -?>
    Storage as abstract

      store() as abstract
        -> payload as String
        <- stored as Boolean?

      default operator ?

    <?-
      Concrete storage implementation.
    -?>
    FileStorage extends Storage

      override store()
        -> payload as String
        <- stored as Boolean: true

      default operator ?

    <?-
      Abstract processing contract.
      Depends on Storage (one direction).
    -?>
    Processor as abstract

      processItem() as abstract
        -> itemData as String
        <- result as String?

      default operator ?

    <?-
      Concrete processor that depends on storage.
      Dependencies flow: Processor -> Storage (no cycle).
    -?>
    ItemProcessor extends Processor

      storage as Storage!

      override processItem()
        -> itemData as String
        <- result as String: ""

        storedOk <- storage.store(itemData)
        if storedOk?
          result: "Processed: " + itemData

      default operator ?

  defines application

    HierarchicalApp
      register FileStorage() as Storage
      register ItemProcessor() as Processor

  defines program

    CircularDependencyDemo() with application of HierarchicalApp
      stdout <- Stdout()

      processor as Processor!
      result <- processor.processItem("test-data")
      stdout.println(result)

      stdout.println("Dependencies flow one direction: Processor -> Storage")
      stdout.println("No circular dependency possible")

Common mistakes

E08190 — Adding an injection of Processor into FileStorage creates a circular dependency: ItemProcessor depends on Storage (via FileStorage), and FileStorage now depends on Processor. Neither can be constructed first. See ek9 -h E08190 for details.

Incorrect:

    FileStorage extends Storage

      processor as Processor!

      override store()
        -> payload as String
        <- stored as Boolean: true

      default operator ?

Correct:

    FileStorage extends Storage

      override store()
        -> payload as String
        <- stored as Boolean: true

      default operator ?
Other ways to ask this
  • What is E08190 circular dependency detected?
  • How do I break a dependency cycle between components?
  • What happens when two components inject each other?

Coming from another language?

Java: Spring detects circular dependencies at runtime (throws exception). Python: no detection. Go: manual wiring avoids cycles. Rust: ownership prevents cycles. EK9: compiler detects cycles at compile time, before any code runs.

Keywords: E08190, component, inject, DI, dependency, cycle, registration, validation, circular, validate