What is the lifecycle of injected components in EK9?

← Dependency Injection · Ref: Q234

EK9 components follow a simple three-phase lifecycle: prepare, execute, decommission. There is only one scope: singleton for the program's lifetime.

PREPARE PHASE

When a program starts, the application's _prepare() method creates all registered components in registration order. Dependencies are created before dependents, so injection fields are satisfied as each component is initialized.

EXECUTE PHASE

The program body runs with all components fully initialized. Every injection point references the same instance throughout the program's execution.

DECOMMISSION PHASE

When the program completes, _decommission() releases components in REVERSE registration order. Dependents are released before their dependencies, ensuring no component uses a decommissioned dependency.

SINGLETON ONLY

EK9 has one scope: singleton for the program lifetime. There are no request, session, or prototype scopes. This simplicity eliminates an entire class of scope-related bugs.

CONTRAST WITH SPRING

Spring has singleton, prototype, request, session, and application scopes. Each scope adds complexity and potential for scope mismatch bugs (e.g., injecting a request-scoped bean into a singleton). EK9 avoids this entirely.

WHY SINGLETON ONLY

Most DI complexity comes from managing multiple scopes. Programs in EK9 are designed to be focused units. If you need different lifecycle management, use explicit construction within the program body.

See Q117 for singleton lifecycle. See Q111 for component basics. See Q227 for compile-time validation. See Q231 for program-application linking.

See Q330 for singleton-only scope rationale. See Q332 for performance comparison.

Example

defines module qa.di.lifecycle

  defines component

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

      default operator ?

    AppLogger is Logger
      override log()
        -> message as String
        <- output as String: `[${message}]`

      default operator ?

    DataStore as abstract
      save() as abstract
        -> content as String
        <- result as String?

      default operator ?

    InMemoryStore is DataStore
      logger as Logger!

      override save()
        -> content as String
        <- result <- String()
        result: logger.log("Saved: " + content)

      default operator ?

    AppService as abstract
      run() as abstract
        -> input as String
        <- output as String?

      default operator ?

    MainService is AppService
      store as DataStore!
      logger as Logger!

      override run()
        -> input as String
        <- output <- String()
        saved <- store.save(input)
        output: logger.log("Service processed: " + saved)

      default operator ?

  defines application

    LifecycleApp
      // _prepare() creates in this order:
      //   1. AppLogger (no dependencies)
      //   2. InMemoryStore (depends on Logger)
      //   3. MainService (depends on DataStore and Logger)
      register AppLogger() as Logger
      register InMemoryStore() as DataStore
      register MainService() as AppService
      // _decommission() releases in reverse:
      //   1. MainService
      //   2. InMemoryStore
      //   3. AppLogger

  defines program

    LifecycleDemo() with application of LifecycleApp
      stdout <- Stdout()

      // === EXECUTE PHASE: all components fully initialized ===

      service as AppService!

      result <- service.run("test data")
      stdout.println(result)

      stdout.println("Components created in order, released in reverse")

Common mistakes

E08200 — MainService depends on both DataStore and Logger, so both must be registered before MainService. The prepare phase creates components in registration order, so dependencies must come first. See ek9 -h E08200 for details.

Incorrect:

register MainService() as AppService
      register AppLogger() as Logger
      register InMemoryStore() as DataStore

Correct:

register AppLogger() as Logger
      register InMemoryStore() as DataStore
      register MainService() as AppService

E08150 — Injection fields must use abstract component types. The lifecycle manager resolves abstract types to their registered concrete implementations during the prepare phase. See ek9 -h E08150 for details.

Incorrect:

store as InMemoryStore!

Correct:

store as DataStore!
Other ways to ask this
  • How are EK9 components created and destroyed?
  • What are the DI lifecycle phases in EK9?
  • Does EK9 support different component scopes like Spring?

Coming from another language?

Java Spring: singleton (default), prototype, request, session, application scopes. Complex lifecycle: @PostConstruct, @PreDestroy, InitializingBean, DisposableBean, BeanPostProcessor. Guice: unscoped (default), @Singleton, @RequestScoped, custom scopes. .NET: transient, scoped, singleton lifetimes. Python: manual lifecycle management. Go: manual lifecycle. Rust: ownership-based lifecycle. EK9: singleton only, _prepare() creates in order, _decommission() releases in reverse, no scope mismatch bugs.

Keywords: create, scope, phase, lifecycle, dependency, prepare, singleton, order, migrate, inject, component, decommission, reverse, destroy