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