How does EK9 validate dependency injection at compile time?

← Dependency Injection · Ref: Q227

EK9 validates ALL dependency injection at compile time. If your program compiles, every injection point is guaranteed to be satisfied. There are zero runtime DI failures.

FOUR COMPILE-TIME VALIDATIONS

The compiler performs four checks on every application definition:
1. Completeness: Every injection field ('!' suffix) must have a matching registration
2. Ordering: Dependencies must be registered before dependents
3. Cycle detection: Circular dependencies are rejected with error E08190
4. Field count: Components with excessive injection fields are flagged with E11040

CONTRAST WITH SPRING/GUICE

In Java Spring, a missing @Bean causes NoSuchBeanDefinitionException at runtime, sometimes minutes into startup. In Guice, a missing binding produces CreationException. In EK9, you find out during compilation, before any code runs.

WHY THIS MATTERS

Runtime DI failures are among the most frustrating bugs. They appear only when a specific code path is exercised, often in production. EK9 eliminates this entire category of defect by moving validation to compile time.

WORKING EXAMPLE

The code below demonstrates a correctly wired application. The compiler has verified that Logger is registered before Formatter, both are registered before MessageService (which injects them), and the program's injection point is satisfied.

See Q111 for component basics. See Q117 for singleton lifecycle. See Q228 for registration ordering. See Q229 for circular dependency detection. See Q230 for missing registration errors. See Q310 for compile-time quality philosophy.

See Q324 for @Autowired equivalent. See Q332 for runtime overhead comparison.

See Q666 for injection pure context. See Q673 for missing registration. See Q675 for transitive validation.

Example

defines module qa.di.compile.time

  defines component

    Logger as abstract
      log() as abstract
        -> message as String
        <- output as String?

      default operator ?

    ConsoleLogger is Logger
      override log()
        -> message as String
        <- output as String: "LOG: " + message

      default operator ?

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

      default operator ?

    UpperFormatter is Formatter
      override format()
        -> text as String
        <- result as String: text.upperCase()

      default operator ?

    MessageService as abstract
      greet() as abstract
        -> name as String
        <- greeting as String?

      default operator ?

    SimpleMessageService is MessageService
      logger as Logger!
      formatter as Formatter!

      override greet()
        -> name as String
        <- greeting <- String()
        formatted <- formatter.format("Hello " + name)
        greeting: logger.log(formatted)

      default operator ?

  defines application

    ValidatedApp
      register ConsoleLogger() as Logger
      register UpperFormatter() as Formatter
      register SimpleMessageService() as MessageService

  defines program

    CompileTimeDiDemo() with application of ValidatedApp
      stdout <- Stdout()

      // === ALL INJECTION VALIDATED AT COMPILE TIME ===

      service as MessageService!

      result <- service.greet("World")
      stdout.println(result)

      stdout.println("All injections verified by compiler")

Common mistakes

E08150 — Injection fields must use abstract types, not concrete implementations. Only abstract components can be injection targets. See ek9 -h E08150 for details.

Incorrect:

logger as ConsoleLogger!

Correct:

logger as Logger!

E50060 — String has no toUpperCase() method in EK9. Use upperCase() instead. See ek9 -h E50060 for details.

Incorrect:

result <- service.greet("World").toUpperCase()

Correct:

result <- service.greet("World")

E08210 — If the register statement for Logger were removed, the injection field 'logger as Logger!' in SimpleMessageService would have no matching registration and the compiler would reject it. See ek9 -h E08210 for details.

Incorrect:

register UpperFormatter() as Formatter

Correct:

register ConsoleLogger() as Logger
Other ways to ask this
  • Does EK9 catch DI errors before runtime?
  • How does EK9 prevent missing dependency errors at compile time?
  • What DI validations does the EK9 compiler perform?
  • How does inversion of control work in EK9?

Coming from another language?

Java Spring: NoSuchBeanDefinitionException at runtime, @Conditional can hide missing beans, circular dependency detection only at startup. Guice: CreationException at Injector creation, JIT bindings can mask issues. .NET: InvalidOperationException during service resolution, runtime-only. Python: no compile-time DI validation at all. Go: no built-in DI. Rust: no built-in DI. EK9: ALL DI validated at compile time, zero runtime failures possible.

Keywords: migrate, inject, injection, circular, inversion, guarantee, ioc, compile, safety, completeness, validate, ordering