> For the complete documentation index, see [llms.txt](https://docs.parsue.io/aceland-unity-packages/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.parsue.io/aceland-unity-packages/core-packages/why-a-new-di-and-lifecycle-stack-for-modern-unity.md).

# Why a New DI & Lifecycle Stack for Modern Unity

> A deep dive into **AceLand Injection** and **AceLand Lifecycle** — a zero-reflection dependency-injection container and a deterministic, dependency-ordered initialization engine, both designed from day one for Unity's **no-domain-reload / CoreCLR** direction.

Most of us inherited our Unity architecture from a different era. Dependency-injection frameworks were designed around domain reload, reflection-heavy resolution, and "one big scene bootstrap" patterns. They still work — but they carry weight that the modern Unity player loop no longer asks us to pay.

This article explains the reasoning behind two AceLand Core packages, how they compare to the tools you already know (Zenject/Extenject, VContainer, Reflex), and when they are the right — or the wrong — choice.

***

### 1. The shift that changes the rules

Two things about modern Unity break old assumptions:

1. **No domain reload on enter-play.** Static state is no longer wiped between play sessions. Any container or bootstrapper that quietly relied on the CLR resetting its statics now leaks or double-initializes.
2. **CoreCLR direction.** The runtime is moving toward faster, more predictable execution where reflection-driven first-access cost and hidden allocation are increasingly out of place.

A DI container and a lifecycle system sit at the very bottom of your architecture. If they assume the old model, everything above them inherits that assumption. AceLand Injection and AceLand Lifecycle are a deliberate reset for the new model.

***

### 2. AceLand Injection — DI without the reflection tax

#### The core idea

`[Inject]` is convenient, but in most frameworks that convenience is paid for at runtime — `Activator.CreateInstance`, cached `MethodInfo`, and a warm-up cost on first access. AceLand Injection moves that cost to **compile time**.

A **Roslyn incremental source generator** emits plain, readable injector code for every injectable type. Resolution is a **direct method call** — not reflection, not a cached delegate you can't inspect. You pay nothing at runtime for the ergonomics of attributes.

```csharp
using AceLand.Injection;

// 1. Publish a service to the global container
[AutoInstall]
public sealed class GameInstaller : IGlobalInstaller
{
    public void Install(IContainerBuilder builder)
    {
        builder.Register<IScoreService, ScoreService>(Lifetime.Singleton);
        builder.AddEntryPoint<GameLoop>();
    }
}

// 2. Consume it anywhere
public class Hud : MonoBehaviour
{
    [Inject] private IScoreService _score;   // injected by an InjectionScope

    void Awake() => DI.Inject(this);          // or DI.Resolve<IScoreService>()
}
```

The injector for `Hud` was generated when you compiled — there is no reflection to warm up the first time the object is created.

#### What you actually get

* **One global container, shared across packages.** Publish a service once with `DI` and resolve it from anywhere. No threading a container reference through every constructor, no re-registering the same service in ten scenes.
* **A surface you can hold in your head.** `Register`, `Resolve`, `Inject`, a handful of `Lifetime` values, and a scene-level `InjectionScope`. That is the whole model — no sub-container ceremony to wire up a simple game.
* **Component injection built in.** `[Self]`, `[Parent]`, `[Child]`, `[FromScene]`, `[AddComponent]` cover the Unity-specific wiring that pure C# DI containers leave to you.
* **Scene scopes.** Drop an `InjectionScope` on a GameObject for a child container per scene or prefab.
* **Object pooling** and **entry points** (`IInitializable` / `ITickable` / `IAsyncEntryPoint`) driven by Unity's loop.
* **Survives no-domain-reload.** No static state that rots between play sessions, no reload-dependent bootstrapping.
* **Fails at build time, not at 2 a.m.** A node-based graph window plus scene/prefab validation surface missing or circular dependencies **before** you ship — not as a `NullReferenceException` in the field.

Contracts live in the companion `com.aceland.injection.abstractions` package, so a library can declare its DI surface without depending on the runtime — keeping your own packages lightweight too.

***

### 3. AceLand Lifecycle — deterministic startup and shutdown

DI answers *"how do objects find each other?"* Lifecycle answers *"in what order does the world come alive, and how does it shut down cleanly?"* — a question most projects solve with a fragile pile of `Awake`/`Start` ordering hacks and `[DefaultExecutionOrder]`.

#### The core idea

You **declare** a module's phase and dependencies with an attribute. You do **not** hand- sort execution order. A **topological sorter** derives the real order from the dependency graph:

```csharp
[LifecycleModule(ModulePhase.Runtime, DependsOn = new[] { typeof(RemoteConfigModule) })]
public sealed class PlayerSystemModule : AsyncModuleBase
{
    public override void Initialize() { /* register services, set up fields */ }

    public override Task InitializeAsync(CancellationToken ct)
    {
        // load assets, open connections, warm caches…
        return Task.CompletedTask;
    }

    public override void Shutdown() { /* reverse cleanup, must be re-entrant */ }
}
```

Because dependencies are expressed as `typeof(...)`, the compiler enforces the reference, the asmdef must reference the assembly, and `package.json` must declare the dependency — **all three stay consistent automatically**, and the same data feeds the editor graph.

#### What you actually get

* **Dependency-ordered initialization.** Attributes only *register*; the sorter decides the true order. Reordering modules never means editing a magic number again.
* **Sync and async modules.** `IModule` for fast synchronous setup, `IAsyncModule` for loading/connecting. A synchronous module can't depend on an async one (the validator reports it), so you never accidentally read a not-yet-ready service.
* **Opt-in parallel init.** Mark same-level async modules `AllowParallel` to warm them up concurrently, with per-module and per-phase timeouts. The philosophy is **never deadlock** — a phase is forced forward on timeout and the issue is recorded.
* **A proper quit pipeline.** Deterministic, reverse-order shutdown with a safe-quit filter — flush saves and close connections *before* the app actually exits.
* **Player-loop scheduling.** Run work at precise player-loop points, every frame, after N frames, after a delay, or when a condition becomes true — without scattering `MonoBehaviour` timers everywhere.
* **Never gates your build.** The runtime is free and open source forever. Only the development-time Editor tools are paid (see §6).

***

### 4. Two packages, one architecture story

Injection and Lifecycle are complementary, and they are best adopted together:

```mermaid
flowchart LR
    subgraph Boot[Application Boot]
        L[Lifecycle<br/>ordered init + quit pipeline]
    end
    subgraph Wire[Object Wiring]
        I[Injection<br/>global container + scopes]
    end
    L -->|initializes systems that| I
    I -->|resolves services used by| L
    L --> Game[Your game systems]
    I --> Game
```

Lifecycle brings your systems up in the right order and tears them down cleanly. Injection wires those systems together without reflection. Together they form a modern, no-domain-reload **architecture baseline** — not "yet another DI container," but the foundation layer a package ecosystem can build on.

***

### 5. How it compares

> The goal here is honest positioning, not point-scoring. Every framework below is good; they were simply designed for different constraints.

<table data-search="false"><thead><tr><th width="157.20001220703125">Concern</th><th width="131.7999267578125">Zenject / Extenject</th><th width="134">VContainer</th><th width="110.800048828125">Reflex</th><th>AceLand Injection</th></tr></thead><tbody><tr><td>Resolution strategy</td><td>Reflection + codegen</td><td>IL/expression, low-alloc</td><td>Reflection, minimal</td><td><strong>Source-generated, zero reflection on hot path</strong></td></tr><tr><td>Runtime warm-up cost</td><td>Noticeable</td><td>Low</td><td>Low</td><td><strong>None (compile-time injectors)</strong></td></tr><tr><td>Mental model size</td><td>Large (sub-containers, bindings)</td><td>Medium</td><td>Small</td><td><strong>Small (Register/Resolve/Inject + scopes)</strong></td></tr><tr><td>Cross-package global container</td><td>Manual</td><td>Manual</td><td>Manual</td><td><strong>Built in (<code>DI</code>)</strong></td></tr><tr><td>Component wiring attributes</td><td>Partial</td><td>No</td><td>No</td><td><strong><code>[Self]/[Parent]/[Child]/[FromScene]/[AddComponent]</code></strong></td></tr><tr><td>No-domain-reload design</td><td>Retrofitted</td><td>Good</td><td>Good</td><td><strong>Designed for it from day one</strong></td></tr><tr><td>Build-time dependency validation</td><td>No</td><td>No</td><td>No</td><td><strong>Node graph + scene/prefab checks</strong></td></tr></tbody></table>

For lifecycle/initialization specifically, most teams roll their own on top of `RuntimeInitializeOnLoadMethod`, execution order, and ad-hoc bootstrappers. AceLand Lifecycle replaces that with a declared dependency graph, a topological sorter, parallel init with timeouts, and a real quit pipeline — plus editor graphs and a timeline profiler to verify and optimize it.

#### When **not** to reach for these

* You have a tiny project where a couple of `[SerializeField]` references are genuinely enough — DI would be overhead.
* Your team is deeply invested in a Zenject-style sub-container architecture and the migration cost outweighs the runtime win.
* You're locked to a Unity version/workflow that still hard-depends on domain reload and you have no plans to move.

Being clear about the non-fit is part of respecting the reader's time.

***

### 6. Open Core, honestly

Both packages follow an **Open Core** model:

* **The runtime is free and open source, forever.** Container, injectors, pooling, bootstrap, dependency-ordered init, quit pipeline, player-loop scheduling — none of it is ever gated. **Builds are never gated.**
* **Only the development-time Editor tools are paid.** The node-based dependency graphs, the initialization timeline profiler, the player-loop graph, and the quit-pipeline graph require an AceLand license. They are verification and optimization aids — remove them and your game still runs identically.

Licensing is an **optional** dependency: installing the packages never force-installs the license package. Unlicensed editors keep the runtime fully functional; the gated windows simply show a one-click install/activation prompt.

***

### 7. Getting started

Both packages are on **OpenUPM** and **GitHub**, and support **Unity 2022.3 through the latest LTS**.

* 📦 Injection: `com.aceland.injection` (+ `com.aceland.injection.abstractions`)
* 📦 Lifecycle: `com.aceland.lifecycle`
* 📖 Docs: <https://docs.parsue.io/aceland-unity-packages>
* 💬 Discord: <https://discord.gg/XsCYGnYzuc>
* ❤️ Sponsor: <https://github.com/sponsors/parsue>

Install from OpenUPM, drop in the sample scene, open the dependency graph, and press Play. If your architecture lives in the modern Unity player loop, these two packages are meant to be the quiet, fast foundation underneath it.

***

*Written by Parsue — AceLand Workshop. Feedback and issues are welcome on GitHub and Discord.*
