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