Repository: 7mind/izumi
Pull request #743 (2019-11-22 20:31:42)

Fix japanese typo in docs + README
The female name 泉水 was used instead of the noun 泉 which means "spring" (both are pronounced "izumi").
See https://jisho.org/search/%E6%B3%89%E6%B0%B4 for more info.

(I also replaced "jap." with "jp." as an abbreviation of "japanese", "jap" is considered a racial slur)


Patch:
diff --git a/README.md b/README.md
index b31d45e71..0d939a883 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
 What is it?
 ===========
 
-Izumi (*jap. 泉水, spring*) is a set of independent libraries and frameworks allowing you to significantly increase productivity of your Scala development.
+Izumi (*jp. 泉, spring*) is a set of independent libraries and frameworks allowing you to significantly increase productivity of your Scala development.
 
 including the following components:
 
diff --git a/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala
index 5a6897ef7..52c9170eb 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala
@@ -2,6 +2,7 @@ package izumi.distage
 
 import izumi.distage.model._
 import izumi.distage.model.definition.DIResource.DIResourceBase
+import izumi.distage.model.definition.ModuleBase
 import izumi.distage.model.monadic.DIEffect
 import izumi.distage.model.plan.{AbstractPlan, DodgyPlan, OrderedPlan, SemiPlan}
 import izumi.distage.model.provisioning.PlanInterpreter
@@ -17,6 +18,14 @@ class InjectorDefaultImpl(parentContext: Locator) extends Injector {
     planner.freeze(plan)
   }
 
+  override def planNoRewrite(input: PlannerInput): OrderedPlan = {
+    planner.planNoRewrite(input)
+  }
+
+  override def rewrite(module: ModuleBase): ModuleBase = {
+    planner.rewrite(module)
+  }
+
   override def prepare(input: PlannerInput): DodgyPlan = {
     planner.prepare(input)
   }
diff --git a/distage/distage-core/src/main/scala/izumi/distage/LocatorDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/LocatorDefaultImpl.scala
index 52a8821bd..e0e20b306 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/LocatorDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/LocatorDefaultImpl.scala
@@ -14,7 +14,7 @@ final class LocatorDefaultImpl[F[_]]
   val parent: Option[Locator],
   val dependencyMap: ProvisionImmutable[F],
 ) extends AbstractLocator {
-  override protected def unsafeLookup(key: RuntimeDIUniverse.DIKey): Option[Any] =
+  override protected def lookupLocalUnsafe(key: RuntimeDIUniverse.DIKey): Option[Any] =
     dependencyMap.get(key)
 
   override protected[distage] def finalizers[F1[_]: TagK]: collection.Seq[PlanInterpreter.Finalizer[F1]] = {
diff --git a/distage/distage-core/src/main/scala/izumi/distage/bootstrap/BootstrapLocator.scala b/distage/distage-core/src/main/scala/izumi/distage/bootstrap/BootstrapLocator.scala
index a9bc3bb78..14cae418a 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/bootstrap/BootstrapLocator.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/bootstrap/BootstrapLocator.scala
@@ -47,7 +47,7 @@ final class BootstrapLocator(bindings: BootstrapContextModule) extends AbstractL
 
   override protected[distage] def finalizers[F[_] : TagK]: collection.Seq[PlanInterpreter.Finalizer[F]] = collection.Seq.empty
 
-  override protected def unsafeLookup(key: RuntimeDIUniverse.DIKey): Option[Any] = {
+  override protected def lookupLocalUnsafe(key: RuntimeDIUniverse.DIKey): Option[Any] = {
     Option(_instances.get()) match {
       case Some(_) =>
         index.get(key)
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/AssignableFromAutoSetHook.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/AutoSetHook.scala
similarity index 83%
rename from distage/distage-core/src/main/scala/izumi/distage/planning/AssignableFromAutoSetHook.scala
rename to distage/distage-core/src/main/scala/izumi/distage/planning/AutoSetHook.scala
index 64bcc85ec..c0fa98753 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/AssignableFromAutoSetHook.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/AutoSetHook.scala
@@ -15,7 +15,7 @@ import scala.collection.immutable.ListSet
   * Usage:
   *
   * {{{
-  *   val collectCloseables = new AssignableFromAutoSetHook[AutoCloseable, AutoCloseable](identity)
+  *   val collectCloseables = new AutoSetHook[AutoCloseable, AutoCloseable](identity)
   *
   *   val injector = Injector(new BootstrapModuleDef {
   *     many[PlanningHook]
@@ -45,13 +45,13 @@ import scala.collection.immutable.ListSet
   *   }
   * }}}
   *
-  * Auto-Sets are NOT subject to [[gc.TracingDIGC Garbage Collection]], they are assembled *after* garbage collection is done,
-  * as such they can't contain garbage by construction.
-  *
-  **/
-class AssignableFromAutoSetHook[INSTANCE: Tag, BINDING: Tag](private val wrap: INSTANCE => BINDING) extends PlanningHook {
+  * Auto-Sets are NOT subject to [[gc.TracingDIGC Garbage Collection]], they are assembled
+  * *after* garbage collection is done, as such they can't contain garbage by construction
+  * and they cannot be designated as GC root keys.
+  */
+class AutoSetHook[INSTANCE: Tag, BINDING: Tag](private val wrap: INSTANCE => BINDING) extends PlanningHook {
   protected val instanceType: SafeType = SafeType.get[INSTANCE]
-  protected val setElemetType: SafeType = SafeType.get[BINDING]
+  protected val setElementType: SafeType = SafeType.get[BINDING]
   protected val setKey: DIKey = DIKey.get[Set[BINDING]]
 
   override def phase45PreForwardingCleanup(plan: SemiPlan): SemiPlan = {
@@ -78,9 +78,14 @@ class AssignableFromAutoSetHook[INSTANCE: Tag, BINDING: Tag](private val wrap: I
     val newMembers = scala.collection.mutable.ArrayBuffer[DIKey]()
 
     val newSteps = plan.steps.flatMap {
-      op =>
+      // do not process top-level references to avoid duplicates (the target of reference will be included anyway)
+      case op @ (_: ExecutableOp.WiringOp.ReferenceKey | _: ExecutableOp.ProxyOp.InitProxy) =>
+        Seq(op)
+
+      case op =>
         op.target match {
           case `setKey` =>
+            // throw out previous set if any
             op match {
               case op: ExecutableOp.CreateSet =>
                 newMembers ++= op.members
@@ -93,7 +98,7 @@ class AssignableFromAutoSetHook[INSTANCE: Tag, BINDING: Tag](private val wrap: I
             Seq(op)
 
           case _ if ExecutableOp.instanceType(op) <:< instanceType =>
-            if (instanceType == setElemetType) {
+            if (instanceType == setElementType) {
               newMembers += op.target
               Seq(op)
             } else {
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/AutoSetModule.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/AutoSetModule.scala
index f09bee1ac..705911d72 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/AutoSetModule.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/AutoSetModule.scala
@@ -13,13 +13,13 @@ import izumi.distage.model.planning.PlanningHook
 abstract class AutoSetModule extends BootstrapModuleDef {
   def register[T: Tag]: AutoSetModule = {
     many[T]
-    many[PlanningHook].add(new AssignableFromAutoSetHook[T, T](identity))
+    many[PlanningHook].add(new AutoSetHook[T, T](identity))
     this
   }
 
   def register[T: Tag, B: Tag](wrap: T => B): AutoSetModule = {
     many[B]
-    many[PlanningHook].add(new AssignableFromAutoSetHook[T, B](wrap))
+    many[PlanningHook].add(new AutoSetHook[T, B](wrap))
     this
   }
 }
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/ForwardingRefResolverDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/ForwardingRefResolverDefaultImpl.scala
index 07eb9ce5e..0ee51995a 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/ForwardingRefResolverDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/ForwardingRefResolverDefaultImpl.scala
@@ -64,30 +64,29 @@ class ForwardingRefResolverDefaultImpl
     }
   }
 
-    protected def iniProxiesJustInTime(proxies: mutable.HashSet[ProxyOp.MakeProxy], resolvedSteps: Seq[ExecutableOp]): Seq[ExecutableOp] = {
-      // more expensive eager just-in-time policy
-      val passed = mutable.HashSet[DIKey]()
-      val proxyOps = resolvedSteps.flatMap {
-        op =>
-          passed.add(op.target)
-
-          val completedProxies = proxies.filter(_.forwardRefs.diff(passed).isEmpty)
-          val inits = completedProxies.map {
-            proxy =>
-              ProxyOp.InitProxy(proxy.target, proxy.forwardRefs, proxy, op.origin)
-          }.toVector
-
-          completedProxies.foreach {
-            i =>
-              proxies.remove(i)
-          }
-
-          op +: inits
-      }
-      assert(proxies.isEmpty)
-      proxyOps
-    }
+  protected def iniProxiesJustInTime(proxies: mutable.HashSet[ProxyOp.MakeProxy], resolvedSteps: Seq[ExecutableOp]): Seq[ExecutableOp] = {
+    // more expensive eager just-in-time policy
+    val passed = mutable.HashSet[DIKey]()
+    val proxyOps = resolvedSteps.flatMap {
+      op =>
+        passed.add(op.target)
+
+        val completedProxies = proxies.filter(_.forwardRefs.diff(passed).isEmpty)
+        val inits = completedProxies.map {
+          proxy =>
+            ProxyOp.InitProxy(proxy.target, proxy.forwardRefs, proxy, op.origin)
+        }.toVector
+
+        completedProxies.foreach {
+          i =>
+            proxies.remove(i)
+        }
 
+        op +: inits
+    }
+    assert(proxies.isEmpty)
+    proxyOps
+  }
 
   protected def allUsagesAreByName(index: Map[DIKey, Vector[ExecutableOp]], target: DIKey, usages: Set[DIKey]): Boolean = {
     val usedByOps = (usages + target).flatMap(index.apply)
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala
index bed64cb17..41bd3b1e8 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala
@@ -10,6 +10,7 @@ import izumi.distage.model.{Planner, PlannerInput}
 import izumi.functional.Value
 import izumi.fundamentals.graphs.Toposort
 import distage.DIKey
+import izumi.distage.model.definition.ModuleBase
 
 final class PlannerDefaultImpl
 (
@@ -26,12 +27,24 @@ final class PlannerDefaultImpl
   extends Planner {
 
   override def plan(input: PlannerInput): OrderedPlan = {
-    Value(prepare(input))
+    Value(input)
+      .map(p => p.copy(bindings = rewrite(p.bindings)))
+      .map(planNoRewrite)
+      .get
+  }
+
+  override def planNoRewrite(input: PlannerInput): OrderedPlan = {
+    Value(input)
+      .map(prepare)
       .map(freeze)
       .map(finish)
       .get
   }
 
+  override def rewrite(module: ModuleBase): ModuleBase= {
+    hook.hookDefinition(module)
+  }
+
   override def freeze(plan: DodgyPlan): SemiPlan = {
     Value(plan)
       .map(hook.phase00PostCompletion)
@@ -46,17 +59,14 @@ final class PlannerDefaultImpl
   }
 
   override def prepare(input: PlannerInput): DodgyPlan = {
-    hook
-      .hookDefinition(input.bindings)
-      .bindings
-      .foldLeft(DodgyPlan.empty(input.bindings, input.mode)) {
-        case (currentPlan, binding) =>
-          Value(bindingTranslator.computeProvisioning(currentPlan, binding))
-            .eff(sanityChecker.assertProvisionsSane)
-            .map(next => currentPlan.append(binding, next))
-            .eff(planningObserver.onSuccessfulStep)
-            .get
-      }
+      input.bindings.bindings.foldLeft(DodgyPlan.empty(input.bindings, input.mode)) {
+      case (currentPlan, binding) =>
+        Value(bindingTranslator.computeProvisioning(currentPlan, binding))
+          .eff(sanityChecker.assertProvisionsSane)
+          .map(next => currentPlan.append(binding, next))
+          .eff(planningObserver.onSuccessfulStep)
+          .get
+    }
   }
 
   override def finish(semiPlan: SemiPlan): OrderedPlan = {
diff --git a/distage/distage-model/src/main/scala/izumi/distage/AbstractLocator.scala b/distage/distage-model/src/main/scala/izumi/distage/AbstractLocator.scala
index 9340824e5..bc0a4dc99 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/AbstractLocator.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/AbstractLocator.scala
@@ -5,13 +5,13 @@ import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
 import izumi.distage.model.Locator
 
 trait AbstractLocator extends Locator {
-  protected def unsafeLookup(key: DIKey): Option[Any]
+  protected def lookupLocalUnsafe(key: DIKey): Option[Any]
 
-  protected[distage] def lookup[T: Tag](key: DIKey): Option[TypedRef[T]] = {
-    unsafeLookup(key)
-      .filter(_ => key.tpe <:< SafeType.get[T])
+  protected[distage] def lookupLocal[T: Tag](key: DIKey): Option[TypedRef[T]] = {
+    lookupLocalUnsafe(key)
       .map {
         value =>
+          assert(key.tpe <:< SafeType.get[T], s"$key in not a subtype of ${SafeType.get[T]}")
           TypedRef[T](value.asInstanceOf[T])
       }
   }
@@ -43,7 +43,7 @@ trait AbstractLocator extends Locator {
   }
 
   private[this] final def recursiveLookup[T: Tag](key: DIKey, locator: Locator): Option[TypedRef[T]] = {
-    locator.lookup[T](key)
+    locator.lookupLocal[T](key)
       .orElse(locator.parent.flatMap(p => recursiveLookup(key, p)))
   }
 }
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/Locator.scala b/distage/distage-model/src/main/scala/izumi/distage/model/Locator.scala
index 41a1ee05f..3bbd8950a 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/Locator.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/Locator.scala
@@ -39,7 +39,7 @@ trait Locator {
   def get[T: Tag](id: String): T
 
   protected[distage] def finalizers[F[_] : TagK]: collection.Seq[Finalizer[F]]
-  protected[distage] def lookup[T: Tag](key: DIKey): Option[TypedRef[T]]
+  protected[distage] def lookupLocal[T: Tag](key: DIKey): Option[TypedRef[T]]
 
   def index: Map[DIKey, Any] = {
     instances.map(i => i.key -> i.value).toMap
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/Planner.scala b/distage/distage-model/src/main/scala/izumi/distage/model/Planner.scala
index 551019801..6211530eb 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/Planner.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/Planner.scala
@@ -2,6 +2,7 @@ package izumi.distage.model
 
 import izumi.distage.model.definition.ModuleBase
 import izumi.distage.model.plan._
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.DIKey
 
 sealed trait GCMode {
@@ -14,16 +15,20 @@ sealed trait GCMode {
       GCMode.GCRoots(aRoots ++ bRoots)
   }
 }
+
 object GCMode {
   def apply(key: DIKey, more: DIKey*): GCMode = GCRoots(more.toSet + key)
 
   final case class GCRoots(roots: Set[DIKey]) extends GCMode {
     assert(roots.nonEmpty, "GC roots set cannot be empty")
+
     override def toSet: Set[DIKey] = roots
   }
+
   case object NoGC extends GCMode {
     override def toSet: Set[DIKey] = Set.empty
   }
+
 }
 
 /**
@@ -53,7 +58,21 @@ object PlannerInput {
   }
 }
 
-case class SplittedPlan(subplan: OrderedPlan, subRoots: Set[DIKey], primary: OrderedPlan, reducedModule: ModuleBase)
+//case class SplittedPlan(
+//                         subplan: OrderedPlan,
+//                         subRoots: Set[DIKey],
+//                         primary: OrderedPlan,
+//                         reducedModule: ModuleBase
+//                       )
+
+case class Subplan(plan: OrderedPlan, roots: Set[DIKey], module: ModuleBase)
+
+case class TriSplittedPlan(
+                            side: Subplan,
+                            primary: Subplan,
+                            shared: Subplan,
+                          )
+
 
 /** Transforms [[ModuleBase]] into [[OrderedPlan]] */
 trait Planner {
@@ -62,6 +81,10 @@ trait Planner {
   def merge(a: AbstractPlan, b: AbstractPlan): OrderedPlan
 
   // plan lifecycle
+  def planNoRewrite(input: PlannerInput): OrderedPlan
+
+  def rewrite(module: ModuleBase): ModuleBase
+
   def prepare(input: PlannerInput): DodgyPlan
 
   def freeze(plan: DodgyPlan): SemiPlan
@@ -70,42 +93,56 @@ trait Planner {
 
   final def plan(input: ModuleBase, gcMode: GCMode): OrderedPlan = plan(PlannerInput(input, gcMode))
 
-  final def splitExistingPlan(appModule: ModuleBase, primaryRoots: Set[DIKey], disabled: Set[DIKey], primaryPlan: OrderedPlan)(extractSubRoots: OrderedPlan => Set[DIKey]): SplittedPlan = {
-    assert(primaryRoots.diff(primaryPlan.keys).isEmpty)
+  final def triSplitPlan(appModule: ModuleBase, primaryRoots: Set[DIKey])(extractSubRoots: OrderedPlan => Set[DIKey]): TriSplittedPlan = {
+    val rewritten = rewrite(appModule)
+    val primaryPlan = toSubplanNoRewrite(appModule, primaryRoots)
+
     // here we extract integration checks out of our shared components plan and build it
-    val extractedRoots = extractSubRoots(primaryPlan)
+    val subplanRoots = extractSubRoots(primaryPlan)
+    triPlan(rewritten, primaryPlan, primaryRoots, subplanRoots)
+  }
 
-    val extractedSubplan = if (extractedRoots.nonEmpty) {
-      // exclude runtime
-      plan(PlannerInput(appModule, extractedRoots))
-    } else {
-      OrderedPlan.empty
-    }
+  final def triPlan(appModule: ModuleBase, primaryRoots: Set[DIKey], subplanRoots: Set[DIKey]): TriSplittedPlan = {
+    val primaryPlan = toSubplanNoRewrite(appModule, primaryRoots)
+    triPlan(appModule, primaryPlan, primaryRoots, subplanRoots)
+  }
 
-    val reduced = appModule.drop(extractedSubplan.keys)
+  private final def triPlan(appModule: ModuleBase, extractedPrimaryPlan: OrderedPlan, primaryRoots: Set[DIKey], subplanRoots: Set[DIKey]): TriSplittedPlan = {
+    assert(primaryRoots.diff(extractedPrimaryPlan.keys).isEmpty)
+    val extractedSubplan = toSubplanNoRewrite(appModule, subplanRoots)
 
-    // and here we build our final plan for shared components, with integration components excluded
-    val primaryPlanWithoutExtractedPart = if (extractedRoots.nonEmpty || disabled.nonEmpty) {
-      val partsLeft = primaryRoots -- extractedRoots -- disabled
-      if (partsLeft.isEmpty) {
-        OrderedPlan.empty
-      } else {
-        plan(PlannerInput(reduced, partsLeft))
-      }
-    } else {
-      primaryPlan
-    }
+    val sharedKeys = extractedSubplan.index.keySet.intersect(extractedPrimaryPlan.index.keySet)
+    val sharedPlan = toSubplanNoRewrite(appModule, sharedKeys)
+
+    val noSharedComponentsModule = appModule.drop(sharedKeys)
+    val primplan = toSubplanNoRewrite(noSharedComponentsModule, primaryRoots)
+    val subModule = noSharedComponentsModule.drop(primplan.index.keySet)
+    val subplan = toSubplanNoRewrite(subModule, subplanRoots)
+
+    //    val conflicts = primplan.index.keySet.intersect(subplan.index.keySet).filterNot(k => primplan.index(k).isInstanceOf[ExecutableOp.ImportDependency])
+    //    assert(conflicts.isEmpty, s"conflicts: ${conflicts}")
 
-    SplittedPlan(extractedSubplan, extractedRoots, primaryPlanWithoutExtractedPart, reduced)
+    val sharedModule = appModule.preserveOnly(sharedPlan.index.keySet)
+    val primModule = noSharedComponentsModule.drop(subplan.index.keySet)
+
+    TriSplittedPlan(
+      Subplan(subplan, subplanRoots, subModule),
+      Subplan(primplan, primaryRoots, primModule),
+      Subplan(sharedPlan, sharedKeys, sharedModule),
+    )
+
+  }
+
+  private def toSubplanNoRewrite(appModule: ModuleBase, extractedRoots: Set[RuntimeDIUniverse.DIKey]): OrderedPlan = {
+    toSubplan(appModule, extractedRoots, planNoRewrite)
   }
 
-  final def splitPlan(appModule: ModuleBase, primaryRoots: Set[DIKey])(extractSubRoots: OrderedPlan => Set[DIKey]): SplittedPlan = {
-    val primaryPlan = if (primaryRoots.nonEmpty) {
-      plan(PlannerInput(appModule, primaryRoots))
+  private def toSubplan(appModule: ModuleBase, extractedRoots: Set[RuntimeDIUniverse.DIKey], plan: PlannerInput => OrderedPlan): OrderedPlan= {
+    if (extractedRoots.nonEmpty) {
+      // exclude runtime
+      plan(PlannerInput(appModule, extractedRoots))
     } else {
       OrderedPlan.empty
     }
-
-    splitExistingPlan(appModule, primaryRoots, Set.empty, primaryPlan)(extractSubRoots)
   }
 }
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/Producer.scala b/distage/distage-model/src/main/scala/izumi/distage/model/Producer.scala
index d91a70fc4..746e2368f 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/Producer.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/Producer.scala
@@ -11,7 +11,6 @@ import izumi.fundamentals.platform.functional.Identity
 /** Executes instructions in [[OrderedPlan]] to produce a [[Locator]] */
 trait Producer {
   protected[distage] def produceFX[F[_]: TagK: DIEffect](plan: OrderedPlan, filter: FinalizersFilter[F]): DIResourceBase[F, Locator]
-
   protected[distage] def produceDetailedFX[F[_]: TagK: DIEffect](plan: OrderedPlan, filter: FinalizersFilter[F]): DIResourceBase[F, Either[FailedProvision[F], Locator]]
 
   final def produceF[F[_]: TagK: DIEffect](plan: OrderedPlan): DIResourceBase[F, Locator] = {
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/definition/LocatorDef.scala b/distage/distage-model/src/main/scala/izumi/distage/model/definition/LocatorDef.scala
index e1e0bcaf7..964e190ee 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/definition/LocatorDef.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/definition/LocatorDef.scala
@@ -40,7 +40,7 @@ trait LocatorDef
 
   protected def initialState: mutable.ArrayBuffer[BindingRef] = mutable.ArrayBuffer.empty
 
-  override protected def unsafeLookup(key: DIKey): Option[Any] = {
+  override protected def lookupLocalUnsafe(key: DIKey): Option[Any] = {
     frozenMap.get(key)
   }
 
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/definition/ModuleBase.scala b/distage/distage-model/src/main/scala/izumi/distage/model/definition/ModuleBase.scala
index 8433214ec..c693d0fed 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/definition/ModuleBase.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/definition/ModuleBase.scala
@@ -86,6 +86,11 @@ object ModuleBase {
       T.make(Set(binding)) ++ moduleDef
     }
 
+    def preserveOnly(preserve: Set[DIKey]): T = {
+      val filtered = moduleDef.bindings.filterNot(b => preserve.contains(b.key))
+      T.make(filtered)
+    }
+
     def drop(ignored: Set[DIKey]): T = {
       val filtered = moduleDef.bindings.filterNot(b => ignored.contains(b.key))
       T.make(filtered)
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/monadic/DIEffectAsync.scala b/distage/distage-model/src/main/scala/izumi/distage/model/monadic/DIEffectAsync.scala
index c65e66bad..0f87e81ca 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/monadic/DIEffectAsync.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/monadic/DIEffectAsync.scala
@@ -11,6 +11,7 @@ import scala.concurrent.{Await, ExecutionContext, Future}
 
 trait DIEffectAsync[F[_]] {
   def parTraverse_[A](l: Iterable[A])(f: A => F[Unit]): F[Unit]
+  def parTraverse[A, B](l: Iterable[A])(f: A => F[B]): F[List[B]]
   def sleep(duration: FiniteDuration): F[Unit]
 }
 
@@ -20,24 +21,32 @@ object DIEffectAsync extends LowPriorityDIEffectAsyncInstances {
   implicit val diEffectParIdentity: DIEffectAsync[Identity] = {
     new DIEffectAsync[Identity] {
       override def parTraverse_[A](l: Iterable[A])(f: A => Unit): Unit = {
-        implicit val ec = ExecutionContext.global
-        val future = Future.sequence(l.map(a => Future(f(a))))
-        Await.result(future, Duration.Inf)
+
+        parTraverse(l)(f)
       }
       override def sleep(duration: FiniteDuration): Identity[Unit] = {
         Thread.sleep(duration.toMillis)
       }
+
+      override def parTraverse[A, B](l: Iterable[A])(f: A => Identity[B]): Identity[List[B]] = {
+        implicit val ec = ExecutionContext.global
+        val future = Future.sequence(l.map(a => Future(f(a))))
+        Await.result(future, Duration.Inf).toList
+      }
     }
   }
 
   implicit def fromBIOAsync[F[+_, +_]: BIOAsync]: DIEffectAsync[F[Throwable, ?]] = {
     new DIEffectAsync[F[Throwable, ?]] {
       override def parTraverse_[A](l: Iterable[A])(f: A => F[Throwable, Unit]): F[Throwable, Unit] = {
-        F.parTraverse_(l)(f).void
+        F.parTraverse_(l)(f)
       }
       override def sleep(duration: FiniteDuration): F[Throwable, Unit] = {
         F.sleep(duration)
       }
+      override def parTraverse[A, B](l: Iterable[A])(f: A => F[Throwable, B]): F[Throwable, List[B]] = {
+        F.parTraverse(l)(f)
+      }
     }
   }
 
@@ -58,6 +67,9 @@ private[monadic] sealed trait LowPriorityDIEffectAsyncInstances {
       override def sleep(duration: FiniteDuration): F[Unit] = {
         T.asInstanceOf[Timer[F]].sleep(duration)
       }
+      override def parTraverse[A, B](l: Iterable[A])(f: A => F[B]): F[List[B]] = {
+        Parallel.parTraverse(l.toList)(f)(cats.instances.list.catsStdInstancesForList, P.asInstanceOf[Parallel[F]])
+      }
     }
   }
 }
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/AbstractPlan.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/AbstractPlan.scala
index 80586980a..273ab5354 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/AbstractPlan.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/AbstractPlan.scala
@@ -160,7 +160,7 @@ final case class SemiPlan(definition: ModuleBase, steps: Vector[ExecutableOp], g
   }
 
   override def locateImports(locator: Locator): SemiPlan = {
-    resolveImports(Function.unlift(i => locator.lookup[Any](i.target)))
+    resolveImports(Function.unlift(i => locator.lookupLocal[Any](i.target)))
   }
 
 }
@@ -220,7 +220,7 @@ final case class OrderedPlan(definition: ModuleBase, steps: Vector[ExecutableOp]
   }
 
   override def locateImports(locator: Locator): OrderedPlan = {
-    resolveImports(Function.unlift(i => locator.lookup[Any](i.target)))
+    resolveImports(Function.unlift(i => locator.lookupLocal[Any](i.target)))
   }
 }
 
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/DodgyPlan.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/DodgyPlan.scala
index 62a8ab3ee..31775089c 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/DodgyPlan.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/DodgyPlan.scala
@@ -4,8 +4,7 @@ import izumi.distage.model.GCMode
 import izumi.distage.model.definition.{Binding, ModuleBase}
 import izumi.distage.model.plan.DodgyPlan._
 import izumi.distage.model.plan.ExecutableOp.{CreateSet, InstantiationOp}
-import izumi.distage.model.reflection.universe
-import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse.DIKey
 import izumi.fundamentals.collections.ImmutableMultiMap
 import izumi.fundamentals.collections.IzCollections._
 import izumi.fundamentals.platform.language.Quirks
@@ -20,7 +19,7 @@ final class DodgyPlan(
 
   private val ops = new mutable.ArrayBuffer[TraceableOp]()
 
-  def freeze: ImmutableMultiMap[universe.RuntimeDIUniverse.DIKey, TraceableOp] = {
+  def freeze: ImmutableMultiMap[DIKey, TraceableOp] = {
     ops.map(op => op.key -> op).toMultimap
   }
 
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/ExecutableOp.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/ExecutableOp.scala
index 1cba7dccf..1a27a6521 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/ExecutableOp.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/ExecutableOp.scala
@@ -16,10 +16,9 @@ sealed trait ExecutableOp {
 }
 
 object ExecutableOp {
+  final case class ImportDependency(target: DIKey, references: Set[DIKey], origin: Option[Binding]) extends ExecutableOp
 
   sealed trait InstantiationOp extends ExecutableOp
-
-  final case class ImportDependency(target: DIKey, references: Set[DIKey], origin: Option[Binding]) extends ExecutableOp
   final case class CreateSet(target: DIKey, element: SafeType, members: Set[DIKey], origin: Option[Binding]) extends InstantiationOp
 
   sealed trait WiringOp extends InstantiationOp {
@@ -27,7 +26,6 @@ object ExecutableOp {
     def wiring: PureWiring
     def origin: Option[Binding]
   }
-
   object WiringOp {
     final case class InstantiateClass(target: DIKey, wiring: SingletonWiring.Constructor, origin: Option[Binding]) extends WiringOp
     final case class InstantiateTrait(target: DIKey, wiring: SingletonWiring.AbstractSymbol, origin: Option[Binding]) extends WiringOp
@@ -42,7 +40,6 @@ object ExecutableOp {
     def effectWiring: PureWiring
     def wiring: Wiring.MonadicWiring
   }
-
   object MonadicOp {
     final case class ExecuteEffect(target: DIKey, effectOp: WiringOp, wiring: Wiring.MonadicWiring.Effect, origin: Option[Binding]) extends MonadicOp {
       override def effectWiring: RuntimeDIUniverse.Wiring.PureWiring = wiring.effectWiring
@@ -53,7 +50,6 @@ object ExecutableOp {
   }
 
   sealed trait ProxyOp extends ExecutableOp
-
   object ProxyOp {
     final case class MakeProxy(op: InstantiationOp, forwardRefs: Set[DIKey], origin: Option[Binding], byNameAllowed: Boolean) extends ProxyOp {
       override def target: DIKey = op.target
@@ -68,7 +64,7 @@ object ExecutableOp {
         w.wiring match {
           case u: Wiring.SingletonWiring =>
             u.instanceType
-          case _ =>
+          case _: Wiring.Factory | _: Wiring.FactoryFunction =>
             w.target.tpe
         }
       case p: MakeProxy =>
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/plan/WithDIWiring.scala b/distage/distage-model/src/main/scala/izumi/distage/model/plan/WithDIWiring.scala
index 174c1e5ca..de0b88f14 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/plan/WithDIWiring.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/plan/WithDIWiring.scala
@@ -16,64 +16,49 @@ trait WithDIWiring {
 
   sealed trait Wiring {
     def instanceType: SafeType
-
     def associations: Seq[Association]
+    def replaceKeys(f: Association => DIKey.BasicKey): Wiring
 
     def requiredKeys: Set[DIKey] = associations.map(_.wireWith).toSet
-
-    def replaceKeys(f: Association => DIKey.BasicKey): Wiring
   }
 
   object Wiring {
-
     sealed trait PureWiring extends Wiring {
       override def replaceKeys(f: Association => DIKey.BasicKey): PureWiring
     }
 
     sealed trait SingletonWiring extends PureWiring {
       def instanceType: SafeType
-
       override def replaceKeys(f: Association => DIKey.BasicKey): SingletonWiring
     }
-
-
     object SingletonWiring {
-
       sealed trait ReflectiveInstantiationWiring extends SingletonWiring {
         def prefix: Option[DIKey]
-        override def replaceKeys(f: Association => DIKey.BasicKey): ReflectiveInstantiationWiring
 
+        override def replaceKeys(f: Association => DIKey.BasicKey): ReflectiveInstantiationWiring
         override final def requiredKeys: Set[DIKey] = super.requiredKeys ++ prefix.toSet
       }
 
       case class Constructor(instanceType: SafeType, associations: Seq[Association.Parameter], prefix: Option[DIKey]) extends ReflectiveInstantiationWiring {
-        override final def replaceKeys(f: Association => DIKey.BasicKey): Constructor =
-          this.copy(associations = this.associations.map(a => a.withWireWith(f(a))))
+        override final def replaceKeys(f: Association => DIKey.BasicKey): Constructor = this.copy(associations = this.associations.map(a => a.withWireWith(f(a))))
       }
-
       case class AbstractSymbol(instanceType: SafeType, associations: Seq[Association.AbstractMethod], prefix: Option[DIKey]) extends ReflectiveInstantiationWiring {
-        override final def replaceKeys(f: Association => DIKey.BasicKey): AbstractSymbol =
-          this.copy(associations = this.associations.map(a => a.withWireWith(f(a))))
+        override final def replaceKeys(f: Association => DIKey.BasicKey): AbstractSymbol = this.copy(associations = this.associations.map(a => a.withWireWith(f(a))))
       }
-
       case class Function(provider: Provider, associations: Seq[Association.Parameter]) extends SingletonWiring {
         override def instanceType: SafeType = provider.ret
 
-        override final def replaceKeys(f: Association => DIKey.BasicKey): Function =
-          this.copy(associations = this.associations.map(a => a.withWireWith(f(a))))
+        override final def replaceKeys(f: Association => DIKey.BasicKey): Function = this.copy(associations = this.associations.map(a => a.withWireWith(f(a))))
       }
-
       case class Instance(instanceType: SafeType, instance: Any) extends SingletonWiring {
         override def associations: Seq[Association] = Seq.empty
 
         override def replaceKeys(f: Association => DIKey.BasicKey): Instance = { f.discard(); this }
       }
-
       case class Reference(instanceType: SafeType, key: DIKey, weak: Boolean) extends SingletonWiring {
         override def associations: Seq[Association] = Seq.empty
 
         override def requiredKeys: Set[DIKey] = super.requiredKeys ++ Set(key)
-
         override def replaceKeys(f: Association => DIKey.BasicKey): Reference = { f.discard(); this }
       }
     }
@@ -82,23 +67,19 @@ trait WithDIWiring {
       def effectDIKey: DIKey
       def effectHKTypeCtor: SafeType
     }
-
     object MonadicWiring {
-
       case class Effect(instanceType: SafeType, effectHKTypeCtor: SafeType, effectWiring: PureWiring) extends MonadicWiring {
+        override def effectDIKey: DIKey = DIKey.TypeKey(effectWiring.instanceType)
         override def associations: Seq[Association] = effectWiring.associations
 
         override def replaceKeys(f: Association => DIKey.BasicKey): Effect = copy(effectWiring = effectWiring.replaceKeys(f))
-
-        override def effectDIKey: DIKey = DIKey.TypeKey(effectWiring.instanceType)
       }
 
       case class Resource(instanceType: SafeType, effectHKTypeCtor: SafeType, effectWiring: PureWiring) extends MonadicWiring {
+        override def effectDIKey: DIKey = DIKey.TypeKey(effectWiring.instanceType)
         override def associations: Seq[Association] = effectWiring.associations
 
         override def replaceKeys(f: Association => DIKey.BasicKey): Resource = copy(effectWiring = effectWiring.replaceKeys(f))
-
-        override def effectDIKey: DIKey = DIKey.TypeKey(effectWiring.instanceType)
       }
     }
 
@@ -117,6 +98,7 @@ trait WithDIWiring {
         factorySuppliedProductDeps ++ fieldDependencies
       }
 
+      override final def instanceType: SafeType = factoryType
 
       override def requiredKeys: Set[DIKey] = {
         super.requiredKeys ++factoryMethods.flatMap(_.wireWith.prefix.toSeq).toSet
@@ -127,8 +109,6 @@ trait WithDIWiring {
           fieldDependencies = this.fieldDependencies.map(a => a.withWireWith(f(a)))
           , factoryMethods = this.factoryMethods.map(m => m.copy(wireWith = m.wireWith.replaceKeys(f)))
         )
-
-      override final def instanceType: SafeType = factoryType
     }
 
     object Factory {
@@ -154,8 +134,8 @@ trait WithDIWiring {
 
       override final def replaceKeys(f: Association => DIKey.BasicKey): FactoryFunction =
         this.copy(
-          providerArguments = this.providerArguments.map(a => a.withWireWith(f(a)))
-          , factoryIndex = this.factoryIndex.mapValues(m => m.copy(wireWith = m.wireWith.replaceKeys(f))).toMap // 2.13 compat
+          providerArguments = this.providerArguments.map(a => a.withWireWith(f(a))),
+          factoryIndex = this.factoryIndex.mapValues(m => m.copy(wireWith = m.wireWith.replaceKeys(f))).toMap // 2.13 compat
         )
 
       override final def instanceType: SafeType = provider.ret
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanMergingPolicy.scala b/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanMergingPolicy.scala
index 35718998f..40bb2264c 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanMergingPolicy.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/planning/PlanMergingPolicy.scala
@@ -12,11 +12,8 @@ trait PlanMergingPolicy {
 object PlanMergingPolicy {
 
   sealed trait DIKeyConflictResolution
-
   object DIKeyConflictResolution {
-
     final case class Successful(op: Set[ExecutableOp]) extends DIKeyConflictResolution
-
     final case class Failed(candidates: Set[ExecutableOp], explanation: String) extends DIKeyConflictResolution
   }
 
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/provisioning/PlanInterpreter.scala b/distage/distage-model/src/main/scala/izumi/distage/model/provisioning/PlanInterpreter.scala
index b4a8c9149..cce784c7b 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/provisioning/PlanInterpreter.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/provisioning/PlanInterpreter.scala
@@ -21,13 +21,11 @@ object PlanInterpreter {
   trait FinalizersFilter[F[_]] {
     def filter(finalizers: collection.Seq[Finalizer[F]]): collection.Seq[Finalizer[F]]
   }
-
   object FinalizersFilter {
     def all[F[_]]: FinalizersFilter[F] = finalizers => finalizers
   }
 
   final case class Finalizer[+F[_]](key: DIKey, effect: () => F[Unit], fType: SafeType)
-
   object Finalizer {
     def apply[F[_]: TagK](key: DIKey, effect: () => F[Unit]): Finalizer[F] = new Finalizer(key, effect, SafeType.getK[F])
   }
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/references/WithDIKey.scala b/distage/distage-model/src/main/scala/izumi/distage/model/references/WithDIKey.scala
index f190438ed..55b04cc6d 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/references/WithDIKey.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/references/WithDIKey.scala
@@ -13,30 +13,28 @@ trait WithDIKey {
   }
 
   object DIKey {
-    def get[K: Tag]: TypeKey = TypeKey(SafeType.get[K])
+    def get[T: Tag]: DIKey.TypeKey = TypeKey(SafeType.get[T])
 
     sealed trait BasicKey extends DIKey {
       def withTpe(tpe: SafeType): DIKey.BasicKey
     }
 
     case class TypeKey(tpe: SafeType) extends BasicKey {
-      override def toString: String = s"{type.${tpe.toString}}"
-
       final def named[I: IdContract](id: I): IdKey[I] = IdKey(tpe, id)
 
       override final def withTpe(tpe: SafeType): DIKey.TypeKey = copy(tpe = tpe)
+      override final def toString: String = s"{type.${tpe.toString}}"
     }
 
     case class IdKey[I: IdContract](tpe: SafeType, id: I) extends BasicKey {
       final val idContract: IdContract[I] = implicitly
 
-      override def toString: String = s"{type.${tpe.toString}@${idContract.repr(id)}}"
-
       override final def withTpe(tpe: SafeType): DIKey.IdKey[I] = copy(tpe = tpe)
+      override final def toString: String = s"{type.${tpe.toString}@${idContract.repr(id)}}"
     }
 
     case class ProxyElementKey(proxied: DIKey, tpe: SafeType) extends DIKey {
-      override def toString: String = s"{proxy.${proxied.toString}}"
+      override final def toString: String = s"{proxy.${proxied.toString}}"
     }
 
     /**
@@ -44,9 +42,9 @@ trait WithDIKey {
       * @param reference Key of `this` individual element. `reference.tpe` must be a subtype of `T`
       */
     case class SetElementKey(set: DIKey, reference: DIKey, disambiguator: Option[ImplDef]) extends DIKey {
-      override def tpe: SafeType = reference.tpe
+      override final def tpe: SafeType = reference.tpe
 
-      override def toString: String = s"{set.$set/${reference.toString}#${disambiguator.fold("0")(_.hashCode().toString)}"
+      override final def toString: String = s"{set.$set/${reference.toString}#${disambiguator.fold("0")(_.hashCode().toString)}"
     }
 
     case class MultiSetImplId(set: DIKey, impl: ImplDef)
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/reflection/SymbolIntrospector.scala b/distage/distage-model/src/main/scala/izumi/distage/model/reflection/SymbolIntrospector.scala
index a8a458f97..f9715f653 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/reflection/SymbolIntrospector.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/reflection/SymbolIntrospector.scala
@@ -8,25 +8,17 @@ trait SymbolIntrospector {
   case class SelectedConstructor(constructorSymbol: u.MethodSymb, arguments: List[List[u.SymbolInfo]])
 
   def hasConstructor(tpe: u.SafeType): Boolean
-
   def selectConstructorMethod(tpe: u.SafeType): Option[u.MethodSymb]
-
   def selectConstructor(tpe: u.SafeType): Option[SelectedConstructor]
-
   def selectNonImplicitParameters(symb: u.MethodSymb): List[List[u.Symb]]
 
   def isConcrete(tpe: u.SafeType): Boolean
-
   def isWireableAbstract(tpe: u.SafeType): Boolean
-
   def isFactory(tpe: u.SafeType): Boolean
-
   def isWireableMethod(tpe: u.SafeType, decl: u.Symb): Boolean
-
   def isFactoryMethod(tpe: u.SafeType, decl: u.Symb): Boolean
 
   def findSymbolAnnotation(annType: u.SafeType, symb: u.SymbolInfo): Option[u.u.Annotation]
-
   def findTypeAnnotation(annType: u.SafeType, tpe: u.SafeType): Option[u.u.Annotation]
 
   def canBeProxied(tpe: u.SafeType): Boolean = {
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/reflection/universe/DIUniverseBase.scala b/distage/distage-model/src/main/scala/izumi/distage/model/reflection/universe/DIUniverseBase.scala
index a5b6d488b..f9f99f6ca 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/reflection/universe/DIUniverseBase.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/reflection/universe/DIUniverseBase.scala
@@ -8,5 +8,4 @@ trait DIUniverseBase extends UniverseGeneric {
   type TypeNative = u.Type
   type Symb = u.Symbol
   type MethodSymb = u.MethodSymbol
-
 }
diff --git a/distage/distage-plugins/src/main/scala/izumi/distage/plugins/load/PluginLoader.scala b/distage/distage-plugins/src/main/scala/izumi/distage/plugins/load/PluginLoader.scala
index 4b36c4861..6748d8231 100644
--- a/distage/distage-plugins/src/main/scala/izumi/distage/plugins/load/PluginLoader.scala
+++ b/distage/distage-plugins/src/main/scala/izumi/distage/plugins/load/PluginLoader.scala
@@ -7,16 +7,15 @@ trait PluginLoader {
 }
 
 object PluginLoader {
-  /**
-    * Create a [[PluginLoader]] that scans classpath according to [[PluginConfig]]
-    * */
+  /** Create a [[PluginLoader]] that scans classpath according to [[PluginConfig]] */
   def apply(pluginConfig: PluginConfig): PluginLoader = new PluginLoaderDefaultImpl(pluginConfig)
 
-  /**
-    * Create a [[PluginLoader]] that simply returns specified plugins
-    */
+  /** Create a [[PluginLoader]] that simply returns specified plugins */
   def apply(plugins: Seq[PluginBase]): PluginLoader = new PluginLoaderPredefImpl(plugins)
 
+  /** Create a [[PluginLoader]] that simply returns specified plugin */
+  def apply(plugins: PluginBase): PluginLoader = new PluginLoaderPredefImpl(Seq(plugins))
+
   def empty: PluginLoader = PluginLoaderNullImpl
 
   final case class PluginConfig
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/monadic/modules/CatsDIEffectModule.scala b/distage/distage-roles/src/main/scala/izumi/distage/monadic/modules/CatsDIEffectModule.scala
index 2c5db265f..b99d537e1 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/monadic/modules/CatsDIEffectModule.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/monadic/modules/CatsDIEffectModule.scala
@@ -9,10 +9,11 @@ import izumi.distage.monadic.modules.CatsDIEffectModule.PublicIOApp
 trait CatsDIEffectModule extends ModuleDef {
   addImplicit[DIEffectRunner[IO]]
   addImplicit[DIEffect[IO]]
+
   make[DIEffectAsync[IO]].from {
     (P0: Parallel[IO], T0: Timer[IO]) =>
-      implicit val P = P0
-      implicit val T = T0
+      implicit val P: Parallel[IO] = P0
+      implicit val T: Timer[IO] = T0
       DIEffectAsync[IO]
   }
 
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/BootstrapConfig.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/BootstrapConfig.scala
index a03e3e072..d08986413 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/BootstrapConfig.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/BootstrapConfig.scala
@@ -2,11 +2,10 @@ package izumi.distage.roles
 
 import distage.plugins.PluginConfig
 
-case class BootstrapConfig(
-                            pluginConfig: PluginConfig,
-                            bootstrapPluginConfig: Option[PluginConfig],
-                          )
-
+final case class BootstrapConfig(
+                                  pluginConfig: PluginConfig,
+                                  bootstrapPluginConfig: Option[PluginConfig],
+                                )
 object BootstrapConfig {
   def apply(pluginConfig: PluginConfig, bootstrapPluginConfig: Option[PluginConfig] = None): BootstrapConfig = {
     new BootstrapConfig(pluginConfig, bootstrapPluginConfig)
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/RoleAppLauncher.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/RoleAppLauncher.scala
index 5a6789837..51d5642e9 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/RoleAppLauncher.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/RoleAppLauncher.scala
@@ -30,6 +30,10 @@ import scala.concurrent.ExecutionContext
 import scala.concurrent.ExecutionContext.global
 import scala.reflect.ClassTag
 
+trait RoleAppLauncher {
+  def launch(parameters: RawAppArgs): Unit
+}
+
 /**
   * Application flow:
   * 1. Parse commandline parameters
@@ -43,27 +47,23 @@ import scala.reflect.ClassTag
   * 9. Build plan for DIEffect runner
   * 10. Build plan for integration checks
   * 11. Build plan for application
-  * 12. Run roles
-  * 13. Run services
+  * 12. Run role tasks
+  * 13. Run role services
   * 14. Await application termination
-  * 15. Close autocloseables
+  * 15. Run finalizers
   * 16. Shutdown executors
   */
-abstract class RoleAppLauncher[F[_] : TagK : DIEffect] {
-
-  private val loggers = new EarlyLoggers()
-
-  protected def bootstrapConfig: BootstrapConfig
-
-  protected val hook: AppShutdownStrategy[F]
+abstract class RoleAppLauncherImpl[F[_]: TagK: DIEffect] extends RoleAppLauncher {
+  protected def pluginSource: PluginSource
+  protected def shutdownStrategy: AppShutdownStrategy[F]
 
   protected def referenceLibraryInfo: Seq[LibraryReference] = Vector.empty
 
-  final def launch(parameters: RawAppArgs): Unit = {
-    val earlyLogger = loggers.makeEarlyLogger(parameters)
+  def launch(parameters: RawAppArgs): Unit = {
+    val earlyLogger = EarlyLoggers.makeEarlyLogger(parameters)
     showBanner(earlyLogger, referenceLibraryInfo)
 
-    val plugins = makePluginLoader(bootstrapConfig).load()
+    val plugins = pluginSource.load()
     val roles = loadRoles(parameters, earlyLogger, plugins)
 
     // default PlanMergingPolicy will be applied to bootstrap module, so any non-trivial conflict in bootstrap bindings will fail the app
@@ -72,7 +72,7 @@ abstract class RoleAppLauncher[F[_] : TagK : DIEffect] {
     earlyLogger.info(s"Loaded ${defBs.bindings.size -> "bootstrap bindings"}...")
 
     val config = makeConfigLoader(earlyLogger, parameters).buildConfig()
-    val lateLogger = loggers.makeLateLogger(parameters, earlyLogger, config)
+    val lateLogger = EarlyLoggers.makeLateLogger(parameters, earlyLogger, config)
 
     val mergeStrategy = makeMergeStrategy(lateLogger, parameters, roles)
     val defApp = mergeStrategy.merge(plugins.app)
@@ -91,9 +91,10 @@ abstract class RoleAppLauncher[F[_] : TagK : DIEffect] {
     val appModule = moduleProvider.appModules().merge overridenBy defApp overridenBy appOverride
     val planner = makePlanner(options, bsModule, activation, lateLogger)
     val appPlan = planner.makePlan(roots, appModule)
-    lateLogger.info(s"Planning finished. ${appPlan.app.keys.size -> "main ops"}, ${appPlan.integration.keys.size -> "integration ops"}, ${appPlan.runtime.keys.size -> "runtime ops"}")
+    lateLogger.info(s"Planning finished. ${appPlan.app.primary.plan.keys.size -> "main ops"}, ${appPlan.app.side.plan.keys.size -> "integration ops"}, ${appPlan.app.shared.plan.keys.size -> "shared ops"}, ${appPlan.runtime.keys.size -> "runtime ops"}")
 
-    val r = makeExecutor(parameters, roles, lateLogger, appPlan.injector)
+    val injector = appPlan.injector
+    val r = makeExecutor(parameters, roles, lateLogger, injector, makeStartupExecutor(lateLogger, injector))
     r.runPlan(appPlan)
   }
 
@@ -128,8 +129,12 @@ abstract class RoleAppLauncher[F[_] : TagK : DIEffect] {
     new RoleAppPlanner.Impl[F](options, bsModule, activation, lateLogger)
   }
 
-  protected def makeExecutor(parameters: RawAppArgs, roles: RolesInfo, lateLogger: IzLogger, injector: Injector): RoleAppExecutor[F] = {
-    new RoleAppExecutor.Impl[F](hook, roles, injector, lateLogger, parameters)
+  protected def makeExecutor(parameters: RawAppArgs, roles: RolesInfo, lateLogger: IzLogger, injector: Injector, startupPlanExecutor: StartupPlanExecutor[F]): RoleAppExecutor[F] = {
+    new RoleAppExecutor.Impl[F](shutdownStrategy, roles, injector, lateLogger, parameters, startupPlanExecutor)
+  }
+
+  protected def makeStartupExecutor(lateLogger: IzLogger, injector: Injector): StartupPlanExecutor[F] = {
+    StartupPlanExecutor.default(lateLogger, injector)
   }
 
   protected def makeModuleProvider(options: ContextOptions, parameters: RawAppArgs, activation: AppActivation, roles: RolesInfo, config: AppConfig, lateLogger: IzLogger): ModuleProvider[F] = {
@@ -214,10 +219,6 @@ abstract class RoleAppLauncher[F[_] : TagK : DIEffect] {
     }
   }
 
-  protected def makePluginLoader(bootstrapConfig: BootstrapConfig): PluginSource = {
-    new PluginSource.Impl(bootstrapConfig)
-  }
-
   protected def makeConfigLoader(logger: IzLogger, parameters: RawAppArgs): ConfigLoader = {
     val maybeGlobalConfig = Options.configParam.findValue(parameters.globalParameters).asFile
 
@@ -240,16 +241,16 @@ object RoleAppLauncher {
     final val use = arg("use", "u", "activate a choice on functionality axis", "<axis>:<choice>")
   }
 
-  abstract class LauncherF[F[_] : TagK : DIEffect : LiftIO](executionContext: ExecutionContext = global) extends RoleAppLauncher[F] {
-    override protected val hook: AppShutdownStrategy[F] = new CatsEffectIOShutdownStrategy(executionContext)
+  abstract class LauncherF[F[_]: TagK: DIEffect: LiftIO](executionContext: ExecutionContext = global) extends RoleAppLauncherImpl[F] {
+    override protected val shutdownStrategy: AppShutdownStrategy[F] = new CatsEffectIOShutdownStrategy(executionContext)
   }
 
-  abstract class LauncherBIO[F[+_, +_]: BIOAsync: BIOPrimitives](implicit tagK: TagK[F[Throwable, ?]]) extends RoleAppLauncher[F[Throwable, ?]] {
-    override protected val hook: AppShutdownStrategy[F[Throwable, ?]] = new BIOShutdownStrategy[F]
+  abstract class LauncherBIO[F[+_, +_]: BIOAsync: BIOPrimitives](implicit tagK: TagK[F[Throwable, ?]]) extends RoleAppLauncherImpl[F[Throwable, ?]] {
+    override protected val shutdownStrategy: AppShutdownStrategy[F[Throwable, ?]] = new BIOShutdownStrategy[F]
   }
 
-  abstract class LauncherIdentity extends RoleAppLauncher[Identity] {
-    override protected val hook: AppShutdownStrategy[Identity] = new JvmExitHookLatchShutdownStrategy
+  abstract class LauncherIdentity extends RoleAppLauncherImpl[Identity] {
+    override protected val shutdownStrategy: AppShutdownStrategy[Identity] = new JvmExitHookLatchShutdownStrategy
   }
 
 }
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/RoleAppMain.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/RoleAppMain.scala
index a44117971..80b3e5aee 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/RoleAppMain.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/RoleAppMain.scala
@@ -1,16 +1,17 @@
 package izumi.distage.roles
 
-import izumi.distage.model.monadic.DIEffect
 import izumi.distage.roles.services.AppFailureHandler
 import izumi.fundamentals.platform.cli.model.raw.{RawAppArgs, RawRoleParams}
 import izumi.fundamentals.platform.cli.{CLIParser, ParserFailureHandler}
-import distage.TagK
 
-abstract class RoleAppMain[F[_] : TagK : DIEffect](
-                                                    launcher: RoleAppLauncher[F],
-                                                    failureHandler: AppFailureHandler,
-                                                    parserFailureHandler: ParserFailureHandler,
-                                                  ) {
+abstract class RoleAppMain
+(
+  launcher: RoleAppLauncher,
+  failureHandler: AppFailureHandler,
+  parserFailureHandler: ParserFailureHandler,
+) {
+  protected def parse(args: Array[String]): Either[CLIParser.ParserError, RawAppArgs] = new CLIParser().parse(args)
+  protected def requiredRoles: Vector[RawRoleParams] = Vector.empty
 
   def main(args: Array[String]): Unit = {
     try {
@@ -28,29 +29,25 @@ abstract class RoleAppMain[F[_] : TagK : DIEffect](
         failureHandler.onError(t)
     }
   }
-
-  protected def parse(args: Array[String]): Either[CLIParser.ParserError, RawAppArgs] = new CLIParser().parse(args)
-
-  protected def requiredRoles: Vector[RawRoleParams] = Vector.empty
 }
 
 object RoleAppMain {
 
-  abstract class Default[F[_] : TagK : DIEffect](launcher: RoleAppLauncher[F])
+  class Default(launcher: RoleAppLauncher)
     extends RoleAppMain(
       launcher,
       AppFailureHandler.TerminatingHandler,
       ParserFailureHandler.TerminatingHandler,
     )
 
-  abstract class Safe[F[_] : TagK : DIEffect](launcher: RoleAppLauncher[F])
+  class Safe(launcher: RoleAppLauncher)
     extends RoleAppMain(
       launcher,
       AppFailureHandler.PrintingHandler,
       ParserFailureHandler.PrintingHandler
     )
 
-  abstract class Silent[F[_] : TagK : DIEffect](launcher: RoleAppLauncher[F])
+  class Silent(launcher: RoleAppLauncher)
     extends RoleAppMain(
       launcher,
       AppFailureHandler.NullHandler,
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/ConfigWriter.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/ConfigWriter.scala
index 0dce6f77b..d2876143d 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/ConfigWriter.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/ConfigWriter.scala
@@ -18,7 +18,7 @@ import izumi.distage.roles.config.ContextOptions
 import izumi.distage.roles.services.RoleAppPlanner
 import izumi.fundamentals.platform.cli.model.raw.RawEntrypointParams
 import izumi.fundamentals.platform.cli.model.schema.{ParserDef, RoleParserSchema}
-import izumi.fundamentals.platform.language.Quirks
+import izumi.fundamentals.platform.language.{Quirks, unused}
 import izumi.fundamentals.platform.resources.ArtifactVersion
 import izumi.logstage.api.IzLogger
 import izumi.logstage.api.logger.LogRouter
@@ -37,8 +37,7 @@ class ConfigWriter[F[_] : DIEffect]
 )
   extends RoleTask[F] {
 
-  override def start(roleParameters: RawEntrypointParams, freeArgs: Vector[String]): F[Unit] = {
-    Quirks.discard(freeArgs)
+  override def start(roleParameters: RawEntrypointParams, @unused freeArgs: Vector[String]): F[Unit] = {
     val config = ConfigWriter.parse(roleParameters)
     DIEffect[F].maybeSuspend(writeReferenceConfig(config))
   }
@@ -139,9 +138,10 @@ class ConfigWriter[F[_] : DIEffect]
       getConfig(plan) getOrElse ConfigFactory.empty()
     }
 
-    if (plans.app.steps.exists(_.target == roleDIKey)) {
-      getConfig(plans.app)
-        .map(_.withFallback(getConfigOrEmpty(plans.integration)))
+    if (plans.app.primary.plan.steps.exists(_.target == roleDIKey)) {
+      getConfig(plans.app.primary.plan)
+        .map(_.withFallback(getConfigOrEmpty(plans.app.side.plan)))
+        .map(_.withFallback(getConfigOrEmpty(plans.app.shared.plan)))
         .map(_.withFallback(getConfigOrEmpty(plans.runtime)))
     } else {
       logger.warn(s"$roleDIKey is not in the refined plan")
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/Help.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/Help.scala
index b78a24703..d0d570a47 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/Help.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/internal/Help.scala
@@ -2,11 +2,12 @@ package izumi.distage.roles.internal
 
 import izumi.distage.model.monadic.DIEffect
 import izumi.distage.roles.RoleAppLauncher
+import izumi.distage.roles.RoleAppLauncher.Options
 import izumi.distage.roles.model.meta.RolesInfo
 import izumi.distage.roles.model.{AppActivation, RoleDescriptor, RoleTask}
 import izumi.fundamentals.platform.cli.model.raw.RawEntrypointParams
 import izumi.fundamentals.platform.cli.model.schema._
-import izumi.fundamentals.platform.language.Quirks
+import izumi.fundamentals.platform.language.unused
 import izumi.fundamentals.platform.strings.IzString._
 
 class Help[F[_] : DIEffect]
@@ -15,8 +16,7 @@ class Help[F[_] : DIEffect]
   activation: AppActivation,
 ) extends RoleTask[F] {
 
-  override def start(roleParameters: RawEntrypointParams, freeArgs: Vector[String]): F[Unit] = {
-    Quirks.discard(roleParameters, freeArgs)
+  override def start(@unused roleParameters: RawEntrypointParams, @unused freeArgs: Vector[String]): F[Unit] = {
     DIEffect[F].maybeSuspend(showHelp())
   }
 
@@ -53,7 +53,7 @@ class Help[F[_] : DIEffect]
          |$activations""".stripMargin
 
     val help = ParserSchemaFormatter.makeDocs(
-      ParserSchema(GlobalArgsSchema(RoleAppLauncher.Options, Some(baseDoc), Some(notes)), descriptors)
+      ParserSchema(GlobalArgsSchema(Options, Some(baseDoc), Some(notes)), descriptors)
     )
 
     println(help)
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/logger/SimpleLoggerConfigurator.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/logger/SimpleLoggerConfigurator.scala
index 8ce2559f1..42f3c34a5 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/logger/SimpleLoggerConfigurator.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/logger/SimpleLoggerConfigurator.scala
@@ -2,6 +2,7 @@ package izumi.distage.roles.logger
 
 import com.typesafe.config.Config
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.SafeType
+import izumi.distage.roles.logger.SimpleLoggerConfigurator.SinksConfig
 import izumi.fundamentals.typesafe.config.{RuntimeConfigReaderCodecs, RuntimeConfigReaderDefaultImpl}
 import izumi.logstage.api.config.{LoggerConfig, LoggerPathConfig}
 import izumi.logstage.api.logger.LogRouter
@@ -17,8 +18,6 @@ class SimpleLoggerConfigurator(
                                 exceptionLogger: IzLogger,
                               ) {
 
-  import SimpleLoggerConfigurator._
-
   // TODO: this is a temporary solution until we finish full-scale logger configuration support
   def makeLogRouter(config: Config, root: Log.Level, json: Boolean): LogRouter = {
     val logconf = readConfig(config)
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/EarlyLoggers.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/EarlyLoggers.scala
index 2d18c37d0..f2d5c71ee 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/EarlyLoggers.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/EarlyLoggers.scala
@@ -1,17 +1,16 @@
 package izumi.distage.roles.services
 
 import izumi.distage.config.model.AppConfig
+import izumi.distage.roles.RoleAppLauncher.Options
 import izumi.distage.roles.logger.SimpleLoggerConfigurator
 import izumi.fundamentals.platform.cli.model.raw.RawAppArgs
 import izumi.logstage.api.Log.Level
 import izumi.logstage.api.{IzLogger, Log}
 
-class EarlyLoggers() {
+object EarlyLoggers {
 
-  import izumi.distage.roles.RoleAppLauncher._
-
-  private val defaultLogLevel = Log.Level.Info
-  private val defaultLogFormatJson = false
+  private[this] final val defaultLogLevel = Log.Level.Info
+  private[this] final val defaultLogFormatJson = false
 
   def makeEarlyLogger(parameters: RawAppArgs): IzLogger = {
     val rootLogLevel = getRootLogLevel(parameters)
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/IntegrationChecker.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/IntegrationChecker.scala
index 4c3983ddc..eba5ba84c 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/IntegrationChecker.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/IntegrationChecker.scala
@@ -1,70 +1,96 @@
 package izumi.distage.roles.services
 
-import distage.DIKey
+import distage.{DIKey, TagK}
 import izumi.distage.model.Locator
 import izumi.distage.model.exceptions.DIException
+import izumi.distage.model.monadic.{DIEffect, DIEffectAsync}
 import izumi.distage.roles.model.{DiAppBootstrapException, IntegrationCheck}
 import izumi.distage.roles.services.IntegrationChecker.IntegrationCheckException
 import izumi.fundamentals.platform.integration.ResourceCheck
 import izumi.fundamentals.platform.strings.IzString._
 import izumi.logstage.api.IzLogger
+import DIEffect.syntax._
 
 import scala.util.control.NonFatal
 
-trait IntegrationChecker {
-  def check(integrationComponents: Set[DIKey], integrationLocator: Locator): Option[Seq[ResourceCheck.Failure]]
+trait IntegrationChecker[F[_]] {
+  def collectFailures(integrationComponents: Set[DIKey], integrationLocator: Locator): F[Either[Seq[ResourceCheck.Failure], Unit]]
 
-  final def checkOrFail(integrationComponents: Set[DIKey], integrationLocator: Locator): Unit = {
-    check(integrationComponents, integrationLocator).fold(()) {
-      failures =>
-        throw new IntegrationCheckException(s"Integration check failed, failures were: ${failures.niceList()}", failures)
+  protected implicit val tag: TagK[F]
+  final def checkOrFail(integrationComponents: Set[DIKey], integrationLocator: Locator): F[Unit] = {
+    implicit val F: DIEffect[F] = integrationLocator.get[DIEffect[F]]
+
+    collectFailures(integrationComponents, integrationLocator).flatMap {
+      case Left(failures) =>
+        F.fail(new IntegrationCheckException(s"Integration check failed, failures were: ${failures.niceList()}", failures))
+      case Right(_) =>
+        F.unit
     }
   }
 }
 
 object IntegrationChecker {
+
   class IntegrationCheckException(message: String, val failures: Seq[ResourceCheck.Failure]) extends DIException(message, null)
 
-  class Impl(
-              logger: IzLogger,
-            ) extends IntegrationChecker {
-    def check(integrationComponents: Set[DIKey], integrationLocator: Locator): Option[Seq[ResourceCheck.Failure]] = {
-      val integrations = integrationComponents.map {
+  class Impl[F[_] : TagK](
+                           logger: IzLogger,
+                         ) extends IntegrationChecker[F] {
+
+
+    override protected val tag: TagK[F] = implicitly[TagK[F]]
+
+    override def collectFailures(integrationComponents: Set[DIKey], integrationLocator: Locator): F[Either[Seq[ResourceCheck.Failure], Unit]] = {
+      logger.info(s"Going to check availability of ${integrationComponents.size -> "resources"}")
+
+      implicit val F: DIEffect[F] = integrationLocator.get[DIEffect[F]]
+      implicit val P: DIEffectAsync[F] = integrationLocator.get[DIEffectAsync[F]]
+
+      val instances = integrationComponents.toList.map {
         ick =>
-          integrationLocator.lookup[IntegrationCheck](ick) match {
-            case Some(ic) =>
-              ic.value
-            case None =>
-              throw new DiAppBootstrapException(s"Inconsistent locator state: integration component $ick is missing from plan")
+          ick -> integrationLocator.lookupInstance[Any](ick).map(_.asInstanceOf[IntegrationCheck])
+      }.toSet
+
+      val good = instances.collect { case (_, Some(ic)) => ic }
+      val bad = instances.collect { case (ick, None) => ick }
+
+      if (bad.isEmpty) {
+        P
+          .parTraverse[IntegrationCheck, Either[ResourceCheck.Failure, Unit]](good)(runCheck)
+          .flatMap {
+            results =>
+              results.collect({ case Left(failure) => failure }) match {
+                case Nil =>
+                  F.pure(Right(()))
+                case failures =>
+                  F.pure(Left(failures))
+              }
           }
+      } else {
+        F.fail(new DiAppBootstrapException(s"Inconsistent locator state: integration components ${bad.mkString("{", ", ", "}")} are missing from locator"))
       }
-      failingIntegrations(integrations)
     }
 
-    private def failingIntegrations(integrations: Set[IntegrationCheck]): Option[Seq[ResourceCheck.Failure]] = {
-      logger.info(s"Going to check availability of ${integrations.size -> "resources"}")
-
-      val failures = integrations.toSeq.flatMap {
-        resource =>
-          logger.debug(s"Checking $resource")
-          try {
-            resource.resourcesAvailable() match {
-              case failure@ResourceCheck.ResourceUnavailable(reason, Some(cause)) =>
-                logger.debug(s"Integration check failed, $resource unavailable: $reason, $cause")
-                Some(failure)
-              case failure@ResourceCheck.ResourceUnavailable(reason, None) =>
-                logger.debug(s"Integration check failed, $resource unavailable: $reason")
-                Some(failure)
-              case ResourceCheck.Success() =>
-                None
-            }
-          } catch {
-            case NonFatal(exception) =>
-              logger.error(s"Integration check for $resource threw $exception")
-              Some(ResourceCheck.ResourceUnavailable(exception.getMessage, Some(exception)))
+    private def runCheck(resource: IntegrationCheck)(implicit F: DIEffect[F], P: DIEffectAsync[F]): F[Either[ResourceCheck.Failure, Unit]] = {
+      F.maybeSuspend {
+        logger.debug(s"Checking $resource")
+        try {
+          resource.resourcesAvailable() match {
+            case failure@ResourceCheck.ResourceUnavailable(reason, Some(cause)) =>
+              logger.debug(s"Integration check failed, $resource unavailable: $reason, $cause")
+              Left(failure)
+            case failure@ResourceCheck.ResourceUnavailable(reason, None) =>
+              logger.debug(s"Integration check failed, $resource unavailable: $reason")
+              Left(failure)
+            case ResourceCheck.Success() =>
+              Right(())
           }
+        } catch {
+          case NonFatal(exception) =>
+            logger.error(s"Integration check for $resource threw $exception")
+            Left(ResourceCheck.ResourceUnavailable(exception.getMessage, Some(exception)))
+        }
       }
-      Some(failures).filter(_.nonEmpty)
     }
   }
 
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/ModuleProvider.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/ModuleProvider.scala
index 4dace5997..d5040f625 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/ModuleProvider.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/ModuleProvider.scala
@@ -23,7 +23,8 @@ trait ModuleProvider[F[_]] {
 
 object ModuleProvider {
 
-  class Impl[F[_]: TagK](
+  class Impl[F[_]: TagK]
+  (
     logger: IzLogger,
     config: AppConfig,
     roles: RolesInfo,
@@ -60,7 +61,7 @@ object ModuleProvider {
           resourceRewriter,
           loggerModule,
         ),
-        condModule(options.addGraphVizDump, new GraphDumpBootstrapModule())
+        if (options.addGraphVizDump) Seq(new GraphDumpBootstrapModule()) else Seq.empty,
       ).flatten
     }
 
@@ -70,10 +71,6 @@ object ModuleProvider {
       Seq(baseMod)
     }
 
-    private def condModule(condition: Boolean, module: => distage.BootstrapModuleDef): Seq[BootstrapModuleDef] = {
-      if (condition) Seq(module) else Seq.empty
-    }
-
   }
 
 }
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/PluginSource.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/PluginSource.scala
index 71a638c0e..3493a4830 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/PluginSource.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/PluginSource.scala
@@ -1,33 +1,45 @@
 package izumi.distage.roles.services
 
-import distage.plugins.PluginBase
+import distage.plugins.{PluginBase, PluginConfig}
 import izumi.distage.plugins.load.PluginLoader
 import izumi.distage.roles.BootstrapConfig
 import izumi.distage.roles.services.PluginSource.AllLoadedPlugins
 
 trait PluginSource {
   def load(): AllLoadedPlugins
+
+  final def map(f: AllLoadedPlugins => AllLoadedPlugins): PluginSource = () => f(load())
 }
 
 object PluginSource {
-  case class AllLoadedPlugins(bootstrap: Seq[PluginBase], app: Seq[PluginBase])
-
-  class Impl(
-              config: BootstrapConfig,
-            ) extends PluginSource {
-
-    def load(): AllLoadedPlugins = {
-      val bsl = mkBootstrapLoader().load()
-      val l = mkLoader().load()
-      AllLoadedPlugins(bsl, l)
-    }
-
-    protected def mkBootstrapLoader(): PluginLoader = {
-      config.bootstrapPluginConfig.fold(PluginLoader.empty)(PluginLoader(_))
-    }
+  def apply(bootstrapConfig: BootstrapConfig): PluginSource = {
+    PluginSource(bootstrapConfig.pluginConfig, bootstrapConfig.bootstrapPluginConfig)
+  }
+  def apply(pluginConfig: PluginConfig, bootstrapPluginConfig: Option[PluginConfig] = None): PluginSource = {
+    new Impl(PluginLoader(pluginConfig), bootstrapPluginConfig.fold(PluginLoader.empty)(PluginLoader(_)))
+  }
+  def apply(pluginLoader: PluginLoader, bootstrapPluginLoader: PluginLoader): PluginSource = {
+    new Impl(pluginLoader, bootstrapPluginLoader)
+  }
+  def apply(pluginLoader: PluginLoader): PluginSource = {
+    new Impl(pluginLoader, PluginLoader.empty)
+  }
+  def apply(allLoadedPlugins: AllLoadedPlugins): PluginSource = {
+    () => allLoadedPlugins
+  }
+  def apply(plugins: Seq[PluginBase], bootstrapPlugins: Seq[PluginBase]): PluginSource = {
+    PluginSource(AllLoadedPlugins(plugins, bootstrapPlugins))
+  }
+  def apply(plugins: Seq[PluginBase]): PluginSource = {
+    PluginSource(plugins, Seq.empty)
+  }
 
-    protected def mkLoader(): PluginLoader = {
-      PluginLoader(config.pluginConfig)
-    }
+  final class Impl(pluginLoader: PluginLoader, bootstrapPluginLoader: PluginLoader) extends PluginSource {
+    def load(): AllLoadedPlugins = AllLoadedPlugins(bootstrapPluginLoader.load(), pluginLoader.load())
   }
+
+  final case class AllLoadedPlugins(
+                                     bootstrap: Seq[PluginBase],
+                                     app: Seq[PluginBase],
+                                   )
 }
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/PruningPlanMergingPolicy.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/PruningPlanMergingPolicy.scala
index 080ac1d24..eb8b0e4d1 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/PruningPlanMergingPolicy.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/PruningPlanMergingPolicy.scala
@@ -8,6 +8,7 @@ import izumi.distage.model.reflection.universe.RuntimeDIUniverse
 import izumi.distage.planning.PlanMergingPolicyDefaultImpl
 import izumi.distage.planning.gc.TracingDIGC
 import izumi.distage.roles.model.AppActivation
+import izumi.fundamentals.platform.strings.IzString._
 import izumi.logstage.api.IzLogger
 
 class PruningPlanMergingPolicy(
@@ -16,26 +17,20 @@ class PruningPlanMergingPolicy(
                               ) extends PlanMergingPolicyDefaultImpl {
   private val activeTags = activation.active.values.toSet
 
-  import izumi.fundamentals.platform.strings.IzString._
-
   override protected def resolveConflict(plan: DodgyPlan, key: RuntimeDIUniverse.DIKey, operations: Set[DodgyPlan.JustOp]): DIKeyConflictResolution = {
     assert(operations.size > 1)
-    val filtered = operations
-      .filter {
-        op =>
-          op.binding.tags
-            .collect({
-              case BindingTag.AxisTag(t) => t
-            })
-            .forall(t => activeTags.contains(t))
-      }
+    val filtered = operations.filter {
+      _.binding.tags
+        .collect { case BindingTag.AxisTag(t) => t }
+        .forall(t => activeTags.contains(t))
+    }
 
     val (explicitlyEnabled, noTags) = filtered.partition(_.binding.tags.nonEmpty)
 
     if (explicitlyEnabled.size == 1) {
       if (noTags.nonEmpty) {
-        logger.info(s"Untagged conflicts were filtered out in $key: ${noTags.niceList() -> "filtered conflicts"}")
-        logger.warn(s"Untagged alternatives for conflict in $key were filtered out, continuing...")
+        logger.debug(s"Untagged conflicts were filtered out in $key: ${noTags.niceList() -> "filtered conflicts"}")
+        //logger.warn(s"Untagged alternatives for conflict in $key were filtered out, continuing...")
       }
       DIKeyConflictResolution.Successful(explicitlyEnabled.map(_.op: ExecutableOp))
     } else if (noTags.size == 1) {
@@ -83,8 +78,8 @@ class PruningPlanMergingPolicy(
           case (k, DIKeyConflictResolution.Successful(s)) => k -> s
         })
         val erased = good.filter(_._2.isEmpty)
-        logger.info(s"${erased.keys.niceList() -> "erased conflicts"}")
-        logger.warn(s"Pruning strategy successfully resolved ${issues.size -> "conlicts"}, ${erased.size -> "erased"}, continuing...")
+        logger.debug(s"Pruning strategy successfully resolved ${issues.size -> "conlicts"}, ${erased.size -> "erased"}, ${erased.keys.niceList() -> "erased conflicts"}")
+        //logger.warn(s"Pruning strategy successfully resolved ${issues.size -> "conlicts"}, ${erased.size -> "erased"}, continuing...")
         val allResolved = (resolved.values.flatten ++ good.values.flatten).toVector
         SemiPlan(plan.definition, allResolved, plan.gcMode)
       }
@@ -97,14 +92,14 @@ class PruningPlanMergingPolicy(
     ops
       .toSeq
       .map {
-        f =>
-          val bindingTags = f.binding.tags.collect({
+        op =>
+          val bindingTags = op.binding.tags.collect({
             case BindingTag.AxisTag(t) => t
           }).diff(activeTags)
-          val alreadyActiveTags = f.binding.tags.collect({
+          val alreadyActiveTags = op.binding.tags.collect({
             case BindingTag.AxisTag(t) => t
           }).intersect(activeTags)
-          s"${f.binding.origin}, possible: {${bindingTags.mkString(", ")}}, active: {${alreadyActiveTags.mkString(", ")}}"
+          s"${op.binding.origin}, possible: {${bindingTags.mkString(", ")}}, active: {${alreadyActiveTags.mkString(", ")}}"
       }
   }
 }
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/ResourceRewriter.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/ResourceRewriter.scala
index cf6f10ca6..f48e4c734 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/ResourceRewriter.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/ResourceRewriter.scala
@@ -27,9 +27,7 @@ class ResourceRewriter(
       defn
         .flatMap(rewrite[AutoCloseable](fromAutoCloseable(logger, _)))
         .flatMap(rewrite[ExecutorService](fromExecutorService(logger, _)))
-    } else {
-      defn
-    }
+    } else defn
   }
 
   private def rewrite[TGT: Tag](convert: TGT => DIResource[Identity, TGT])(b: Binding): Seq[Binding] = {
@@ -37,7 +35,7 @@ class ResourceRewriter(
       case implBinding: Binding.ImplBinding =>
         implBinding match {
           case binding: Binding.SingletonBinding[_] =>
-            rewriteImpl(convert, binding.key, binding.origin, binding.implementation, set = false) match {
+            rewriteImpl(convert, binding.key, binding.origin, binding.implementation, isSet = false) match {
               case ReplaceImpl(newImpl) =>
                 logger.info(s"Adapting ${binding.key} defined at ${binding.origin} as ${SafeType.get[TGT] -> "type"}")
                 Seq(finish(binding, newImpl))
@@ -49,7 +47,7 @@ class ResourceRewriter(
             }
 
           case binding: Binding.SetElementBinding =>
-            rewriteImpl(convert, binding.key, binding.origin, binding.implementation, set = true) match {
+            rewriteImpl(convert, binding.key, binding.origin, binding.implementation, isSet = true) match {
               case ReplaceImpl(newImpl) =>
                 logger.info(s"Adapting set element ${binding.key} defined at ${binding.origin} as ${SafeType.get[TGT] -> "type"}")
                 Seq(finish(binding, newImpl))
@@ -67,7 +65,7 @@ class ResourceRewriter(
     }
   }
 
-  private def rewriteImpl[TGT: Tag](convert: TGT => DIResource[Identity, TGT], key: DIKey, origin: SourceFilePosition, implementation: ImplDef, set: Boolean): RewriteResult = {
+  private def rewriteImpl[TGT: Tag](convert: TGT => DIResource[Identity, TGT], key: DIKey, origin: SourceFilePosition, implementation: ImplDef, isSet: Boolean): RewriteResult = {
     implementation match {
       case implDef: ImplDef.DirectImplDef =>
         val implType = implDef.implType
@@ -79,9 +77,7 @@ class ResourceRewriter(
               DontChange
 
             case _: ImplDef.InstanceImpl =>
-              if (rules.warnOnExternal) {
-                logger.warn(s"External entity $key defined at $origin is <:< ${SafeType.get[TGT] -> "type"}, but it will NOT be finalized!!! Because it's not an explicit DIResource")
-              }
+              logger.warn(s"Instance binding for $key defined at $origin is <:< ${SafeType.get[TGT] -> "type"}, but it will NOT be finalized, because we assume it's defined for outer scope!!! Because it's not an explicit DIResource (define as function binding to force conversion)")
               DontChange
 
             case ImplDef.ProviderImpl(_, function) =>
@@ -92,7 +88,7 @@ class ResourceRewriter(
               val implTypeKey = DIKey.TypeKey(implType)
               val newKey = DIKey.IdKey(
                 tpe = implType,
-                id = ResId(if (set) DIKey.SetElementKey(key, implTypeKey, Some(implDef)) else implTypeKey)
+                id = ResId(if (isSet) DIKey.SetElementKey(key, implTypeKey, Some(implDef)) else implTypeKey)
               )
 
               val parameter = {
@@ -158,10 +154,7 @@ object ResourceRewriter {
     implicit val idContract: IdContract[ResId] = new RuntimeDIUniverse.IdContractImpl[ResId]
   }
 
-  final case class RewriteRules(
-                                 applyRewrites: Boolean = true,
-                                 warnOnExternal: Boolean = true,
-                               )
+  final case class RewriteRules(applyRewrites: Boolean = true)
 
   /** Like [[DIResource.fromAutoCloseable]], but with added logging */
   def fromAutoCloseable[A <: AutoCloseable](logger: IzLogger, acquire: => A): DIResource[Identity, A] = {
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppExecutor.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppExecutor.scala
index 3c7df8dd9..1fd4bbe9d 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppExecutor.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppExecutor.scala
@@ -18,26 +18,23 @@ trait RoleAppExecutor[F[_]] {
 
 object RoleAppExecutor {
 
-  class Impl[F[_] : TagK](
-                           protected val hook: AppShutdownStrategy[F],
-                           roles: RolesInfo,
-                           injector: Injector,
-                           lateLogger: IzLogger,
-                           parameters: RawAppArgs,
-                         ) extends RoleAppExecutor[F] {
+  class Impl[F[_]: TagK](
+                          hook: AppShutdownStrategy[F],
+                          roles: RolesInfo,
+                          injector: Injector,
+                          lateLogger: IzLogger,
+                          parameters: RawAppArgs,
+                          startupPlanExecutor: StartupPlanExecutor[F],
+                        ) extends RoleAppExecutor[F] {
 
     final def runPlan(appPlan: AppStartupPlans): Unit = {
       try {
-        makeStartupExecutor().execute(appPlan, Filters.all[F])(doRun)
+        startupPlanExecutor.execute(appPlan, Filters.all[F])(doRun)
       } finally {
         hook.release()
       }
     }
 
-    protected def makeStartupExecutor(): StartupPlanExecutor = {
-      StartupPlanExecutor.default(lateLogger, injector)
-    }
-
     protected def doRun(locator: Locator, effect: DIEffect[F]): F[Unit] = {
       val roleIndex = getRoleIndex(locator)
       implicit val e: DIEffect[F] = effect
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppPlanner.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppPlanner.scala
index 1db05e29e..a6cdb6941 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppPlanner.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/RoleAppPlanner.scala
@@ -1,8 +1,9 @@
 package izumi.distage.roles.services
 
 import distage.{BootstrapModule, DIKey, Injector, TagK, _}
+import izumi.distage.model.TriSplittedPlan
 import izumi.distage.model.definition.{ModuleBase, ModuleDef}
-import izumi.distage.model.monadic.{DIEffect, DIEffectRunner}
+import izumi.distage.model.monadic.{DIEffect, DIEffectAsync, DIEffectRunner}
 import izumi.distage.model.plan.OrderedPlan
 import izumi.distage.roles.config.ContextOptions
 import izumi.distage.roles.model.{AppActivation, IntegrationCheck}
@@ -17,18 +18,16 @@ trait RoleAppPlanner[F[_]] {
 object RoleAppPlanner {
   case class AppStartupPlans(
                               runtime: OrderedPlan,
-                              integration: OrderedPlan,
-                              integrationKeys: Set[DIKey],
-                              app: OrderedPlan,
+                              app: TriSplittedPlan,
                               injector: Injector,
                             )
 
-  class Impl[F[_] : TagK](
-                           options: ContextOptions,
-                           bsModule: BootstrapModule,
-                           activation: AppActivation,
-                           logger: IzLogger,
-                         ) extends RoleAppPlanner[F] { self =>
+  class Impl[F[_]: TagK](
+                          options: ContextOptions,
+                          bsModule: BootstrapModule,
+                          activation: AppActivation,
+                          logger: IzLogger,
+                        ) extends RoleAppPlanner[F] { self =>
 
     private val injector = Injector.Standard(bsModule)
 
@@ -47,23 +46,31 @@ object RoleAppPlanner {
       val runtimeGcRoots: Set[DIKey] = Set(
         DIKey.get[DIEffectRunner[F]],
         DIKey.get[DIEffect[F]],
+        DIKey.get[DIEffectAsync[F]],
       )
+
       val runtimePlan = injector.plan(PlannerInput(fullAppModule, runtimeGcRoots))
 
-      val appPlan = injector.splitPlan(fullAppModule.drop(runtimeGcRoots), appMainRoots) {
+      val appPlan = injector.triSplitPlan(fullAppModule.drop(runtimeGcRoots), appMainRoots) {
         _.collectChildren[IntegrationCheck].map(_.target).toSet
       }
 
       val check = new PlanCircularDependencyCheck(options, logger)
       check.verify(runtimePlan)
-      check.verify(appPlan.subplan)
-      check.verify(appPlan.primary)
+      check.verify(appPlan.shared.plan)
+      check.verify(appPlan.side.plan)
+      check.verify(appPlan.primary.plan)
+
+//      import izumi.fundamentals.platform.strings.IzString._
+//      logger.info(s"${fullAppModule.bindings.niceList() -> "app module"}")
+//      logger.info(s"${runtimePlan.render() -> "runtime plan"}")
+//      logger.info(s"${appPlan.shared.plan.render() -> "shared plan"}")
+//      logger.info(s"${appPlan.side.plan.render() -> "integration plan"}")
+//      logger.info(s"${appPlan.primary.plan.render() -> "primary plan"}")
 
       AppStartupPlans(
         runtimePlan,
-        appPlan.subplan,
-        appPlan.subRoots,
-        appPlan.primary,
+        appPlan,
         injector
       )
     }
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/StartupPlanExecutor.scala b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/StartupPlanExecutor.scala
index 44a0d5292..38fd5f771 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/roles/services/StartupPlanExecutor.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/roles/services/StartupPlanExecutor.scala
@@ -2,35 +2,37 @@ package izumi.distage.roles.services
 
 import distage.{Injector, TagK}
 import izumi.distage.model.Locator
+import izumi.distage.model.monadic.DIEffect.syntax._
 import izumi.distage.model.monadic.{DIEffect, DIEffectRunner}
 import izumi.distage.model.provisioning.PlanInterpreter.FinalizersFilter
 import izumi.distage.roles.services.RoleAppPlanner.AppStartupPlans
 import izumi.fundamentals.platform.functional.Identity
 import izumi.logstage.api.IzLogger
 
-trait StartupPlanExecutor {
-  def execute[F[_] : TagK](appPlan: AppStartupPlans, filters: StartupPlanExecutor.Filters[F])(doRun: (Locator, DIEffect[F]) => F[Unit]): Unit
+trait StartupPlanExecutor[F[_]] {
+  def execute(appPlan: AppStartupPlans, filters: StartupPlanExecutor.Filters[F])(doRun: (Locator, DIEffect[F]) => F[Unit]): Unit
 }
 
 object StartupPlanExecutor {
-  def default(logger: IzLogger, injector: Injector): StartupPlanExecutor = {
-    val checker = new IntegrationChecker.Impl(logger)
-    new StartupPlanExecutor.Impl(injector, checker)
+  def default[F[_]:TagK](logger: IzLogger, injector: Injector): StartupPlanExecutor[F] = {
+    val checker = new IntegrationChecker.Impl[F](logger)
+    new StartupPlanExecutor.Impl[F](injector, checker)
   }
 
   final case class Filters[F[_]](
                                   filterF: FinalizersFilter[F],
                                   filterId: FinalizersFilter[Identity],
                                 )
+
   object Filters {
     def all[F[_]]: Filters[F] = Filters[F](FinalizersFilter.all, FinalizersFilter.all)
   }
 
-  class Impl(
+  class Impl[F[_]: TagK](
               injector: Injector,
-              integrationChecker: IntegrationChecker,
-            ) extends StartupPlanExecutor {
-    final def execute[F[_] : TagK](appPlan: AppStartupPlans, filters: StartupPlanExecutor.Filters[F])(doRun: (Locator, DIEffect[F]) => F[Unit]): Unit = {
+              integrationChecker: IntegrationChecker[F],
+            ) extends StartupPlanExecutor[F] {
+    def execute(appPlan: AppStartupPlans, filters: StartupPlanExecutor.Filters[F])(doRun: (Locator, DIEffect[F]) => F[Unit]): Unit = {
       injector.produceFX[Identity](appPlan.runtime, filters.filterId)
         .use {
           runtimeLocator =>
@@ -39,14 +41,23 @@ object StartupPlanExecutor {
 
             runner.run {
               Injector.inherit(runtimeLocator)
-                .produceFX[F](appPlan.integration, filters.filterF)
+                .produceFX[F](appPlan.app.shared.plan, filters.filterF)
                 .use {
-                  integrationLocator =>
-                    integrationChecker.checkOrFail(appPlan.integrationKeys, integrationLocator)
+                  sharedLocator =>
+
 
-                    Injector.inherit(integrationLocator)
-                      .produceFX[F](appPlan.app, filters.filterF)
-                      .use(doRun(_, effect))
+                    Injector.inherit(sharedLocator)
+                      .produceFX[F](appPlan.app.side.plan, filters.filterF)
+                      .use {
+                        integrationLocator =>
+                          integrationChecker.checkOrFail(appPlan.app.side.roots, integrationLocator)
+                      }
+                      .flatMap {
+                        _ =>
+                          Injector.inherit(sharedLocator)
+                            .produceFX[F](appPlan.app.primary.plan, filters.filterF)
+                            .use(doRun(_, effect))
+                      }
                 }
             }
         }
diff --git a/distage/distage-roles/src/main/scala/izumi/distage/staticinjector/plugins/macrodefs/StaticPluginCheckerMacro.scala b/distage/distage-roles/src/main/scala/izumi/distage/staticinjector/plugins/macrodefs/StaticPluginCheckerMacro.scala
index 8ead32ffd..09f3070e2 100644
--- a/distage/distage-roles/src/main/scala/izumi/distage/staticinjector/plugins/macrodefs/StaticPluginCheckerMacro.scala
+++ b/distage/distage-roles/src/main/scala/izumi/distage/staticinjector/plugins/macrodefs/StaticPluginCheckerMacro.scala
@@ -18,6 +18,7 @@ import izumi.distage.plugins.load.PluginLoader.PluginConfig
 import izumi.distage.plugins.load.PluginLoaderDefaultImpl
 import izumi.distage.plugins.merge.SimplePluginMergeStrategy
 import izumi.distage.roles.RoleAppLauncher
+import izumi.distage.roles.RoleAppLauncher.Options
 import izumi.distage.roles.services.{ActivationParser, PruningPlanMergingPolicy}
 import izumi.distage.staticinjector.plugins.ModuleRequirements
 import izumi.fundamentals.platform.cli.model.raw.{RawAppArgs, RawEntrypointParams, RawValue}
@@ -140,7 +141,7 @@ object StaticPluginCheckerMacro {
     val module = SimplePluginMergeStrategy.merge(loadedPlugins :+ additional.morph[PluginBase] :+ root.toList.merge.morph[PluginBase])
 
     val logger = IzLogger.NullLogger
-    val args = RawAppArgs.empty.copy(globalParameters = RawEntrypointParams(Vector.empty, activations.filter(_.nonEmpty).map(a => RawValue(RoleAppLauncher.Options.use.name.long, a)).toVector))
+    val args = RawAppArgs.empty.copy(globalParameters = RawEntrypointParams(Vector.empty, activations.filter(_.nonEmpty).map(a => RawValue(Options.use.name.long, a)).toVector))
     val activation = new ActivationParser().parseActivation(logger, args, module, Map.empty, Map.empty)
     val policy: PlanMergingPolicy = new PruningPlanMergingPolicy(logger, activation)
 
diff --git a/doc/microsite/src/main/tut/distage/other-features.md b/doc/microsite/src/main/tut/distage/other-features.md
index e2e02c3b1..0c0839382 100644
--- a/doc/microsite/src/main/tut/distage/other-features.md
+++ b/doc/microsite/src/main/tut/distage/other-features.md
@@ -411,7 +411,7 @@ class C(val b: B) extends PrintResource("C")
 
 val resources = Injector(new BootstrapModuleDef {
   many[PlanningHook]
-    .add(new AssignableFromAutoSetHook[PrintResource])
+    .add(new AutoSetHook[PrintResource])
 }).produce(new ModuleDef {
   make[C]
   make[B]
diff --git a/doc/microsite/src/main/tut/izumi.md b/doc/microsite/src/main/tut/izumi.md
index d9badf49e..30c0b8277 100644
--- a/doc/microsite/src/main/tut/izumi.md
+++ b/doc/microsite/src/main/tut/izumi.md
@@ -4,7 +4,7 @@ out: index.html
 Izumi Project
 =============
 
-Izumi *(jap. 泉水, spring)* is a set of independent libraries and frameworks allowing you to significantly increase productivity of your Scala development.
+Izumi *(jp. 泉, spring)* is a set of independent libraries and frameworks allowing you to significantly increase productivity of your Scala development.
 
 including the following components:
 
diff --git a/doc/microsite/src/main/tut/logstage/00_logstage.md b/doc/microsite/src/main/tut/logstage/00_logstage.md
index 181350462..0bdaebece 100644
--- a/doc/microsite/src/main/tut/logstage/00_logstage.md
+++ b/doc/microsite/src/main/tut/logstage/00_logstage.md
@@ -62,22 +62,26 @@ Note:
 Syntax Reference
 ------------
 
-1) Simple variable:
+1. Simple variable:
    ```scala
    logger.info(s"My message: $argument")
    ```
-2) Chain:
+
+2. Chain:
    ```scala
    logger.info(s"My message: ${call.method} ${access.value}")
    ```
-3) Named expression:
+
+3. Named expression:
    ```scala
    logger.info(s"My message: ${Some.expression -> "argname"}")
    ```
-4) Invisible name expression:
+
+4. Invisible named expression:
    ```scala
    logger.info(s"My message: ${Some.expression -> "argname" -> null}")
    ```
+
 5) De-camelcased name:
    ```scala
    logger.info(${camelCaseName-> ' '})
@@ -87,7 +91,7 @@ Dependencies
 ------------
 
 ```scala
-// LogStage API, you need it to use the logger
+// LogStage core
 libraryDependencies += Izumi.R.logstage_core
 
 // Optional
@@ -110,10 +114,10 @@ or
 @@@vars
 ```scala
 val izumi_version = "$izumi.version$"
-// LogStage API, you need it to use the logger
+// LogStage core
 libraryDependencies += "io.7mind.izumi" %% "logstage-core" % izumi_version
 
-// optional
+// Optional
 libraryDependencies ++= Seq(
   // Json output
   "io.7mind.izumi" %% "logstage-rendering-circe" % izumi_version,
@@ -222,9 +226,9 @@ SLF4J Router
 
 When not configured, `logstage-adapter-slf4j` will log messages with level `>= Info` to `stdout`.
 
-Due to the global mutable nature of `slf4j` to configure slf4j logging you'll
-have to mutate a global singleton. To change its settings, replace its `LogRouter`
-with the same one you use elsewhere in your application. 
+Due to the global mutable nature of `slf4j`, to configure slf4j logging you'll
+have to mutate a global singleton `StaticLogRouter`. Replace its `LogRouter`
+with the same one you use elsewhere in your application to use the same configuration for Slf4j. 
 
 ```scala mdoc:reset
 import logstage._
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/package.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/package.scala
index 22fac6c88..6c37f3472 100644
--- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/package.scala
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/package.scala
@@ -250,11 +250,14 @@ package object bio extends BIOSyntax {
 
     implicit object BIOForkZio extends BIOFork3[ZIO] {
       override def fork[R, E, A](f: ZIO[R, E, A]): ZIO[R, Nothing, BIOFiber[ZIO[Any, +?, +?], E, A]] =
-        f.fork
+        f
           // FIXME: ZIO Bug / feature (interruption inheritance) breaks behavior in bracket/DIResource
           //  unless wrapped in `interruptible`
           //  see: https://github.com/zio/zio/issues/945
           .interruptible
+          .nonDaemon
+          .fork
+          .daemon
           .map(BIOFiber.fromZIO)
     }
   }
diff --git a/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/cli/CLIParser.scala b/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/cli/CLIParser.scala
index f6fb4af12..c5827ecb2 100644
--- a/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/cli/CLIParser.scala
+++ b/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/cli/CLIParser.scala
@@ -40,7 +40,7 @@ class CLIParser {
     } yield roles
   }
 
-  private def validate(arguments: RawAppArgs): Either[ParserError, Unit] = {
+  private[this] def validate(arguments: RawAppArgs): Either[ParserError, Unit] = {
     val bad = arguments.roles.groupBy(_.role).filter(_._2.size > 1)
     if (bad.nonEmpty) {
       Left(ParserError.DuplicatedRoles(bad.keySet))
@@ -51,19 +51,12 @@ class CLIParser {
 }
 
 object CLIParser {
-
   sealed trait ParserError
-
   object ParserError {
-
     final case class DanglingArgument(processed: Vector[String], arg: String) extends ParserError
-
     final case class DanglingSplitter(processed: Vector[String]) extends ParserError
-
     final case class DuplicatedRoles(bad: Set[String]) extends ParserError
-
   }
-
 }
 
 
diff --git a/logstage/logstage-api/src/main/scala/izumi/logstage/macros/ArgumentNameExtractionMacro.scala b/logstage/logstage-api/src/main/scala/izumi/logstage/macros/ArgumentNameExtractionMacro.scala
index 64588197b..ef673d1ee 100644
--- a/logstage/logstage-api/src/main/scala/izumi/logstage/macros/ArgumentNameExtractionMacro.scala
+++ b/logstage/logstage-api/src/main/scala/izumi/logstage/macros/ArgumentNameExtractionMacro.scala
@@ -18,7 +18,7 @@ object ArgumentNameExtractionMacro {
        |   logger.info(s"My message: $${call.method} $${access.value}")
        |3) Named expression:
        |   logger.info(s"My message: $${Some.expression -> "argname"}")
-       |4) Invisible name expression:
+       |4) Invisible named expression:
        |   logger.info(s"My message: $${Some.expression -> "argname" -> null}")
        |5) De-camelcased name:
        |   logger.info($${camelCaseName-> ' '})


Test patch:
diff --git a/distage/distage-core/src/test/scala/izumi/distage/impl/BootstrapTest.scala b/distage/distage-core/src/test/scala/izumi/distage/impl/BootstrapTest.scala
index aaf7fa1d3..50578c514 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/impl/BootstrapTest.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/impl/BootstrapTest.scala
@@ -14,8 +14,8 @@ class BootstrapTest extends WordSpec {
       val context = new BootstrapLocator(BootstrapLocator.noProxiesBootstrap)
 
       val maybeRef = context.find[PlanAnalyzer]
-      val ref = context.lookup[PlanAnalyzer](DIKey.get[PlanAnalyzer])
-      val refBySuper = context.lookup[Any](DIKey.get[PlanAnalyzer])
+      val ref = context.lookupLocal[PlanAnalyzer](DIKey.get[PlanAnalyzer])
+      val refBySuper = context.lookupLocal[Any](DIKey.get[PlanAnalyzer])
 
       assert(maybeRef.exists(_.isInstanceOf[PlanAnalyzerDefaultImpl]))
       assert(ref.exists(_.value.isInstanceOf[PlanAnalyzerDefaultImpl]))
@@ -27,8 +27,9 @@ class BootstrapTest extends WordSpec {
         context.get[PlanAnalyzer]("another.one")
       }
 
-      val badRef = context.lookup[Long](DIKey.get[PlanAnalyzer])
-      assert(badRef.isEmpty)
+      intercept[AssertionError] {
+        context.lookupLocal[Long](DIKey.get[PlanAnalyzer])
+      }
 
       val noRef = context.find[PlanAnalyzer]("another.one")
       assert(noRef.isEmpty)
diff --git a/distage/distage-core/src/test/scala/izumi/distage/injector/AutoSetTest.scala b/distage/distage-core/src/test/scala/izumi/distage/injector/AutoSetTest.scala
index ce78b64ac..9f937b1c6 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/injector/AutoSetTest.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/injector/AutoSetTest.scala
@@ -1,12 +1,11 @@
 package izumi.distage.injector
 
-import org.scalatest.WordSpec
+import distage.{BootstrapModuleDef, Injector, ModuleDef}
 import izumi.distage.fixtures.SetCases._
 import izumi.distage.model.PlannerInput
 import izumi.distage.model.planning.PlanningHook
-import izumi.distage.planning.AssignableFromAutoSetHook
-import distage.{BootstrapModuleDef, Injector, ModuleDef}
-import izumi.distage.fixtures.SetCases.SetCase3.{ServiceA, ServiceB, ServiceC, ServiceD}
+import izumi.distage.planning.AutoSetHook
+import org.scalatest.WordSpec
 
 class AutoSetTest extends WordSpec with MkInjector {
 
@@ -21,9 +20,8 @@ class AutoSetTest extends WordSpec with MkInjector {
     }
 
     val injector = Injector.Standard(new BootstrapModuleDef {
-      many[AutoCloseable]
       many[PlanningHook]
-        .add(new AssignableFromAutoSetHook[Ordered, Ordered](identity))
+        .add(new AutoSetHook[Ordered, Ordered](identity))
     })
 
     val autoset = injector.produceUnsafe(PlannerInput.noGc(definition)).get[Set[Ordered]]
@@ -42,9 +40,8 @@ class AutoSetTest extends WordSpec with MkInjector {
     }
 
     val injector = Injector.Standard(new BootstrapModuleDef {
-      many[AutoCloseable]
       many[PlanningHook]
-        .add(new AssignableFromAutoSetHook[Int, Int](identity))
+        .add(new AutoSetHook[Int, Int](identity))
     })
 
     val autoset = injector.produceUnsafe(PlannerInput.noGc(definition)).get[Set[Int]]
diff --git a/distage/distage-core/src/test/scala/izumi/distage/injector/PlanOperationsTest.scala b/distage/distage-core/src/test/scala/izumi/distage/injector/PlanOperationsTest.scala
new file mode 100644
index 000000000..04872f846
--- /dev/null
+++ b/distage/distage-core/src/test/scala/izumi/distage/injector/PlanOperationsTest.scala
@@ -0,0 +1,49 @@
+package izumi.distage.injector
+
+import distage._
+import izumi.distage.model.PlannerInput
+import org.scalatest.WordSpec
+
+
+class PlanOperationsTest extends WordSpec with MkInjector {
+
+  import PlanOperationsTest._
+
+  "maintain correct operation order" in {
+    val primary = Set[DIKey](DIKey.get[PrimaryComponent])
+    val sub = Set[DIKey](DIKey.get[IntegrationComponent])
+
+    val definition = PlannerInput(new ModuleDef {
+      make[PrimaryComponent]
+      make[IntegrationComponent]
+      make[SharedComponent0]
+      make[SharedComponent1]
+    }, primary ++ sub)
+
+    val injector = mkInjector()
+
+    val split = injector.triSplitPlan(definition.bindings, primary)(_ => sub)
+
+    assert(Set[DIKey](DIKey.get[SharedComponent0], DIKey.get[SharedComponent1]).diff(split.shared.plan.index.keySet).isEmpty)
+    assert((primary ++ sub).intersect(split.shared.plan.index.keySet).isEmpty)
+    assert(primary.intersect(split.side.plan.index.keySet).isEmpty)
+    assert(sub.intersect(split.primary.plan.index.keySet).isEmpty)
+
+    assert(split.primary.plan.index.keySet.intersect(split.side.plan.index.keySet) == Set[DIKey](DIKey.get[SharedComponent1]))
+    assert(split.primary.plan.index.keySet.intersect(split.shared.plan.index.keySet) == Set[DIKey](DIKey.get[SharedComponent1]))
+    assert(split.side.plan.index.keySet.intersect(split.shared.plan.index.keySet) == Set[DIKey](DIKey.get[SharedComponent1]))
+  }
+
+}
+
+object PlanOperationsTest {
+
+  case class SharedComponent0()
+
+  case class SharedComponent1(com0: SharedComponent0)
+
+  case class IntegrationComponent(sharedComponent1: SharedComponent1)
+
+  case class PrimaryComponent(sharedComponent1: SharedComponent1)
+
+}
\ No newline at end of file
diff --git a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
index 899226d43..866966f3e 100644
--- a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
+++ b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
@@ -4,8 +4,20 @@ import java.nio.charset.StandardCharsets.UTF_8
 import java.nio.file.{Files, Paths}
 import java.util.UUID
 
-import izumi.distage.roles.test.fixtures.{AdoptedAutocloseablesCase, TestPlugin, TestRole00}
+import distage.plugins.{PluginBase, PluginDef}
+import distage.{DIKey, Injector, Locator}
+import izumi.distage.model.Locator.LocatorRef
+import izumi.distage.model.definition.{BootstrapModule, ModuleDef}
+import izumi.distage.monadic.modules.IdentityDIEffectModule
+import izumi.distage.roles.RoleAppMain
+import izumi.distage.roles.config.ContextOptions
+import izumi.distage.roles.model.AppActivation
+import izumi.distage.roles.services.{IntegrationChecker, PluginSource, RoleAppPlanner}
+import izumi.distage.roles.test.fixtures.Fixture.{InitCounter, Resource0, Resource1, Resource2}
+import izumi.distage.roles.test.fixtures._
+import izumi.fundamentals.platform.functional.Identity
 import izumi.fundamentals.platform.resources.ArtifactVersion
+import izumi.logstage.api.IzLogger
 import org.scalatest.WordSpec
 
 class RoleAppTest extends WordSpec
@@ -13,18 +25,149 @@ class RoleAppTest extends WordSpec
   private val prefix = "target/configwriter"
 
   private val overrides = Map(
-    "testservice.systemPropInt" -> "265"
-    , "testservice.systemPropList.0" -> "111"
-    , "testservice.systemPropList.1" -> "222"
+    "testservice.systemPropInt" -> "265",
+    "testservice.systemPropList.0" -> "111",
+    "testservice.systemPropList.1" -> "222",
   )
 
+  object TestEntrypoint extends RoleAppMain.Silent(new TestLauncher)
+
   "Role Launcher" should {
     "be able to start roles" in {
-      TestEntrypoint.main(Array(
-        "-ll", "critical",
-        ":"+ AdoptedAutocloseablesCase.id,
-        ":"+ TestRole00.id,
+      val initCounter = new InitCounter
+      var locator0: LocatorRef = null
+      lazy val locator: Locator = locator0.get
+
+      new RoleAppMain.Silent({
+        new TestLauncher {
+          override protected def pluginSource: PluginSource = super.pluginSource.map { l =>
+            l.copy(app = Seq(l.app.merge overridenBy new ModuleDef {
+              make[InitCounter].from {
+                locatorRef: LocatorRef =>
+                  locator0 = locatorRef
+                  initCounter
+              }
+            }))
+          }
+        }
+      }).main(Array(
+        "-ll", "info",
+        ":" + AdoptedAutocloseablesCase.id,
+        ":" + TestRole00.id,
       ))
+
+//      println(initCounter.startedCloseables)
+//      println(initCounter.closedCloseables)
+//      println(initCounter.checkedResources)
+
+      assert(initCounter.startedCloseables == initCounter.closedCloseables.reverse)
+      assert(initCounter.checkedResources.toSet == Set(locator.get[Resource1], locator.get[Resource2]))
+    }
+
+    "start roles regression test" in {
+      val initCounter = new InitCounter
+      var locator0: LocatorRef = null
+      lazy val locator: Locator = locator0.get
+
+      new RoleAppMain.Silent({
+        new TestLauncher {
+          override protected def pluginSource: PluginSource = super.pluginSource.map { l =>
+            l.copy(app = Seq(
+              new ResourcesPluginBase().morph[PluginBase],
+              new ConflictPlugin,
+              new TestPlugin,
+              new AdoptedAutocloseablesCasePlugin,
+              new PluginDef {
+                make[Resource0].from[Resource1]
+                many[Resource0].ref[Resource0]
+                make[InitCounter].from {
+                  locatorRef: LocatorRef =>
+                    locator0 = locatorRef
+                    initCounter
+                }
+              }))
+          }
+        }
+      }).main(Array(
+        "-ll", "info",
+        ":" + AdoptedAutocloseablesCase.id,
+        ":" + TestRole00.id,
+      ))
+
+
+
+//      println(initCounter.startedCloseables)
+//      println(initCounter.closedCloseables)
+//      println(initCounter.checkedResources)
+
+      assert(initCounter.startedCloseables == initCounter.closedCloseables.reverse)
+      assert(initCounter.checkedResources.toSet == Set(locator.get[Resource0], locator.get[Resource2]))
+    }
+
+    "integration checks are discovered and ran from a class binding when key is not an IntegrationCheck" in {
+      val logger = IzLogger()
+      val initCounter = new InitCounter
+      val definition = (new ResourcesPluginBase {
+        make[Resource0].from[Resource1]
+        many[Resource0].ref[Resource0]
+        make[InitCounter].fromValue(initCounter)
+      } ++ IdentityDIEffectModule)
+      val roleAppPlanner = new RoleAppPlanner.Impl[Identity](
+        ContextOptions(),
+        BootstrapModule.empty,
+        AppActivation.empty,
+        logger,
+      )
+      val integrationChecker = new IntegrationChecker.Impl[Identity](logger)
+
+      val plans = roleAppPlanner.makePlan(Set(DIKey.get[Set[Resource0]]), definition)
+      Injector().produce(plans.runtime).use {
+        Injector.inherit(_).produce(plans.app.shared.plan).use {
+          Injector.inherit(_).produce(plans.app.side.plan).use {
+            locator =>
+              integrationChecker.checkOrFail(plans.app.side.roots, locator)
+
+              assert(initCounter.startedCloseables.size == 3)
+              assert(initCounter.checkedResources.size == 2)
+              assert(initCounter.checkedResources.toSet == Set(locator.get[Resource0], locator.get[Resource2]))
+          }
+        }
+      }
+    }
+
+    "integration checks are discovered and ran, ignoring duplicating reference bindings" in {
+      val logger = IzLogger()
+      val initCounter = new InitCounter
+      val definition = new ResourcesPluginBase {
+        make[Resource1]
+        make[Resource0].using[Resource1]
+        make[Resource0 with AutoCloseable].using[Resource1]
+        many[Resource0]
+          .ref[Resource0]
+          .ref[Resource0 with AutoCloseable]
+        make[InitCounter].fromValue(initCounter)
+      } ++ IdentityDIEffectModule
+      val roleAppPlanner = new RoleAppPlanner.Impl[Identity](
+        ContextOptions(),
+        BootstrapModule.empty,
+        AppActivation.empty,
+        logger,
+      )
+      val integrationChecker = new IntegrationChecker.Impl[Identity](logger)
+
+      val plans = roleAppPlanner.makePlan(Set(DIKey.get[Set[Resource0]]), definition)
+      Injector().produce(plans.runtime).use {
+        Injector.inherit(_).produce(plans.app.shared.plan).use {
+          Injector.inherit(_).produce(plans.app.side.plan).use {
+            locator =>
+              integrationChecker.checkOrFail(plans.app.side.roots, locator)
+
+              assert(initCounter.startedCloseables.size == 3)
+              assert(initCounter.checkedResources.size == 2)
+              assert(initCounter.checkedResources.toSet == Set(locator.get[Resource1], locator.get[Resource2]))
+          }
+        }
+      }
     }
 
     "produce config dumps and support minimization" in {
@@ -48,8 +191,7 @@ class RoleAppTest extends WordSpec
     }
   }
 
-private def cfg(role: String, version: ArtifactVersion) = {
-    val justConfig = Paths.get(prefix, s"$role-${version.version}.json").toFile
-    justConfig
+  private def cfg(role: String, version: ArtifactVersion) = {
+    Paths.get(prefix, s"$role-${version.version}.json").toFile
   }
 }
diff --git a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/TestEntrypoint.scala b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/TestEntrypoint.scala
deleted file mode 100644
index 7a9a70548..000000000
--- a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/TestEntrypoint.scala
+++ /dev/null
@@ -1,39 +0,0 @@
-package izumi.distage.roles.test
-
-import cats.effect.IO
-import izumi.distage.plugins.load.PluginLoader.PluginConfig
-import izumi.distage.roles.internal.{ConfigWriter, Help}
-import izumi.distage.roles.test.fixtures._
-import izumi.distage.roles._
-import izumi.fundamentals.platform.cli.model.raw.{RawEntrypointParams, RawRoleParams}
-import izumi.fundamentals.platform.language.SourcePackageMaterializer.thisPkg
-
-class TestLauncherBase extends RoleAppLauncher.LauncherF[IO]() {
-  protected val bootstrapConfig: BootstrapConfig = BootstrapConfig(
-    PluginConfig(
-      debug = false
-      , packagesEnabled = Seq(s"$thisPkg.fixtures")
-      , packagesDisabled = Seq.empty
-    )
-  )
-}
-
-object ExampleLauncher extends TestLauncherBase
-
-object TestLauncher extends TestLauncherBase {
-  override protected val hook: AppShutdownStrategy[IO] = new ImmediateExitShutdownStrategy()
-}
-
-object ExampleEntrypoint extends RoleAppMain.Default(TestLauncher) {
-  override protected def requiredRoles: Vector[RawRoleParams] = Vector(
-    RawRoleParams(AdoptedAutocloseablesCase.id, RawEntrypointParams.empty, Vector.empty),
-    RawRoleParams(TestRole00.id, RawEntrypointParams.empty, Vector.empty),
-    RawRoleParams(TestRole01.id, RawEntrypointParams.empty, Vector.empty),
-    RawRoleParams(TestRole02.id, RawEntrypointParams.empty, Vector.empty),
-    RawRoleParams(TestTask00.id, RawEntrypointParams.empty, Vector.empty),
-    RawRoleParams(ConfigWriter.id, RawEntrypointParams.empty, Vector.empty),
-    RawRoleParams(Help.id, RawEntrypointParams.empty, Vector.empty),
-  )
-}
-
-object TestEntrypoint extends RoleAppMain.Silent(TestLauncher)
diff --git a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/TestLauncher.scala b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/TestLauncher.scala
new file mode 100644
index 000000000..3fe6a39ad
--- /dev/null
+++ b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/TestLauncher.scala
@@ -0,0 +1,18 @@
+package izumi.distage.roles.test
+
+import cats.effect.IO
+import izumi.distage.plugins.load.PluginLoader.PluginConfig
+import izumi.distage.roles._
+import izumi.distage.roles.services.PluginSource
+import izumi.fundamentals.platform.language.SourcePackageMaterializer.thisPkg
+
+class TestLauncher extends RoleAppLauncher.LauncherF[IO] {
+  override protected def pluginSource: PluginSource = PluginSource(
+    PluginConfig(
+      debug = false,
+      packagesEnabled = Seq(s"$thisPkg.fixtures"),
+      packagesDisabled = Seq.empty,
+    )
+  )
+  override protected val shutdownStrategy: AppShutdownStrategy[IO] = new ImmediateExitShutdownStrategy()
+}
diff --git a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/Junk.scala b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala
similarity index 69%
rename from distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/Junk.scala
rename to distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala
index 6e720fe55..9add98faa 100644
--- a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/Junk.scala
+++ b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala
@@ -5,26 +5,26 @@ import izumi.fundamentals.platform.integration.ResourceCheck
 
 import scala.collection.mutable
 
-object Junk {
+object Fixture {
   trait Dummy
 
-case class TestServiceConf(
-                              intval: Int
-                              , strval: String
-                              , overridenInt: Int
-                              , systemPropInt: Int
-                              , systemPropList: List[Int]
+  case class TestServiceConf(
+                              intval: Int,
+                              strval: String,
+                              overridenInt: Int,
+                              systemPropInt: Int,
+                              systemPropList: List[Int],
                             )
 
-class InitCounter {
+  class InitCounter {
     val startedCloseables: mutable.ArrayBuffer[AutoCloseable] = mutable.ArrayBuffer()
     val closedCloseables: mutable.ArrayBuffer[AutoCloseable] = mutable.ArrayBuffer()
     val checkedResources: mutable.ArrayBuffer[IntegrationCheck] = mutable.ArrayBuffer()
   }
 
-  trait Resource
+  trait Resource0
 
-  class Resource1(val closeable: Resource2, counter: InitCounter) extends Resource with AutoCloseable with IntegrationCheck {
+  class Resource1(val closeable: Resource2, counter: InitCounter) extends Resource0 with AutoCloseable with IntegrationCheck {
     counter.startedCloseables += this
 
     override def close(): Unit = counter.closedCloseables += this
@@ -35,7 +35,7 @@ class InitCounter {
     }
   }
 
-  class Resource2(val roleComponent: Resource3, counter: InitCounter) extends Resource with AutoCloseable with IntegrationCheck {
+  class Resource2(val roleComponent: Resource3, counter: InitCounter) extends Resource0 with AutoCloseable with IntegrationCheck {
     counter.startedCloseables += this
 
     override def close(): Unit = counter.closedCloseables += this
@@ -46,16 +46,16 @@ class InitCounter {
     }
   }
 
-  case class Resource3(roleComponent: Resource4, counter: InitCounter) extends Resource
+  case class Resource3(roleComponent: Resource4, counter: InitCounter) extends Resource0
 
-  case class Resource4(closeable: Resource5, counter: InitCounter) extends Resource
+  case class Resource4(closeable: Resource5, counter: InitCounter) extends Resource0
 
-  case class Resource5(roleComponent: Resource6, counter: InitCounter) extends Resource with AutoCloseable {
+  case class Resource5(roleComponent: Resource6, counter: InitCounter) extends Resource0 with AutoCloseable {
     counter.startedCloseables += this
 
     override def close(): Unit = counter.closedCloseables += this
   }
 
-  case class Resource6(counter: InitCounter) extends Resource
+  case class Resource6(counter: InitCounter) extends Resource0
 
 }
diff --git a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/ResourcesPlugin.scala b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/ResourcesPlugin.scala
index 8759a8e6d..b8475cc1e 100644
--- a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/ResourcesPlugin.scala
+++ b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/ResourcesPlugin.scala
@@ -2,29 +2,30 @@ package izumi.distage.roles.test.fixtures
 
 import java.util.concurrent.{ExecutorService, Executors}
 
+import izumi.distage.model.definition.ModuleDef
 import izumi.distage.model.definition.StandardAxis._
 import izumi.distage.plugins.PluginDef
-import izumi.distage.roles.test.fixtures.Junk._
+import izumi.distage.roles.test.fixtures.Fixture._
+import izumi.distage.roles.test.fixtures.ResourcesPlugin._
 
-class ResourcesPlugin extends PluginDef {
-  import ResourcesPlugin._
-  make[InitCounter]
+class ConflictPlugin extends PluginDef {
   make[Conflict].tagged(Env.Prod).from[Conflict1]
   make[Conflict].tagged(Env.Test).from[Conflict2]
   make[Conflict].from[Conflict3]
+
   make[UnsolvableConflict].from[UnsolvableConflict1]
   make[UnsolvableConflict].from[UnsolvableConflict2]
+}
 
+class ResourcesPluginBase extends ModuleDef {
   make[ExecutorService].from(Executors.newCachedThreadPool())
-  make[Resource1]
   make[Resource2]
   make[Resource3]
   make[Resource4]
   make[Resource5]
   make[Resource6]
 
-  many[Resource]
-    .ref[Resource1]
+  many[Resource0]
     .ref[Resource2]
     .ref[Resource3]
     .ref[Resource4]
@@ -32,6 +33,14 @@ class ResourcesPlugin extends PluginDef {
     .ref[Resource6]
 }
 
+class ResourcesPlugin extends ResourcesPluginBase with PluginDef {
+  make[InitCounter]
+
+  make[Resource1]
+  many[Resource0]
+    .ref[Resource1]
+}
+
 object ResourcesPlugin {
   trait Conflict
   case class Conflict1() extends Conflict
@@ -41,5 +50,4 @@ object ResourcesPlugin {
   trait UnsolvableConflict
   class UnsolvableConflict1 extends UnsolvableConflict
   class UnsolvableConflict2 extends UnsolvableConflict
-
 }
diff --git a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
index c1bc52470..7f658049d 100644
--- a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
+++ b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
@@ -2,22 +2,18 @@ package izumi.distage.roles.test.fixtures
 
 import cats.effect.IO
 import izumi.distage.model.definition.StandardAxis._
-import izumi.distage.model.monadic.{DIEffect, DIEffectRunner}
+import izumi.distage.model.monadic.{DIEffect, DIEffectAsync, DIEffectRunner}
+import izumi.distage.monadic.modules.CatsDIEffectModule
 import izumi.distage.plugins.PluginDef
 import izumi.distage.roles.internal.{ConfigWriter, Help}
-import izumi.distage.roles.test.fixtures.Junk._
+import izumi.distage.roles.test.fixtures.Fixture._
+import izumi.distage.roles.test.fixtures.TestPlugin.{InheritedCloseable, NotCloseable}
 import izumi.distage.roles.test.fixtures.TestRole00.{TestRole00Resource, TestRole00ResourceIntegrationCheck}
 import izumi.fundamentals.platform.resources.ArtifactVersion
 
-class TestPlugin extends PluginDef {
-
-  import TestPlugin._
-
+class TestPlugin extends CatsDIEffectModule with PluginDef {
   tag(Env.Prod)
 
-  addImplicit[DIEffect[IO]]
-  addImplicit[DIEffectRunner[IO]]
-
   private val version = Option(System.getProperty(TestPlugin.versionProperty)) match {
     case Some(value) =>
       value
@@ -42,14 +38,11 @@ class TestPlugin extends PluginDef {
 }
 
 object TestPlugin {
-
   trait NotCloseable
-
   final val versionProperty = "launcher-version-test"
 
   class InheritedCloseable extends NotCloseable with AutoCloseable {
     override def close(): Unit = {}
   }
-
 }
 
diff --git a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala
index 5c19612f7..ac4df70aa 100644
--- a/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala
+++ b/distage/distage-roles/src/test/scala/izumi/distage/roles/test/fixtures/TestRole00.scala
@@ -6,7 +6,7 @@ import izumi.distage.config.annotations.ConfPath
 import izumi.distage.model.definition.DIResource
 import izumi.distage.model.monadic.DIEffect
 import izumi.distage.roles.model.{IntegrationCheck, RoleDescriptor, RoleService, RoleTask}
-import izumi.distage.roles.test.fixtures.Junk._
+import izumi.distage.roles.test.fixtures.Fixture._
 import izumi.distage.roles.test.fixtures.ResourcesPlugin.Conflict
 import izumi.distage.roles.test.fixtures.TestPlugin.NotCloseable
 import izumi.distage.roles.test.fixtures.TestRole00.TestRole00Resource
@@ -16,7 +16,7 @@ import izumi.fundamentals.platform.integration.ResourceCheck
 import izumi.fundamentals.platform.language.Quirks._
 import izumi.logstage.api.IzLogger
 
-class TestTask00[F[_] : DIEffect](logger: IzLogger) extends RoleTask[F] {
+class TestTask00[F[_]: DIEffect](logger: IzLogger) extends RoleTask[F] {
   override def start(roleParameters: RawEntrypointParams, freeArgs: Vector[String]): F[Unit] = {
     DIEffect[F].maybeSuspend {
       logger.info(s"[TestTask00] Entrypoint invoked!: $roleParameters, $freeArgs")
@@ -28,18 +28,18 @@ object TestTask00 extends RoleDescriptor {
   override final val id = "testtask00"
 }
 
-class TestRole00[F[_] : DIEffect]
+class TestRole00[F[_]: DIEffect]
 (
-  val conf: TestServiceConf @ConfPath("testservice")
-, val dummies: Set[Dummy]
-, val counter: InitCounter
-, val resource: TestRole00Resource[F]
-, logger: IzLogger
-, notCloseable: NotCloseable
-, val resources: Set[Resource]
-, val conflict: Conflict
-, val es: ExecutorService
-) extends RoleService[F]  {
+  val conf: TestServiceConf@ConfPath("testservice"),
+  val dummies: Set[Dummy],
+  val counter: InitCounter,
+  val resource: TestRole00Resource[F],
+  logger: IzLogger,
+  notCloseable: NotCloseable,
+  val resources: Set[Resource0],
+  val conflict: Conflict,
+  val es: ExecutorService,
+) extends RoleService[F] {
   notCloseable.discard()
 
   override def start(roleParameters: RawEntrypointParams, freeArgs: Vector[String]): DIResource[F, Unit] = DIResource.make(DIEffect[F].maybeSuspend {
@@ -62,13 +62,13 @@ object TestRole00 extends RoleDescriptor {
   final class TestRole00Resource[F[_]](private val it: TestRole00ResourceIntegrationCheck)
   final class TestRole00ResourceIntegrationCheck
   (
-    private val cfg: IntegrationOnlyCfg @ConfPath("integrationOnlyCfg")
+    private val cfg: IntegrationOnlyCfg@ConfPath("integrationOnlyCfg"),
   ) extends IntegrationCheck {
     override def resourcesAvailable(): ResourceCheck = ResourceCheck.Success()
   }
 }
 
-class TestRole01[F[_] : DIEffect](logger: IzLogger) extends RoleService[F] {
+class TestRole01[F[_]: DIEffect](logger: IzLogger) extends RoleService[F] {
   override def start(roleParameters: RawEntrypointParams, freeArgs: Vector[String]): DIResource[F, Unit] = DIResource.make(DIEffect[F].maybeSuspend {
     logger.info(s"[TestRole01] started: $roleParameters, $freeArgs")
   }) {
@@ -85,7 +85,7 @@ object TestRole01 extends RoleDescriptor {
   override def parserSchema: RoleParserSchema = RoleParserSchema(id, ParserDef.Empty, Some("Example role"), None, freeArgsAllowed = false)
 }
 
-class TestRole02[F[_] : DIEffect](logger: IzLogger) extends RoleService[F] {
+class TestRole02[F[_]: DIEffect](logger: IzLogger) extends RoleService[F] {
   override def start(roleParameters: RawEntrypointParams, freeArgs: Vector[String]): DIResource[F, Unit] = DIResource.make(DIEffect[F].maybeSuspend {
     logger.info(s"[TestRole02] started: $roleParameters, $freeArgs")
   }) {
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala
index d8e576eb1..e9df919e4 100644
--- a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala
@@ -8,7 +8,7 @@ import izumi.distage.model.monadic.{DIEffect, DIEffectAsync, DIEffectRunner}
 import izumi.distage.model.plan.OrderedPlan
 import izumi.distage.model.providers.ProviderMagnet
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.TagK
-import izumi.distage.model.{Locator, SplittedPlan}
+import izumi.distage.model.{Locator, TriSplittedPlan}
 import izumi.distage.roles.model.IntegrationCheck
 import izumi.distage.roles.services.{IntegrationChecker, PlanCircularDependencyCheck}
 import izumi.distage.testkit.services.dstest.DistageTestRunner.{DistageTest, TestReporter}
@@ -18,10 +18,10 @@ import izumi.fundamentals.platform.language.CodePosition
 
 import scala.concurrent.duration.FiniteDuration
 
-class DistageTestRunner[F[_]: TagK]
+class DistageTestRunner[F[_] : TagK]
 (
   reporter: TestReporter,
-  integrationChecker: IntegrationChecker,
+  integrationChecker: IntegrationChecker[F],
   runnerEnvironment: SpecEnvironment[F],
   tests: Seq[DistageTest[F]],
 ) {
@@ -77,15 +77,10 @@ class DistageTestRunner[F[_]: TagK]
 
         logger.info(s"Memoized components in env $sharedKeys")
 
-        val shared = injector.splitPlan(appModule.drop(runtimeGcRoots), sharedKeys) {
+        val shared = injector.triSplitPlan(appModule.drop(runtimeGcRoots), sharedKeys) {
           _.collectChildren[IntegrationCheck].map(_.target).toSet
         }
 
-        //        println(shared.primary.render())
-        //        println("===")
-        //        println(shared.subplan.render())
-        //        println("===")
-
         checker.verify(runtimePlan)
 
         // first we produce our Monad's runtime
@@ -97,38 +92,46 @@ class DistageTestRunner[F[_]: TagK]
 
             runner.run {
               // now we produce integration components for our shared plan
-              checker.verify(shared.subplan)
-              Injector.inherit(runtimeLocator).produceF[F](shared.subplan).use {
-                sharedIntegrationLocator =>
-                  check(testplans.map(_._1), shared, F, sharedIntegrationLocator) {
-                    proceed(checker, testplans, shared, sharedIntegrationLocator)
+              checker.verify(shared.side.plan)
+
+              Injector.inherit(runtimeLocator).produceF[F](shared.shared.plan).use {
+                sharedLocator =>
+
+                  Injector.inherit(sharedLocator).produceF[F](shared.side.plan).use {
+                    sharedIntegrationLocator =>
+                      ifIntegChecksOk(F, sharedIntegrationLocator)(testplans.map(_._1), shared) {
+                        proceed(checker, testplans, shared, sharedLocator)
+                      }
                   }
               }
+
             }
         }
     }
   }
 
-  private def check(testplans: Seq[DistageTest[F]], plans: SplittedPlan, F: DIEffect[F], integLocator: Locator)(onSuccess: => F[Unit]): F[Unit] = {
-    integrationChecker.check(plans.subRoots, integLocator) match {
-      case Some(value) =>
+  private def ifIntegChecksOk(F: DIEffect[F], integLocator: Locator)(testplans: Seq[DistageTest[F]], plans: TriSplittedPlan)(onSuccess: => F[Unit]): F[Unit] = {
+    implicit val FF: DIEffect[F] = F
+    integrationChecker.collectFailures(plans.side.roots, integLocator).flatMap {
+      case Left(value) =>
         F.traverse_(testplans) {
           test =>
             F.maybeSuspend(reporter.testStatus(test.meta, TestStatus.Cancelled(value)))
         }
 
-      case None =>
+      case Right(_) =>
         onSuccess
+
     }
   }
 
-  private def proceed(checker: PlanCircularDependencyCheck, testplans: Seq[(DistageTest[F], OrderedPlan)], shared: SplittedPlan, sharedIntegrationLocator: Locator)
+  private def proceed(checker: PlanCircularDependencyCheck, testplans: Seq[(DistageTest[F], OrderedPlan)], shared: TriSplittedPlan, parent: Locator)
                      (implicit F: DIEffect[F], P: DIEffectAsync[F]): F[Unit] = {
     // here we produce our shared plan
-    checker.verify(shared.primary)
-    Injector.inherit(sharedIntegrationLocator).produceF[F](shared.primary).use {
-      sharedLocator =>
-        val testInjector = Injector.inherit(sharedLocator)
+    checker.verify(shared.primary.plan)
+    Injector.inherit(parent).produceF[F](shared.primary.plan).use {
+      mainSharedLocator =>
+        val testInjector = Injector.inherit(mainSharedLocator)
 
         // now we are ready to run each individual test
         // note: scheduling here is custom also and tests may automatically run in parallel for any non-trivial monad
@@ -142,19 +145,23 @@ class DistageTestRunner[F[_]: TagK]
               _ <- F.maybeSuspend(reporter.beginSuite(id))
               _ <- P.parTraverse_(plans) {
                 case (test, testplan) =>
-                  val allSharedKeys = sharedLocator.allInstances.map(_.key).toSet
+                  val allSharedKeys = mainSharedLocator.allInstances.map(_.key).toSet
 
-                  val newtestplan = testInjector.splitExistingPlan(shared.reducedModule.drop(allSharedKeys), testplan.keys -- allSharedKeys, allSharedKeys, testplan) {
-                    _.collectChildren[IntegrationCheck].map(_.target).toSet -- allSharedKeys
-                  }
+                  val integrations = testplan.collectChildren[IntegrationCheck].map(_.target).toSet -- allSharedKeys
+                  val newtestplan = testInjector.triPlan(shared.primary.module.drop(allSharedKeys), testplan.keys -- allSharedKeys, integrations)
+
+                  checker.verify(newtestplan.primary.plan)
+                  checker.verify(newtestplan.side.plan)
+                  checker.verify(newtestplan.shared.plan)
 
-                  checker.verify(newtestplan.subplan)
-                  checker.verify(newtestplan.primary)
                   // we are ready to run the test, finally
-                  testInjector.produceF[F](newtestplan.subplan).use {
-                    integLocator =>
-                      check(Seq(test), newtestplan, F, integLocator) {
-                        proceedIndividual(test, newtestplan, integLocator)
+                  testInjector.produceF[F](newtestplan.shared.plan).use {
+                    sharedLocator =>
+                      Injector.inherit(sharedLocator).produceF[F](newtestplan.side.plan).use {
+                        integLocator =>
+                          ifIntegChecksOk(F, integLocator)(Seq(test), newtestplan) {
+                            proceedIndividual(test, newtestplan, sharedLocator)
+                          }
                       }
                   }
               }
@@ -164,8 +171,9 @@ class DistageTestRunner[F[_]: TagK]
     }
   }
 
-  private def proceedIndividual(test: DistageTest[F], newtestplan: SplittedPlan, integLocator: Locator)(implicit F: DIEffect[F]): F[Unit] = {
-    Injector.inherit(integLocator).produceF[F](newtestplan.primary).use {
+  private def proceedIndividual(test: DistageTest[F], newtestplan: TriSplittedPlan, parent: Locator)(implicit F: DIEffect[F]): F[Unit] = {
+    Injector.inherit(parent)
+      .produceF[F](newtestplan.primary.plan).use {
       testLocator =>
         def doRun(before: Long): F[Unit] = {
           for {
@@ -207,12 +215,19 @@ object DistageTestRunner {
   final case class SuiteData(suiteName: String, suiteId: String, suiteClassName: String)
 
   sealed trait TestStatus
+
   object TestStatus {
+
     case object Scheduled extends TestStatus
+
     case object Running extends TestStatus
+
     final case class Cancelled(checks: Seq[ResourceCheck.Failure]) extends TestStatus
+
     final case class Succeed(duration: FiniteDuration) extends TestStatus
+
     final case class Failed(t: Throwable, duration: FiniteDuration) extends TestStatus
+
   }
 
   trait TestReporter {
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/dstest/TestEnvironmentProviderImpl.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/dstest/TestEnvironmentProviderImpl.scala
index bdf54b3e1..792c8e4b7 100644
--- a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/dstest/TestEnvironmentProviderImpl.scala
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/dstest/TestEnvironmentProviderImpl.scala
@@ -90,7 +90,7 @@ class TestEnvironmentProviderImpl
   }
 
   override protected def makePluginLoader(bootstrapConfig: BootstrapConfig): PluginSource = {
-    new PluginSource.Impl(bootstrapConfig)
+    PluginSource(bootstrapConfig)
   }
 
 }
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/DistagePluginTestSupport.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/DistagePluginTestSupport.scala
index 3ed9142a6..128f73124 100644
--- a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/DistagePluginTestSupport.scala
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/DistagePluginTestSupport.scala
@@ -107,7 +107,7 @@ abstract class DistagePluginTestSupport[F[_] : TagK] extends DistageTestSupport[
   }
 
   protected def makePluginLoader(bootstrapConfig: BootstrapConfig): PluginSource = {
-    new PluginSource.Impl(bootstrapConfig)
+    PluginSource(bootstrapConfig)
   }
 
 }
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/DistageTestSupport.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/DistageTestSupport.scala
index 01490c6e1..175f147f0 100644
--- a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/DistageTestSupport.scala
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/DistageTestSupport.scala
@@ -84,7 +84,7 @@ abstract class DistageTestSupport[F[_]](implicit val tagK: TagK[F])
     verifyTotalSuppression()
     try {
       makeExecutor(plan.injector, logger)
-        .execute[F](plan, filters) {
+        .execute(plan, filters) {
           (locator, effect) =>
             implicit val F: DIEffect[F] = effect
 
@@ -164,7 +164,7 @@ abstract class DistageTestSupport[F[_]](implicit val tagK: TagK[F])
     new RoleAppPlanner.Impl[F](options, bsModule, activation, logger)
   }
 
-  protected def makeExecutor(injector: Injector, logger: IzLogger): StartupPlanExecutor = {
+  protected def makeExecutor(injector: Injector, logger: IzLogger): StartupPlanExecutor[F] = {
     StartupPlanExecutor.default(logger, injector)
   }
 
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/ExternalResourceProvider.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/ExternalResourceProvider.scala
index 057a16c19..46c21581c 100644
--- a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/ExternalResourceProvider.scala
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/services/st/adapter/ExternalResourceProvider.scala
@@ -4,7 +4,7 @@ import distage.{DIKey, Locator, SafeType, TagK}
 import izumi.distage.bootstrap.BootstrapLocator
 import izumi.distage.model.definition.DIResource.DIResourceBase
 import izumi.distage.model.definition.{DIResource, LocatorDef}
-import izumi.distage.model.monadic.{DIEffect, DIEffectRunner}
+import izumi.distage.model.monadic.{DIEffect, DIEffectAsync, DIEffectRunner}
 import izumi.distage.model.provisioning.PlanInterpreter.Finalizer
 import izumi.distage.model.references.IdentifiedRef
 import izumi.distage.testkit.services.SyncCache
@@ -121,6 +121,7 @@ object ExternalResourceProvider {
           new LocatorDef {
             addImplicit[DIEffectRunner[Identity]]
             addImplicit[DIEffect[Identity]]
+            addImplicit[DIEffectAsync[Identity]]
           }
         }
       )