How does EK9 prevent circular dependencies?

← Dependency Injection · Ref: Q229

EK9 detects circular dependencies at compile time using depth-first search on the dependency graph. If component A depends on B and B depends on A, the compiler reports error E08190 and refuses to compile.

WHAT E08190 CATCHES

The compiler traces every injection chain. If it finds a cycle of any length (A->B->A, or A->B->C->A, etc.), it reports E08190 with the cycle path so you know exactly which components are involved.

FIXING CIRCULAR DEPENDENCIES

The solution is always the same: break the cycle by extracting the shared responsibility into a separate component. If A and B both need each other, extract the shared logic into C, then have both A and B depend on C.

BEFORE (circular, would not compile):

  ServiceA injects ServiceB
  ServiceB injects ServiceA

AFTER (refactored, compiles correctly):

  SharedLogic has no injections
  ServiceA injects SharedLogic
  ServiceB injects SharedLogic

CONTRAST WITH SPRING

Spring tries to resolve circular dependencies with proxy objects and lazy initialization, which leads to subtle bugs, partial initialization, and startup order sensitivity. EK9 simply rejects cycles.

DESIGN PRINCIPLE

Circular dependencies indicate a design flaw. By rejecting them at compile time, EK9 forces clean layered architecture from the start.

See Q228 for registration ordering. See Q227 for compile-time validation overview. See Q232 for clean multi-layer dependency chains.

See Q329 for Guice comparison.

Example

defines module qa.di.circular

  defines component

    // === CORRECT PATTERN: Shared logic extracted to break potential cycle ===

    SharedFormatter as abstract
      format() as abstract
        -> text as String
        <- result as String?

      default operator ?

    PlainFormatter is SharedFormatter
      override format()
        -> text as String
        <- result as String: `[${text}]`

      default operator ?

    UserService as abstract
      getUser() as abstract
        -> id as Integer
        <- name as String?

      default operator ?

    SimpleUserService is UserService
      formatter as SharedFormatter!

      override getUser()
        -> id as Integer
        <- name <- String()
        name: formatter.format("User-" + $id)

      default operator ?

    OrderService as abstract
      getOrder() as abstract
        -> id as Integer
        <- details as String?

      default operator ?

    SimpleOrderService is OrderService
      formatter as SharedFormatter!

      override getOrder()
        -> id as Integer
        <- details <- String()
        details: formatter.format("Order-" + $id)

      default operator ?

  defines application

    CleanApp
      // SharedFormatter registered first (no dependencies)
      // Both services depend on formatter, NOT on each other
      register PlainFormatter() as SharedFormatter
      register SimpleUserService() as UserService
      register SimpleOrderService() as OrderService

  defines program

    CircularDepsDemo() with application of CleanApp
      stdout <- Stdout()

      // === CLEAN DESIGN: no circular dependencies ===

      users as UserService!
      orders as OrderService!

      stdout.println(users.getUser(42))
      stdout.println(orders.getOrder(99))

      stdout.println("Both services share formatter without circular dependency")

Common mistakes

E50001 — If SimpleUserService injected OrderService and SimpleOrderService injected UserService, it would create a circular dependency cycle (A->B->A) that the compiler detects and rejects. The correct pattern extracts shared logic into a separate component. See ek9 -h E50001 for details.

Incorrect:

orderService as OrderService!

Correct:

formatter as SharedFormatter!

E08150 — Injection fields must use abstract component types. Injecting the concrete PlainFormatter directly violates the abstract injection requirement. See ek9 -h E08150 for details.

Incorrect:

formatter as PlainFormatter!

Correct:

formatter as SharedFormatter!
Other ways to ask this
  • What happens with circular DI in EK9?
  • Does EK9 detect dependency cycles at compile time?
  • How do I fix circular dependencies in EK9?

Coming from another language?

Java Spring: circular dependencies resolved via proxies and @Lazy, deprecated since Spring 6, often indicates design flaw. Guice: circular dependency causes ProvisionException, except with Provider indirection. .NET: circular DI causes InvalidOperationException at runtime. Python: circular imports cause ImportError. Go: no built-in DI, circular package imports are compile errors. Rust: no built-in DI. EK9: E08190 compile-time error, DFS cycle detection, forces clean architecture.

Keywords: detect, inject, cycle, circular, break, design, E08190, dependency, compile, refactor, graph, migrate