Repository: avaje/avaje-inject
Pull request #800: Multi-Scope Wiring
Author: login: SentryMan
Labels: name: enhancement
description: New feature or request
Created: 2025-04-11T04:08:41Z | Merged: 2025-04-14T10:45:17Z

@cbarlin would this suffice?

With this, you would have to add the `requires` property in your `@InjectModule` annotations to control the order.


- now will order manually provided `AvajeModule`s
- will generate an `allRequiredModules` static method if the module has no external project scope dependencies and no transitive constructor params

Resolves #793

Linked issue #793: Cross-/multi-scope bean wiring
Hi there! I'm experimenting with avaje-inject version 11.4 and exploring ways to work with `@Scopes` in order to make a mono-repo with multiple deployments, which share a lot of common code by virtue of sharing a common core and because it was previously a monolith.

Ideally, there'd be an easy way to:
 * Annotate each component with a small number of annotations, which defines which deployments it's applicable to
 * Easily start the application with a small "main" method

I've tried a few things, but have gotten stuck on these paths (code examples+more detail in the expandables):

<details>
<summary>Declare scopes requiring other scopes, automatically including them</summary>

Using the following structure:

```java
@Scope
@InjectModule(provides = {BeanInModA.class}, strictWiring = true)
public @interface ModAScope {}

@ModAScope
public class BeanInModA{}

@Scope
@InjectModule(provides = {BeanInModB.class}, strictWiring = true)
public @interface ModBScope {}

@ModBScope
public class BeanInModB{}

@Scope
@InjectModule(requires = {ModAScope.class, ModBScope.class}, strictWiring = true)
public @interface CrossCutScope {}

@CrossCutScope
public class BeanCross{
    public BeanCross(final BeanInModA beanInModA, final BeanInModB beanInModB) {

    }
}
```

The result means you cannot just refer to the `CrossCutModule` when starting an entire `BeanScope` - you have to declare parent scopes (which is documented), or start the `BeanScope` with all modules declared in order. You can't pass them to `BeanScope.builder().modules(...)` out of order (e.g. `ModAScope, CrossCutScope, ModBScope`) as you'll end up with exception at startup ("`java.lang.IllegalStateException`: Injecting null for `com.example.BeanInModB name:!beanInModB` ..."). 

This is fine if you have a small number of scopes with a clear parent/child hierarchy, but if you have a large number of scopes/modules, or there's not a clear relationship between them, it becomes quite unmanageable.

---

</details>
<details>
<summary>Meta-annotating defined Scopes</summary>

Using the following structure:

```java
@Scope
@Target(TYPE)
@Retention(SOURCE)
@InjectModule(strictWiring = true)
public @interface ModAScope {}

@Scope
@Target(TYPE)
@Retention(SOURCE)
@InjectModule(strictWiring = true)
public @interface ModBScope {}

@ModAScope
@ModBScope
@Target(TYPE)
@Retention(SOURCE)
public @interface MetaScope {}

@MetaScope
public class MyBean {}
```

I hoped it would result in the same output as:

```java
@ModAScope
@ModBScope
public class MyBean {}
```

So the bean exists in both `ModAScope` and `ModBScope`, but instead it's not picked up by Avaje at all.

---

</details>

While `@Profile` would probably do what I need (and I can meta-annotate those), that's less easily shaded (in the sense of dropping unreferenced classes) and doesn't give compile time errors. While neither of those cons are deal-breakers (Avaje is fast and light enough from my experiments you can just check the profiles work in tests, and there's negligible runtime overhead to non-constructed beans) I'm surprised that neither of my scope attempts worked (and at the `BeanScope.builder().modules(...)` being order-dependent). Another option would be to have many Gradle/Maven sub-projects and rely on the default scope, but that would also become quite unwieldy depending on the size of the code base and how much you wish to split it.

Is there any scope/intention to support something like this with `@Scope`/`@InjectModule`? Ideas on making something like this work without introducing potentially breaking changes:
 1. Add another value to the `InjectModule` called something like `automaticallyImport` which acts like `requires` for all validation checking, but when the generated module's `build` method is called the first operations are to create and build the listed modules/scopes
 2. Add an option on the `BeanScopeBuilder` to correct module order (default `false` - current behaviour) and/or to define our own `ModuleOrdering` with custom logic
 3. Allow `Scope` composite annotations. A bean annotated with a meta-scope would be treated as if it were directly annotated with those scopes, which currently is to be created in each scope.

Option 3 is probably the most elegant for both Avaje and consuming applications. A blend of Options 1 and 2 could also be done outside of Avaje as the details are available at run-time, so someone using Avaje could write a method that takes in a module and it returns an array of modules in the correct order (if an order is possible).

Thanks heaps for taking the time to read!
Linked issue comments: user: login: SentryMan
created_at: 2025-04-02T12:46:48Z
body: > @Scopes

I added all the Profile and conditional annotations precisely because I found custom scopes unwieldy. @rbygrave is the guy to ask about custom scopes.

> Another option would be to have many Gradle/Maven sub-projects and rely on the default scope

Other than conditional annotations, this is the option I see most people go with.

> define our own ModuleOrdering with custom logic

You can do this today, though it is an SPI and not a direct argument on the builder.
author_association: COLLABORATOR
user: login: SentryMan
created_at: 2025-04-02T16:33:27Z
body: One question I have is how custom scopes change the shading situation.
author_association: COLLABORATOR
user: login: cbarlin
created_at: 2025-04-02T20:08:38Z
body: > I added all the Profile and conditional annotations precisely because I found custom scopes unwieldy... 

That's fair - they do appear to be more user friendly. The trade-offs I mentioned really are very minor given how trivial they are to overcome.

> You can do this today, though it is an SPI and not a direct argument on the builder.

Oh? If so that's good, although from my quick poke around it did appear the SPI point was the `BeanScopeBuilder` instead. Now I think on it though that would work to - write a wrapper that delegates everything to the default, and just overload the `modules` method

> One question I have is how custom scopes change the shading situation

I could give a proper example later when I'm not on mobile, but `Profile` still references the classes that won't be loaded as it wraps them in a conditional vs `Scope` whose module doesn't reference it at all. If you had a bean `PullsInLargeDependencyBean` that is only needed in an `TalkToMachinaryApp` then when shading `Profile` will (probably) result in the bean (and dependencies) being included vs `Scope` which wouldn't (assuming your "app" module only loads a relevant scope).

Again, that could be solved with having different gradle/maven projects, but working out where to draw those harder boundaries and how many can be hard on a medium/large project. 
author_association: CONTRIBUTOR
user: login: rbygrave
created_at: 2025-04-03T02:43:51Z
body: I'd like to understand the problem / context of the issue better.

> with multiple deployments

How many different deployments are we talking about and can you give some examples. (All / Admin Only ... ? .. I'm wondering if we thinking the same thing here with "deployments" split by functionality?)


> less easily shaded 

Why do we care about shading so much? Are we building using graalvm native-image or is it just truely large or something else? [just wondering about the bang for buck and testing etc or am I completely missing something interesting here] [edit: shading isn't great for docker and shading isn't great for java module-info adoption so I'm wary when shading comes up as opposed to excluding optional dependencies but maybe there is a really good need for shading / tree shaking here]


> This is fine if you have a small number of scopes with a clear parent/child hierarchy, but if you have a large number of scopes/modules, or there's not a clear relationship between them, it becomes quite unmanageable.

How many scopes do you have in mind? Can you put some examples of what you think these scopes are and how they are related?

An example of how bean scopes are used is in a build system which supports multiple platforms (Windows, Linux, Mac). So there are a few scopes for common build features and a scope for each platform specific one. **_Lets say 7 scopes with pretty well understood relationships_**. I'm getting a feeling you are talking a LOT more than 7 scopes? Like how many scopes are we talking about? [which might give me a sense of what we are trying to do. If you say 100 scopes then I'm not understanding the use case ... that is basically a big dependency tree of scopes etc]


>  it was previously a monolith.

So it sounds like its pretty big and maybe not broken into multiple maven modules yet, its still one src/main?  No existing modularity in the code base? No intention to add modularity in the form of splitting it into maven modules yet?


author_association: CONTRIBUTOR
user: login: rbygrave
created_at: 2025-04-03T03:00:45Z
body: ```java
@ModAScope
@ModBScope
public class MyBean {}
```
> So the bean exists in both ModAScope and ModBScope ...

You are going to have to give a practical example of this so that I can get a sense of what you are trying to achieve here / why a bean **_"exists"_** in both ModAScope and ModBScope? 

The issue is that **_"exists"_** is actually more **_"owned, created in"_** and a bean in this sense should be created and owned by only one scope. So what were we trying to achieve with a bean in multiple scopes? Can you put a real world example there so that I can understand what we were aiming for? 

author_association: CONTRIBUTOR
user: login: rbygrave
created_at: 2025-04-03T03:36:00Z
body: > `PullsInLargeDependencyBean`

Just to say that for me it's more `PullsInLargeDependencyBean` **_attached to an expensive resource_** like additional db connection pool, queue, external resource, expensive UI canvas etc ... its really the expensive resource part we are looking to avoid initialisation on, and so `Profile` and optional dependencies can work well enough.

Scope has been used when the final deployment artifact literally only includes certain scopes - Linux, Windows, MacOS.

I think I'm keen to here if this application is a server app or desktop app or build system or ...  so that might help understand the goals we are aiming for and better choose Scope vs Profile vs [optional] AvajeModule etc.
author_association: CONTRIBUTOR
user: login: cbarlin
created_at: 2025-04-03T09:16:16Z
body:  > Can you put a real world example there so that I can understand what we were aiming for?

Yep! I'll walk through the journey and answer the other questions after that :D

 > So it sounds like its pretty big and maybe not broken into multiple maven modules yet, its still one src/main? No existing modularity in the code base? No intention to add modularity in the form of splitting it into maven modules yet?

A server app all in one src/main yes, no clear modularity in code, but the tasks it performs can be broken up semi-cleanly which hopefully will become k8s deployments with APIs, or jobs/cronjobs. The number of deployments is a little hazy as it depends on how things "fall out", but is around 12-15.

After getting a consistent DI into the project, plan is to try and move to a structure that looks like this:

```
project/
├─ app_warehouse_mgmt_sea/
│  ├─ src/main/java/.../App.java
├─ app_warehouse_mgmt_air/
│  ├─ src/main/java/.../App.java
├─ app_analytics_api/
│  ├─ src/main/java/.../App.java
├─ job_analytics_processor/
│  ├─ src/main/java/.../App.java
... repeat for the rest of the apps/jobs
├─ was_monolith/
│  ├─ src/main/java/
│  │  ├─ <all the code is here>
├─ components/
│  ├─ <nothing here ... yet!>
```

Each `App.java` starts up the bean scope, and either exposes the API or does its job. The API ones will all start out being the same (i.e. the monolith minus jobs deployed a bunch of times). After that would then be to start trying to break apart the `was_monolith` so that each app only loads beans it needs either going top-down or bottom-up [edit: or both, although probably only surface level on one side]. Once that's done, start trying to group the beans up and lift them out into their own projects under components or into the app/job project itself (and maybe eventually start breaking it into independent projects). The goal of doing it in passes is to de-risk the overall process.

You can kinda see it above, but the warehousing items above would be typical candidates for `Profile` (warehouses near the sea vs near airports), whereas the warehouse vs analytics would probably be more akin to `Scope`. Both could be used, and probably will be at the end, but I'd like to keep the number of metaphorical balls in the air down (where reasonable). A `Scope` (being a module) could also be used as a prospective component boundary as it isolates the beans within it from those outside of it (while the code lives in the same compilation unit).

So the journey I went on with exploration is what I (aimed, a little poorly) to list in my original post:
 1. Try a top-down approach - use `Profile` as a meta-annotation that denotes which deployment a bean should end up in. Think `@WarehouseMgmtSea` = `@Profile(all = {"warehouse_mgmt", "sea"})`. Pros are it's pretty flexible and easy to mix or rework, cons are it's possibly too flexible and prone to simple mistakes [edit: at scale it'd be prone, small behavioural differences would be fine], and it doesn't convey potential module boundaries
 2. Try a bottom-up approach - can I create `InjectModules` items that would be candidates to be components? No, end up with errors about multiple modules in the default scope, which makes sense to me.
 3. Try a bottom-up approach - can I create `Scope` items that would be candidates to be components (e.g. database init, validating incoming inventory manifests)? First define the scope and tag the beans wherever they are, then try and refactor the beans into a single package so when we start lifing it should be easy. Pros are its concrete in that it doesn't rely on string matching, it makes it clear what could be an actual module boundary, and compile checks over test checks, cons are loading all the needed scopes in the `App.java` could be frustrating
 4. Try a top-down approach with `Scope` instead - `Scope` generates a module that lists all the beans right there, so have each app only load the one `Scope` (i.e. `Scope` = deployment). Pros are it's definitive and very clear what is or isn't in an app, cons are that you end up with lots of annotations on a single bean and it doesn't easily convey potential module boundaries

I'm still OK with the trade-off of `Profile` being less "concrete" than `Scope` because tests will pick up if a bean isn't enabled in a profile it should be, and beans being enabled in profiles they don't need to be isn't as much of an issue if nothing external can interact with it/it doesn't produce external effects. If the outcome of this issue is that no change will be made to Avaje that's totally fine - there is a totally workable path with `Profile` and I'm more exploring if there is a better path. The point about potentially using the SPI to load in a different `ModuleOrdering` (or `BeanScopeBuilder`) also gives a path forward there.

Addressing some of the other questions:

 > Why do we care about shading so much? Are we building using graalvm native-image or...

Shading would be useful for two purposes:
 * Working out what dependencies are actually used and where
 * Vulnerabilities in dependencies only flag on the apps they are actually used in, making reasoning with them (patch, assert that there's no path to exploit, etc) much easier to do

I agree that modules (both gradle/maven and Java) being the better solution for dependency tree management overall, or tools like jib for docker containers if you'd rather avoid modules. Re graalvm: Doesn't it do tree-shaking automatically?

 > shading isn't great for java module-info adoption

Huh, never thought of that but I guess it makes sense. Why should e.g. guava modularise when shading can do the job, I suppose (although if java had multi-module jar files...)

 > How many scopes do you have in mind? Can you put some examples of what you think these scopes are and how they are related?

That depends on if a top-down (`Scope` = deployed artifact) or bottom-up (`Scope` = eventual component) approach is taken. The latter will end up with a lot more `Scope`s than the former, although it comes down to how aggressively it's broken up and if a path of break a few scopes out then lift them out, break a few more then lift, rinse/repeat.

 > The issue is that "exists" is actually more "owned, created in" and a bean in this sense should be created and owned by only one scope

For this example I was talking about the `Scope` = deployed artifact path I was exploring. At runtime, the bean is only owned/created in by only one scope because only one scope is loaded. I noticed at compile time though, the bean creation/ownership is written into multiple `Scope`s (and it sounds like that maybe shouldn't be the case?). The meta-annotation idea was extending the existing current behaviour (hopefully causing the least impact).

 > Just to say that for me it's more `PullsInLargeDependencyBean` **attached to an expensive resource**...

Completely reasonable - why load an expensive resource when it doesn't need to be? `Profile` definitely fills this role nicely.
author_association: CONTRIBUTOR
user: login: cbarlin
created_at: 2025-04-03T09:28:57Z
body:  > For this example I was talking about the `Scope` = deployed artifact path I was exploring. At runtime, the bean is only owned...

To clarify this a bit more: In both the `Scope` = deployed artifact path and the `Scope` = potential component path, a bean at runtime would/should be created by and owned/managed by only one scope (or not loaded at all). For the deployment path, this is because only one scope is loaded, and for the component path this is because it'd be in one scope then `requires` (or `automaticallyImport`) to the others.

The difference was in the source code (and by extension at compile time) how many `Scope`s a bean belongs to.
author_association: CONTRIBUTOR
user: login: rbygrave
created_at: 2025-04-03T10:38:05Z
body: Ok, so if I understand it right then, say in `app_warehouse_mgmt_sea/` there is the `App` but there isn't much else at all. This is really just code to bootstrap the "warehouse_mgmt_sea App", and 99% of the code is in ... `was_monolith` / `src/main/java`.

Rob background thinking ...  
- Naturally a [default scope] AvajeModule maps 1 to 1 to a maven module - a src/main/java
- We like to use custom `Scope` when we have a lot of components inside a single src/main/java and we wish to bring some DI modularity there


> deployments ...  "fall out", but is around 12-15.

Right. So at a very _minimum_ we are talking 15 odd `Scope` ... and it's just basically pretty darn big. Its pretty likely that each of those 15 deployments could well desire a bunch of sub-scopes to vertically or horizontally slice their components.

Right, I think I'm getting it. My gut is saying that custom `Scope` could well be pretty close to a good fit conceptually. Problems with it like say ordering are related to the shear scale we've got here (to date it's only been used at much smaller scale, say less that 10 custom scopes). 

author_association: CONTRIBUTOR
user: login: rbygrave
created_at: 2025-04-03T11:18:11Z
body: > 3. Try a bottom-up approach ... cons are loading all the needed scopes in the App.java could be frustrating

> The result means you cannot just refer to the CrossCutModule when starting an entire BeanScope  ...  You can't pass them to BeanScope.builder().modules(...) out of order

Right, at scale yes this isn't practical to being done manually. This seems like something that is fixable. We have the custom scopes, and their requires/provides so it could be that ordering for the custom scopes could be generated at compile time or determined at runtime. 

e.g. We want to build a BeanScope with CrossCutScope. It's dependencies and their ordering can be determined by avaje-inject at runtime or compile time.

A failing test here could be, using BeanScope Builder pass the CrossCutScope module only, and the builder automatically determines the dependencies (ModAScope, ModBScope) and the ordering based on requires/provides  and that "just works".



> 4. Try a top-down approach with ... cons are that you end up with lots of annotations on a single bean 

I'm not sure here and need to look at what's going on. The single bean in multiple scopes wasn't aligned to my thinking of a bean only being in a single Scope. Not sure here and need to check this.






author_association: CONTRIBUTOR
user: login: cbarlin
created_at: 2025-04-03T18:55:08Z
body:  > Ok, so if I understand it right then, say in `app_warehouse_mgmt_sea/` there is the `App` but there isn't much else at all.

That's the goal for now, yes. After all the slicing is done, it's possible that there will instead be an `app_warehouse_mgmt` with a `sea` and `air` `Profile`, but that's long-term. Modularity is what we want for now.

 > My gut is saying that custom `Scope` could well be pretty close to a good fit conceptually. 

It did start to seem like that while I was experimenting, so I'm glad that you also think so 😄 

 > Right, at scale yes this isn't practical to being done manually. This seems like something that is fixable. We have the custom scopes, and their requires/provides so it could be that ordering for the custom scopes could be generated at compile time or determined at runtime.

Yes, with a slight preference for compile time ordering as it has all the context and can probably better handle situations in which the dependency graph isn't strictly linear. For example, a `manifest-validator` Scope which requires both a `manifest-database` and `core-database`, and `manifest-database` also requires `core-database`, compile-time analysis would be better suited to ensuring that `core-database` is loaded before the other two (or fail the compilation if the graph isn't complete).

To avoid breaking existing behaviour while keeping resolution done at compile time I proposed the `automaticallyImport`, so:
 * `requires` = beans `provided` in that scope/module are available in this one, but I will do the wiring (current behaviour)
 * `automaticallyImport` = beans `provided` in that scope/module are available in this one, and I want the wiring done for me

And if compile-time isn't the place to do this, to avoid breaking existing behaviour for runtime I proposed a `BeanScopeBuilder` option to turn on the resolver.

That's my outside looking in perspective - you guys might have better ideas knowing other projects/Avaje better than I do. 

 > A failing test here could be, using BeanScope Builder pass the CrossCutScope module only, and the builder automatically determines the dependencies (ModAScope, ModBScope) and the ordering based on requires/provides and that "just works".

Yep, although I think that's the passing case - the failure would be that it didn't work it out and errors out at runtime/in tests instead.

 > The single bean in multiple scopes wasn't aligned to my thinking of a bean only being in a single Scope

Which is fair - a bean being in multiple scopes is a bit odd. At runtime it would only exist in one, but the compiler has no way of knowing that. It does seem like the bottom-up approach where `Scope` = component is more promising for this case though.

Thanks heaps for the help so far to both of you!
author_association: CONTRIBUTOR
user: login: SentryMan
created_at: 2025-04-05T02:24:00Z
body: > Profile still references the classes that won't be loaded

Perchance can you elaborate on this?
author_association: COLLABORATOR
user: login: cbarlin
created_at: 2025-04-05T05:38:47Z
body: > Perchance can you elaborate on this?

Sure! Using version 11.4 still for these examples.

Let's say that the following is in the `was_monolith` module:

```java
public interface IncomingContainerPlacementPlanner {}

@Singleton
@Profile(all = {"warehouse", "air"})
public class AirInConPlanner implements IncomingContainerPlacementPlanner {}

@Singleton
@Profile(all = {"warehouse", "sea"})
public class SeaInConPlanner implements IncomingContainerPlacementPlanner {}

// In package-info.java
@io.avaje.inject.InjectModule(provides = com.example.profile.IncomingContainerPlacementPlanner.class)
package com.example.profile;
```

The following is generated (in collapsible):

<details>
<summary>Generated code</summary>

`AirInConPlanner$DI.java`

```java
@Generated("io.avaje.inject.generator")
public final class AirInConPlanner$DI  {

  public static void build(Builder builder) {
    if (!builder.containsAllProfiles(List.of("air","warehouse"))) {
      return;
    }

    if (builder.isBeanAbsent(AirInConPlanner.class, IncomingContainerPlacementPlanner.class)) {
      var bean = new AirInConPlanner();
      builder.register(bean);
    }
  }

}
```

`SeaInConPlanner$DI.java`

```java
@Generated("io.avaje.inject.generator")
public final class SeaInConPlanner$DI  {

  public static void build(Builder builder) {
    if (!builder.containsAllProfiles(List.of("warehouse","sea"))) {
      return;
    }

    if (builder.isBeanAbsent(SeaInConPlanner.class, IncomingContainerPlacementPlanner.class)) {
      var bean = new SeaInConPlanner();
      builder.register(bean);
    }
  }

}
```

`ProfileModule.java`

```java
@Generated("io.avaje.inject.generator")
@InjectModule(provides = {com.example.profile.IncomingContainerPlacementPlanner.class})
public final class ProfileModule implements AvajeModule {

  @Override
  public Type[] provides() {
    return new Type[] {
      com.example.profile.IncomingContainerPlacementPlanner.class,
    };
  }

  @Override
  public Type[] autoProvides() {
    return new Type[] {
      com.example.profile.AirInConPlanner.class,
      com.example.profile.SeaInConPlanner.class,
    };
  }

  @Override
  public Class<?>[] classes() {
    return new Class<?>[] {
      com.example.profile.AirInConPlanner.class,
      com.example.profile.SeaInConPlanner.class,
    };
  }

  /**
   * Creates all the beans in order based on constructor dependencies.
   * The beans are registered into the builder along with callbacks for
   * field/method injection, and lifecycle support.
   */
  @Override
  public void build(Builder builder) {
    // create beans in order based on constructor dependencies
    // i.e. "provides" followed by "dependsOn"
    build_profile_AirInConPlanner(builder);
    build_profile_SeaInConPlanner(builder);
  }

  @DependencyMeta(
      type = "com.example.profile.AirInConPlanner",
      provides = {"com.example.profile.IncomingContainerPlacementPlanner"},
      autoProvides = {
        "com.example.profile.IncomingContainerPlacementPlanner",
        "com.example.profile.AirInConPlanner"
      })
  private void build_profile_AirInConPlanner(Builder builder) {
    AirInConPlanner$DI.build(builder);
  }

  @DependencyMeta(
      type = "com.example.profile.SeaInConPlanner",
      provides = {"com.example.profile.IncomingContainerPlacementPlanner"},
      autoProvides = {
        "com.example.profile.IncomingContainerPlacementPlanner",
        "com.example.profile.SeaInConPlanner"
      })
  private void build_profile_SeaInConPlanner(Builder builder) {
    SeaInConPlanner$DI.build(builder);
  }

}
```
---
</details>

If we then have an `App.java` in our `app_warehouse_sea`:

```java
public class App 
{
    public static void main( String[] args )
    {
        try(
            BeanScope cross = BeanScope.builder()
                .profiles("warehouse", "sea")
                .build();
        ) {
            // Do all the things
        }
    }
}
```

When you shade the output with an Service Loader aware plugin, it will:
 * `BeanScopeBuilder` is referenced - include that
 * We have the Service Loader plugin enabled, so it detects the app uses SPI to load all `AvajeModule`, include all those
 * `ProfileModule` references both `SeaInConnPlanner` and `AirInConnPlanner` and their `$DI` classes, so include those and those they reference

Both beans (and their dependencies) will be included in the output unless the shader has some way of knowing that the result of `!builder.containsAllProfiles(List.of("air","warehouse"))` is always false. There may be a way to do that, I'm not sure, but as far as I know that's not possible.

Compare that to the following layout in the `was_monolith` module:

```java
@Scope
public @interface AirAppScope {}

@Scope
public @interface SeaAppScope {}

public interface IncomingContainerPlacementPlanner {}

@AirAppScope
public class AirInConPlanner implements IncomingContainerPlacementPlanner {}

@SeaAppScope
public class SeaInConPlanner implements IncomingContainerPlacementPlanner {}
```

The following is the generated output:

<details>
<summary>Generated code</summary>

`AirInConPlanner$DI.java`

```java
@Generated("io.avaje.inject.generator")
public final class AirInConPlanner$DI  {

  public static void build(Builder builder) {
    if (builder.isBeanAbsent(AirInConPlanner.class, IncomingContainerPlacementPlanner.class)) {
      var bean = new AirInConPlanner();
      builder.register(bean);
    }
  }

}
```

`SeaInConPlanner$DI.java`

```java
@Generated("io.avaje.inject.generator")
public final class SeaInConPlanner$DI  {

  public static void build(Builder builder) {
    if (builder.isBeanAbsent(SeaInConPlanner.class, IncomingContainerPlacementPlanner.class)) {
      var bean = new SeaInConPlanner();
      builder.register(bean);
    }
  }

}
```

`AirAppModule.java`

```java
@Generated("io.avaje.inject.generator")
@InjectModule(customScopeType = "com.example.profile.AirAppScope")
public final class AirAppModule implements AvajeModule.Custom {

  @Override
  public Type[] autoProvides() {
    return new Type[] {
      com.example.profile.AirInConPlanner.class,
      com.example.profile.IncomingContainerPlacementPlanner.class,
    };
  }

  @Override
  public Class<?>[] classes() {
    return new Class<?>[] {
      com.example.profile.AirInConPlanner.class,
    };
  }

  /**
   * Creates all the beans in order based on constructor dependencies.
   * The beans are registered into the builder along with callbacks for
   * field/method injection, and lifecycle support.
   */
  @Override
  public void build(Builder builder) {
    // create beans in order based on constructor dependencies
    // i.e. "provides" followed by "dependsOn"
    build_profile_AirInConPlanner(builder);
  }

  @DependencyMeta(
      type = "com.example.profile.AirInConPlanner",
      provides = {"com.example.profile.IncomingContainerPlacementPlanner"},
      autoProvides = {
        "com.example.profile.IncomingContainerPlacementPlanner",
        "com.example.profile.AirInConPlanner"
      })
  private void build_profile_AirInConPlanner(Builder builder) {
    AirInConPlanner$DI.build(builder);
  }

}
```

`SeaAppModule.java`

```java
@Generated("io.avaje.inject.generator")
@InjectModule(customScopeType = "com.example.profile.SeaAppScope")
public final class SeaAppModule implements AvajeModule.Custom {

  @Override
  public Type[] autoProvides() {
    return new Type[] {
      com.example.profile.IncomingContainerPlacementPlanner.class,
      com.example.profile.SeaInConPlanner.class,
    };
  }

  @Override
  public Class<?>[] classes() {
    return new Class<?>[] {
      com.example.profile.SeaInConPlanner.class,
    };
  }

  /**
   * Creates all the beans in order based on constructor dependencies.
   * The beans are registered into the builder along with callbacks for
   * field/method injection, and lifecycle support.
   */
  @Override
  public void build(Builder builder) {
    // create beans in order based on constructor dependencies
    // i.e. "provides" followed by "dependsOn"
    build_profile_SeaInConPlanner(builder);
  }

  @DependencyMeta(
      type = "com.example.profile.SeaInConPlanner",
      provides = {"com.example.profile.IncomingContainerPlacementPlanner"},
      autoProvides = {
        "com.example.profile.IncomingContainerPlacementPlanner",
        "com.example.profile.SeaInConPlanner"
      })
  private void build_profile_SeaInConPlanner(Builder builder) {
    SeaInConPlanner$DI.build(builder);
  }

}
```

---
</details>

If we then have an `App.java` in our `app_warehouse_sea`:

```java
public class App 
{
    public static void main( String[] args )
    {
        try(
            BeanScope cross = BeanScope.builder()
                .modules(new SeaAppModule())
                .build();
        ) {
            // Do all the things
        }
    }
}
```

When you attempt to shade the output, it will:
 * `BeanScopeBuilder` is referenced - include that
 * `SeaAppModule` is referenced - include that
 * `SeaAppModule` references `SeaInConnPlanner` and its `$DI` generated class, include them

The main difference is the way the `Module` files are generated - `Profile` creates a single module that is picked up by the Service Loader that references both and uses a conditional to pick beans, vs `Scope` which creates two modules and then we explicitly reference one class. `Profile` definitely does work to prevent *starting* the beans (and thus anything they connect to like databases or message queues or machine bus interfaces), so the runtime/startup cost is still successfully massively reduced, which is the primary concern. It also works for cases where we need to specify behaviour for certain situations. It's just that shading (probably) won't remove the unneeded classes/dependencies.

I haven't yet put enough together to ascertain if shading still needs to be SPI aware in this case (it might do - a lot of things use Service Loaders, not just avaje-inject). But because `AirAppModule` isn't referenced, it's not include and neither is `AirInConPlanner` and its dependencies etc...

You could probably exclude the class `AirInConnPlanner` manually in the shading configuration of the `warehouse_sea` module in this example. If you did need/want Service loader support though I imagine that manually excluding `AirAppModule` would be easier than listing all the specific classes to exclude (because it could be a lot of classes, at least in my case).

Does that help?
author_association: CONTRIBUTOR
user: login: SentryMan
created_at: 2025-04-05T05:55:20Z
body: If you use shading to remove the unused third-party dependencies but keep your own classes, is there an issue? As the execution paths are gated behind a conditional check I would imagine you wouldn't get any NoClassDefFound errors.
author_association: COLLABORATOR
user: login: cbarlin
created_at: 2025-04-05T06:37:10Z
body: Oh you mean like exclude the transitive dependencies a given app doesn't need? That should work yes - thank you for that idea! It'd be manual, but that probably forces thinking about how those dependencies are used and if they are even needed to begin with. Number of dependencies might still pose an issue, but it's a workable way forward for that path. 

It wouldn't work if you wanted to shade your own classes though - not sure there's too many cases where that would be a strongly desired goal though.
author_association: CONTRIBUTOR
user: login: cbarlin
created_at: 2025-04-10T08:42:53Z
body: I did have some thoughts about a way to potentially implement the `automaticallyImport` idea too. I'm not 100% certain it's the best approach - I've only poked about the avaje-inject code, not worked with it. Seeing the new test module did prompt me to write those thoughts down though, in case they end up being helpful during design (if the idea is implemented, that is).

<details>
<summary>Implementation idea</summary>

Add the `automaticallyImport` value to the `InjectModule` annotation. Valid targets are `AvajeModule` classes, or interfaces annotated with `@Scope`. Target modules/imports cannot have external `requires` unless the current module can provide them (via `automaticallyImport`ing the required item, or by requiring the item itself). Circular dependencies are prohibited at compile time.

Add two more extensions to the `AvajeModule` interface - a `ModuleTreeAware` which directly extends the `AvajeModule`, and a `CompleteModuleTree` which extends `ModuleTreeAware`.

`ModuleTreeAware` adds an additional method `automaticallyImportedModules` which returns an ordered `AvajeModule[]` (default empty).

When generating an `AvajeModule`:
 * If the `InjectModule` has any `automaticallyImport` items, it is eligble to become a `ModuleTreeAware` module. Scopes are translated to their modules, and then they get written into the generated `AvajeModule[]` array.
 * If `ModuleA` says to auto import `ModuleB` and `ModuleC`, and `ModuleB` says to auto import `ModuleC`, then `ModuleA` only needs to include `ModuleB` in the output.
 * If the `InjectModule` has no `requires` itself and has `strictWiring` set to true, then it becomes a `CompleteModuleTree` module
   * This is even if it lists `automaticallyImport` as it knows how to wire them
   * If the module would require items from other modules that aren't listed, then it's not a `ModuleTreeAware` module (as in, automatically created `AvajeModule`s created as they are right now are unchanged)

When loading `AvajeModule`s if a module is `ModuleTreeAware`, extract its `automaticallyImportedModules` and insert those modules into the load order before the current one. Repeat for those discovered modules. If a module is attempted to be loaded again, ignore the duplicate (although ensure that load-order is correct).
 
Once all modules to be loaded are found, if all the modules in the list to be built are `CompleteModuleTree` modules, then we can skip loading the default scope/re-sorting/etc as the module tree is already complete.

This covers the request to have scopes depend on other scopes, and also has the nice benefit that if the entire module tree was known at compile time each step of the way then less work needs to be done at runtime (although the wiring is already very very fast, and it's probably not the slow point at startup!).

</details>

Also: is the expectation here that I would implement this feature, or am I fine to leave it for someone else to assign themselves to it? Considering it's making some not-so-small changes I'm not sure a "first contribution" is entirely appropriate...
author_association: CONTRIBUTOR
user: login: SentryMan
created_at: 2025-04-10T13:08:04Z
body: If you think it's taking too long you can try to implement. That's how I got started. I'm not really a scopes guy so I was assuming Rob was  working on it.

If you create a PR with a failing test I could try and take a look.
author_association: COLLABORATOR
user: login: cbarlin
created_at: 2025-04-11T01:37:32Z
body:  > If you think it's taking too long you can ...

Wasn't aiming to be impatient sorry! More just checking expectations

 > ... was assuming Rob was working on it.

I'm happy to do that too (or you, if you wanted to work on it)

> If you create a PR with a failing test

If that would be helpful, I can do that! I might make two - one building off of Rob's existing test (testing module ordering when they are all defined at runtime manually), and one with the `automaticallyImport` idea. I'll get started on those shortly
author_association: CONTRIBUTOR
user: login: rbygrave
created_at: 2025-04-11T10:14:21Z
body: >  implement the automaticallyImport idea too

That might be a thing but for me right now its actually just down to determining the module ordering of the required modules and generating code based on that.

So the guts is that to use the `CrossCutModule` ... we actually need the required modules AND ordering of those so:
```java
      .modules(new ModAModule(), new ModCModule(), new ModBModule(),  new CrossCutModule())
```
 
This could be just done by adding a helper method onto the generated CrossCutModule like:

```java
@Generated("io.avaje.inject.generator")
@InjectModule(requires = {org.multi.scope.ModAScope.class,org.multi.scope.ModBScope.class}, customScopeType = "org.multi.scope.CrossCutScope")
public final class CrossCutModule implements AvajeModule.Custom {

  // new method ... return all required modules in correct order
  public static List<AvajeModule> allRequiredModules() {
    return List.of(new ModAModule(), new ModCModule(), new ModBModule(), new CrossCutModule());
  }
  ...

```

And then we could use it like:
```java
return BeanScope.builder()
      .modules(CrossCutModule.allRequiredModules())
      .build();
```

Is that not what we need here? Or have I missed something and there is more required? 

If thats the case then I think this becomes:
We already have the `FactoryOrder` class that can take a list of ModuleData. I think this is a case of starting from CrossCutScope, get its required list, continue recursively getting the required scopes ... create a ModuleData for each custom module with the required properties, give those all to FactoryOrder to order them, take that ordered list and generate the new `allRequiredModules()` method.  
author_association: CONTRIBUTOR
user: login: SentryMan
created_at: 2025-04-11T18:17:21Z
body: >  public final class CrossCutModule implements AvajeModule.Custom {
> 
>   // new method ... return all required modules in correct order
>   public static List<AvajeModule> allRequiredModules() {
>     return List.of(new ModAModule(), new ModCModule(), new ModBModule(), new CrossCutModule());
>   }

@cbarlin do you want this too?
author_association: COLLABORATOR
user: login: cbarlin
created_at: 2025-04-11T19:54:55Z
body: > This could be just done by adding a helper method onto the generated CrossCutModule like:
>   ...
> And then we could use it like:
> ```java
> return BeanScope.builder()
>       .modules(CrossCutModule.allRequiredModules())
>       .build();
> ```
> Is that not what we need here? Or have I missed something and there is more required?

Yes! If you are happy for it to be done that way, then that definitely works for me. I was only suggesting `automaticallyImport` to avoid changing existing behaviour, but this also does that without adding a new setting in the annotation.

> do you want this too?

Yes please!


author_association: CONTRIBUTOR
user: login: SentryMan
created_at: 2025-04-11T21:30:15Z
body: > adding a helper method

sounds cool, but I feel like this might complicate things. 

1.  How can we determine the module class names from just the annotation?
2.  The same as above, but for custom scopes that require scopes from external dependencies/maven modules
author_association: COLLABORATOR
user: login: SentryMan
created_at: 2025-04-11T21:40:35Z
body: I can figure out the first item as long as it's in the current compilation unit, but the second stumps me.
author_association: COLLABORATOR
user: login: SentryMan
created_at: 2025-04-12T02:30:14Z
body: Or perhaps I'm thinking about it wrong, do we want to have the module itself added as a requires instead of the scope annotation?


author_association: COLLABORATOR
user: login: SentryMan
created_at: 2025-04-12T02:50:24Z
body: > ```java
>   // new method ... return all required modules in correct order
>   public static List<AvajeModule> allRequiredModules() {
>     return List.of(new ModAModule(), new ModCModule(), new ModBModule(), new CrossCutModule());
>   }
> ```

@rbygrave it seems this doesn't work as there are cases where the generated module can have constructor parameters.

tbh, I think what we have in that PR should be good for now
author_association: COLLABORATOR
user: login: cbarlin
created_at: 2025-04-12T03:28:04Z
body: What if it the method took in all the params that were needed for the modules constructor's? 
author_association: CONTRIBUTOR
user: login: SentryMan
created_at: 2025-04-12T04:58:29Z
body: It seems that this is harder than it looks. [This is what I got so far](https://github.com/SentryMan/avaje-inject/tree/module-custom-order).
author_association: COLLABORATOR
user: login: cbarlin
created_at: 2025-04-12T05:45:14Z
body: Sorry! Only made that suggestion since it sounded like a hard requirement for the idea to be implemented.

I don't have a requirement to pass in external dependencies, especially given the implementation above makes things very flexible as you can use scopes and factories to manage difficult things instead. Could you instead not generate the method if there are externally required items, and deal with generating a requiring version if/when someone puts forth a use case for such a thing?

Also thank you so much for doing the digging into making it happen - I very much appreciate it! 
author_association: CONTRIBUTOR
user: login: SentryMan
created_at: 2025-04-12T06:14:12Z
body: I'm trying that, but it fails for a different reason: something about the dependencies not being satisfied. Doesn't happen with the annotations approach though.
author_association: COLLABORATOR
user: login: rbygrave
created_at: 2025-04-14T10:52:12Z
body: Releasing 11.5-RC2 that contains this change to maven central now. Should be available in central soon to try out etc. 

Awesome work @SentryMan 
author_association: CONTRIBUTOR
user: login: cbarlin
created_at: 2025-04-14T18:33:17Z
body: Awesome! Thanks heaps, especially @SentryMan! 
author_association: CONTRIBUTOR

Diff:
diff --git a/blackbox-multi-scope/pom.xml b/blackbox-multi-scope/pom.xml
index 017719bb4..459ae740f 100644
--- a/blackbox-multi-scope/pom.xml
+++ b/blackbox-multi-scope/pom.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
     <groupId>io.avaje</groupId>
@@ -31,7 +31,11 @@
       <version>1.5</version>
       <scope>test</scope>
     </dependency>
-
+    <dependency>
+      <groupId>io.avaje</groupId>
+      <artifactId>blackbox-other</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/blackbox-multi-scope/src/main/java/org/multi/crosscut/BeanCross.java b/blackbox-multi-scope/src/main/java/org/multi/crosscut/BeanCross.java
index 9ef5340ff..a4a00b754 100644
--- a/blackbox-multi-scope/src/main/java/org/multi/crosscut/BeanCross.java
+++ b/blackbox-multi-scope/src/main/java/org/multi/crosscut/BeanCross.java
@@ -5,8 +5,6 @@
 import org.multi.scope.CrossCutScope;
 
 @CrossCutScope
-public class BeanCross{
-  public BeanCross(final BeanInModA beanInModA, final BeanInModB beanInModB) {
-
-  }
+public class BeanCross {
+  public BeanCross(final BeanInModA beanInModA, final BeanInModB beanInModB) {}
 }
diff --git a/blackbox-multi-scope/src/main/java/org/multi/crosscut/BeanCross2.java b/blackbox-multi-scope/src/main/java/org/multi/crosscut/BeanCross2.java
index a6916d412..18bfd976c 100644
--- a/blackbox-multi-scope/src/main/java/org/multi/crosscut/BeanCross2.java
+++ b/blackbox-multi-scope/src/main/java/org/multi/crosscut/BeanCross2.java
@@ -1,13 +1,10 @@
 package org.multi.crosscut;
 
 import org.multi.moda.BeanInModA;
-import org.multi.modb.BeanInModB;
-import org.multi.modc.modb.BeanInModC;
+import org.multi.modc.BeanInModC;
 import org.multi.scope.CrossCutScope;
 
 @CrossCutScope
 public class BeanCross2 {
-  public BeanCross2(final BeanInModA beanInModA, final BeanInModC beanInModC) {
-
-  }
+  public BeanCross2(final BeanInModA beanInModA, final BeanInModC beanInModC) {}
 }
diff --git a/blackbox-multi-scope/src/main/java/org/multi/crosscut/BeanCross3.java b/blackbox-multi-scope/src/main/java/org/multi/crosscut/BeanCross3.java
index e8ce47019..0764859df 100644
--- a/blackbox-multi-scope/src/main/java/org/multi/crosscut/BeanCross3.java
+++ b/blackbox-multi-scope/src/main/java/org/multi/crosscut/BeanCross3.java
@@ -1,11 +1,9 @@
 package org.multi.crosscut;
 
-import org.multi.modc.modb.COther;
+import org.multi.modc.COther;
 import org.multi.scope.CrossCutScope;
 
 @CrossCutScope
 public class BeanCross3 {
-  public BeanCross3(final COther other) {
-
-  }
+  public BeanCross3(final COther other) {}
 }
diff --git a/blackbox-multi-scope/src/main/java/org/multi/main/CrossCutMain.java b/blackbox-multi-scope/src/main/java/org/multi/main/CrossCutMain.java
index 45452311a..966869abb 100644
--- a/blackbox-multi-scope/src/main/java/org/multi/main/CrossCutMain.java
+++ b/blackbox-multi-scope/src/main/java/org/multi/main/CrossCutMain.java
@@ -1,15 +1,13 @@
 package org.multi.main;
 
-import io.avaje.inject.BeanScope;
 import org.multi.crosscut.BeanCross;
 import org.multi.crosscut.BeanCross2;
 import org.multi.crosscut.BeanCross3;
 import org.multi.crosscut.CrossCutModule;
 import org.multi.moda.BeanInModA;
-import org.multi.moda.ModAModule;
 import org.multi.modb.BeanInModB;
-import org.multi.modb.ModBModule;
-import org.multi.modc.modb.ModCModule;
+
+import io.avaje.inject.BeanScope;
 
 public class CrossCutMain {
 
@@ -24,8 +22,6 @@ public static void main(String[] args) {
   }
 
   public static BeanScope buildScope() {
-    return BeanScope.builder()
-      .modules(new ModAModule(), new ModCModule(), new ModBModule(),  new CrossCutModule())
-      .build();
+    return BeanScope.builder().modules(CrossCutModule.allRequiredModules()).build();
   }
 }
diff --git a/blackbox-multi-scope/src/main/java/org/multi/many/BeanInMany.java b/blackbox-multi-scope/src/main/java/org/multi/many/BeanInMany.java
new file mode 100644
index 000000000..0a9002de9
--- /dev/null
+++ b/blackbox-multi-scope/src/main/java/org/multi/many/BeanInMany.java
@@ -0,0 +1,19 @@
+package org.multi.many;
+
+import org.multi.moda.BeanInModA;
+import org.multi.modc.COther;
+import org.multi.mode.BeanInModE;
+import org.multi.scope.ManyScope;
+import org.other.one.custom.OtherCustomComponent;
+
+import io.avaje.inject.External;
+
+@ManyScope
+public class BeanInMany {
+
+  public BeanInMany(
+      BeanInModE beanInModE,
+      COther cOther,
+      BeanInModA modA,
+      @External OtherCustomComponent external) {}
+}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/moda/BeanInModA.java b/blackbox-multi-scope/src/main/java/org/multi/moda/BeanInModA.java
index d2ab9b119..b53276214 100644
--- a/blackbox-multi-scope/src/main/java/org/multi/moda/BeanInModA.java
+++ b/blackbox-multi-scope/src/main/java/org/multi/moda/BeanInModA.java
@@ -3,5 +3,4 @@
 import org.multi.scope.ModAScope;
 
 @ModAScope
-public class BeanInModA {
-}
+public class BeanInModA {}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/modb/BOther.java b/blackbox-multi-scope/src/main/java/org/multi/modb/BOther.java
index 5fc5cd162..9ef1fd2ce 100644
--- a/blackbox-multi-scope/src/main/java/org/multi/modb/BOther.java
+++ b/blackbox-multi-scope/src/main/java/org/multi/modb/BOther.java
@@ -1,6 +1,6 @@
 package org.multi.modb;
 
-import org.multi.modc.modb.COther;
+import org.multi.modc.COther;
 import org.multi.scope.ModBScope;
 
 @ModBScope
diff --git a/blackbox-multi-scope/src/main/java/org/multi/modb/BeanInModB.java b/blackbox-multi-scope/src/main/java/org/multi/modb/BeanInModB.java
index 0e56cf44a..4d5d97cf3 100644
--- a/blackbox-multi-scope/src/main/java/org/multi/modb/BeanInModB.java
+++ b/blackbox-multi-scope/src/main/java/org/multi/modb/BeanInModB.java
@@ -3,5 +3,4 @@
 import org.multi.scope.ModBScope;
 
 @ModBScope
-public class BeanInModB {
-}
+public class BeanInModB {}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/modc/BeanInModC.java b/blackbox-multi-scope/src/main/java/org/multi/modc/BeanInModC.java
new file mode 100644
index 000000000..6245ec3f5
--- /dev/null
+++ b/blackbox-multi-scope/src/main/java/org/multi/modc/BeanInModC.java
@@ -0,0 +1,6 @@
+package org.multi.modc;
+
+import org.multi.scope.ModCScope;
+
+@ModCScope
+public class BeanInModC {}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/modc/COther.java b/blackbox-multi-scope/src/main/java/org/multi/modc/COther.java
new file mode 100644
index 000000000..5d718b4f7
--- /dev/null
+++ b/blackbox-multi-scope/src/main/java/org/multi/modc/COther.java
@@ -0,0 +1,6 @@
+package org.multi.modc;
+
+import org.multi.scope.ModCScope;
+
+@ModCScope
+public class COther {}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/modc/modb/BeanInModC.java b/blackbox-multi-scope/src/main/java/org/multi/modc/modb/BeanInModC.java
deleted file mode 100644
index ff783368a..000000000
--- a/blackbox-multi-scope/src/main/java/org/multi/modc/modb/BeanInModC.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.multi.modc.modb;
-
-import org.multi.scope.ModCScope;
-
-@ModCScope
-public class BeanInModC {
-}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/modc/modb/COther.java b/blackbox-multi-scope/src/main/java/org/multi/modc/modb/COther.java
deleted file mode 100644
index adec1e6b5..000000000
--- a/blackbox-multi-scope/src/main/java/org/multi/modc/modb/COther.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.multi.modc.modb;
-
-import org.multi.scope.ModCScope;
-
-@ModCScope
-public class COther {
-}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/modd/BeanInModD.java b/blackbox-multi-scope/src/main/java/org/multi/modd/BeanInModD.java
new file mode 100644
index 000000000..bf7f73834
--- /dev/null
+++ b/blackbox-multi-scope/src/main/java/org/multi/modd/BeanInModD.java
@@ -0,0 +1,14 @@
+package org.multi.modd;
+
+import org.multi.moda.BeanInModA;
+import org.multi.scope.ModDScope;
+
+@ModDScope
+public class BeanInModD {
+
+  private final BeanInModA beanA;
+
+  public BeanInModD(final BeanInModA beanInModA) {
+    this.beanA = beanInModA;
+  }
+}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/mode/BeanInModE.java b/blackbox-multi-scope/src/main/java/org/multi/mode/BeanInModE.java
new file mode 100644
index 000000000..4d46684f3
--- /dev/null
+++ b/blackbox-multi-scope/src/main/java/org/multi/mode/BeanInModE.java
@@ -0,0 +1,6 @@
+package org.multi.mode;
+
+import org.multi.scope.ModEScope;
+
+@ModEScope
+public class BeanInModE {}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/scope/CrossCutScope.java b/blackbox-multi-scope/src/main/java/org/multi/scope/CrossCutScope.java
index 0534a3751..02ae57a14 100644
--- a/blackbox-multi-scope/src/main/java/org/multi/scope/CrossCutScope.java
+++ b/blackbox-multi-scope/src/main/java/org/multi/scope/CrossCutScope.java
@@ -2,9 +2,7 @@
 
 import io.avaje.inject.InjectModule;
 import jakarta.inject.Scope;
-import org.multi.modb.BeanInModB;
 
 @Scope
-@InjectModule(requires = {ModAScope.class, ModBScope.class}, strictWiring = true)
-public @interface CrossCutScope {
-}
+@InjectModule(requires = {ModAScope.class, ModBScope.class})
+public @interface CrossCutScope {}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/scope/ManyScope.java b/blackbox-multi-scope/src/main/java/org/multi/scope/ManyScope.java
new file mode 100644
index 000000000..9a36756fd
--- /dev/null
+++ b/blackbox-multi-scope/src/main/java/org/multi/scope/ManyScope.java
@@ -0,0 +1,11 @@
+package org.multi.scope;
+
+import org.other.one.custom.ExternalScope;
+
+import io.avaje.inject.InjectModule;
+import jakarta.inject.Scope;
+
+@Scope
+@InjectModule(
+    requires = {ExternalScope.class, ModDScope.class, CrossCutScope.class, ModEScope.class})
+public @interface ManyScope {}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/scope/ModAScope.java b/blackbox-multi-scope/src/main/java/org/multi/scope/ModAScope.java
index e1b23ba10..142f118e5 100644
--- a/blackbox-multi-scope/src/main/java/org/multi/scope/ModAScope.java
+++ b/blackbox-multi-scope/src/main/java/org/multi/scope/ModAScope.java
@@ -4,6 +4,5 @@
 import jakarta.inject.Scope;
 
 @Scope
-@InjectModule(strictWiring = true)
-public @interface ModAScope {
-}
+@InjectModule
+public @interface ModAScope {}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/scope/ModBScope.java b/blackbox-multi-scope/src/main/java/org/multi/scope/ModBScope.java
index ab6b1ae94..39a8a084c 100644
--- a/blackbox-multi-scope/src/main/java/org/multi/scope/ModBScope.java
+++ b/blackbox-multi-scope/src/main/java/org/multi/scope/ModBScope.java
@@ -4,6 +4,5 @@
 import jakarta.inject.Scope;
 
 @Scope
-@InjectModule(requires = ModCScope.class, strictWiring = true)
-public @interface ModBScope {
-}
+@InjectModule(requires = ModCScope.class)
+public @interface ModBScope {}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/scope/ModCScope.java b/blackbox-multi-scope/src/main/java/org/multi/scope/ModCScope.java
index 14306b992..15b067acc 100644
--- a/blackbox-multi-scope/src/main/java/org/multi/scope/ModCScope.java
+++ b/blackbox-multi-scope/src/main/java/org/multi/scope/ModCScope.java
@@ -4,6 +4,5 @@
 import jakarta.inject.Scope;
 
 @Scope
-@InjectModule(strictWiring = true)
-public @interface ModCScope {
-}
+@InjectModule
+public @interface ModCScope {}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/scope/ModDScope.java b/blackbox-multi-scope/src/main/java/org/multi/scope/ModDScope.java
new file mode 100644
index 000000000..9c82e2728
--- /dev/null
+++ b/blackbox-multi-scope/src/main/java/org/multi/scope/ModDScope.java
@@ -0,0 +1,8 @@
+package org.multi.scope;
+
+import io.avaje.inject.InjectModule;
+import jakarta.inject.Scope;
+
+@Scope
+@InjectModule(requires = {ModAScope.class})
+public @interface ModDScope {}
diff --git a/blackbox-multi-scope/src/main/java/org/multi/scope/ModEScope.java b/blackbox-multi-scope/src/main/java/org/multi/scope/ModEScope.java
new file mode 100644
index 000000000..1a4677387
--- /dev/null
+++ b/blackbox-multi-scope/src/main/java/org/multi/scope/ModEScope.java
@@ -0,0 +1,8 @@
+package org.multi.scope;
+
+import io.avaje.inject.InjectModule;
+import jakarta.inject.Scope;
+
+@Scope
+@InjectModule
+public @interface ModEScope {}
diff --git a/blackbox-multi-scope/src/test/java/org/multi/crosscut/BeanCrossTest.java b/blackbox-multi-scope/src/test/java/org/multi/crosscut/BeanCrossTest.java
index dad539ba3..24ef1ea5e 100644
--- a/blackbox-multi-scope/src/test/java/org/multi/crosscut/BeanCrossTest.java
+++ b/blackbox-multi-scope/src/test/java/org/multi/crosscut/BeanCrossTest.java
@@ -1,24 +1,64 @@
 package org.multi.crosscut;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import io.avaje.inject.BeanScope;
 import org.junit.jupiter.api.Test;
+import org.multi.many.BeanInMany;
+import org.multi.many.ManyModule;
+import org.multi.moda.BeanInModA;
+import org.multi.moda.ModAModule;
+import org.multi.modb.BOther;
 import org.multi.modb.BeanInModB;
+import org.multi.modb.ModBModule;
+import org.multi.modc.BeanInModC;
+import org.multi.modc.COther;
+import org.multi.modc.ModCModule;
+import org.multi.modd.BeanInModD;
+import org.multi.modd.ModDModule;
+import org.multi.mode.BeanInModE;
+import org.multi.mode.ModEModule;
+import org.other.one.custom.ExternalModule;
 
-import static org.assertj.core.api.Assertions.assertThat;
+import io.avaje.inject.BeanScope;
 
 class BeanCrossTest {
 
-  @Test
-  void bootstrap() {
+  private static final Class<?>[] CHECKABLE = {
+    BeanCross.class,
+    BeanCross2.class,
+    BeanCross3.class,
+    BeanInMany.class,
+    BeanInModA.class,
+    BeanInModB.class,
+    BOther.class,
+    BeanInModC.class,
+    COther.class,
+    BeanInModD.class,
+    BeanInModE.class
+  };
 
-    try (BeanScope beanScope = BeanScope.builder()
-//      .modules(new CrossCutModule())
-      .build()) {
+  @Test
+  void testCustomOrdering() {
 
-//      var beanInModB = beanScope.get(BeanInModB.class);
-//      assertThat(beanInModB).isNotNull();
+    try (BeanScope beanScope =
+        assertDoesNotThrow(
+            () ->
+                BeanScope.builder()
+                    .modules(
+                        new ModEModule(),
+                        new ModBModule(),
+                        new ModDModule(),
+                        new ManyModule(),
+                        new ModCModule(),
+                        new ModAModule(),
+                        new CrossCutModule(),
+                        new ExternalModule())
+                    .build())) {
+      for (final Class<?> clazz : CHECKABLE) {
+        assertTrue(
+            beanScope.getOptional(clazz).isPresent(), "Bean not found: " + clazz.getSimpleName());
+      }
     }
-
   }
 }
diff --git a/blackbox-other/src/main/java/org/other/one/custom/ExternalScope.java b/blackbox-other/src/main/java/org/other/one/custom/ExternalScope.java
new file mode 100644
index 000000000..3a858c74a
--- /dev/null
+++ b/blackbox-other/src/main/java/org/other/one/custom/ExternalScope.java
@@ -0,0 +1,8 @@
+package org.other.one.custom;
+
+import io.avaje.inject.InjectModule;
+import jakarta.inject.Scope;
+
+@Scope
+@InjectModule
+public @interface ExternalScope {}
diff --git a/blackbox-other/src/main/java/org/other/one/custom/OtherCustomComponent.java b/blackbox-other/src/main/java/org/other/one/custom/OtherCustomComponent.java
new file mode 100644
index 000000000..f8b62b3b9
--- /dev/null
+++ b/blackbox-other/src/main/java/org/other/one/custom/OtherCustomComponent.java
@@ -0,0 +1,4 @@
+package org.other.one.custom;
+
+@ExternalScope
+public class OtherCustomComponent {}
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java b/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java
index 62d35a28a..962944c43 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java
@@ -71,7 +71,7 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
     ProcessingContext.registerProvidedTypes(moduleFileProvided);
     moduleData.forEach(ProcessingContext::addModule);
     this.elementUtils = processingEnv.getElementUtils();
-    this.allScopes = new AllScopes();
+    this.allScopes = ProcessingContext.allScopes();
     this.defaultScope = allScopes.defaultScope();
     ExternalProvider.registerPluginProvidedTypes(defaultScope);
     pluginFileProvided.forEach(defaultScope::pluginProvided);
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/ProcessingContext.java b/inject-generator/src/main/java/io/avaje/inject/generator/ProcessingContext.java
index edf12ace9..2e87abb06 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/ProcessingContext.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ProcessingContext.java
@@ -1,21 +1,37 @@
 package io.avaje.inject.generator;
 
+import static io.avaje.inject.generator.APContext.asTypeElement;
+import static io.avaje.inject.generator.APContext.elements;
+import static io.avaje.inject.generator.APContext.filer;
+import static io.avaje.inject.generator.APContext.logError;
+import static io.avaje.inject.generator.APContext.logNote;
+import static io.avaje.inject.generator.APContext.logWarn;
+import static io.avaje.inject.generator.APContext.typeElement;
+import static io.avaje.inject.generator.APContext.types;
+import static java.util.stream.Collectors.toSet;
+
 import java.io.BufferedReader;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.LineNumberReader;
 import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
 
 import javax.annotation.processing.FilerException;
 import javax.lang.model.element.TypeElement;
 import javax.lang.model.type.TypeMirror;
 import javax.tools.FileObject;
 import javax.tools.StandardLocation;
-import java.util.*;
-
-import static io.avaje.inject.generator.APContext.*;
-import static java.util.stream.Collectors.toSet;
 
 final class ProcessingContext {
 
@@ -34,6 +50,7 @@ static final class Ctx {
     private final List<TypeElement> delayQueue = new ArrayList<>();
     private final Set<String> spiServices = new TreeSet<>();
     private final Set<String> externalSpi = new TreeSet<>();
+    private final AllScopes scopes = new AllScopes();
     private boolean strictWiring;
     private final boolean mergeServices = APContext.getOption("mergeServices").map(Boolean::valueOf).orElse(true);
 
@@ -103,6 +120,10 @@ private static List<String> loadMetaInf(String fullName) {
     return Collections.emptyList();
   }
 
+  static AllScopes allScopes() {
+    return CTX.get().scopes;
+  }
+
   static void addInjectSPI(String type) {
     CTX.get().spiServices.add(type);
   }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java
index 49a8b4893..9596791d7 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java
@@ -1,9 +1,23 @@
 package io.avaje.inject.generator;
 
-import static io.avaje.inject.generator.ProcessingContext.*;
-import static io.avaje.inject.generator.APContext.*;
+import static io.avaje.inject.generator.APContext.createSourceFile;
+import static io.avaje.inject.generator.APContext.logError;
+import static io.avaje.inject.generator.APContext.logNote;
+import static io.avaje.inject.generator.APContext.logWarn;
+import static io.avaje.inject.generator.ProcessingContext.addModule;
+import static io.avaje.inject.generator.ProcessingContext.isDuplicateModule;
+
 import java.io.IOException;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 import javax.annotation.processing.FilerException;
@@ -163,6 +177,10 @@ String name() {
     return name;
   }
 
+  String scopeAnnotationFQN(){
+    return annotationType.getQualifiedName().toString();
+  }
+
   Set<String> requires() {
     return requires;
   }
@@ -175,6 +193,23 @@ Set<String> pluginProvided() {
     return pluginProvided;
   }
 
+  List<ScopeInfo> dependentScopes() {
+    final List<ScopeInfo> depScopes = new ArrayList<>();
+    requires().stream()
+      .map(APContext::typeElement)
+      .filter(e -> e.getKind() == ElementKind.ANNOTATION_TYPE)
+      .filter(ScopePrism::isPresent)
+      .map(TypeElement::getQualifiedName)
+      .map(Object::toString)
+      .map(scopes::get)
+      .filter(Objects::nonNull)
+      .flatMap(scope -> scope.dependentScopes().stream())
+      .forEach(depScopes::add);
+
+    depScopes.add(this);
+    return List.copyOf(depScopes);
+  }
+
   void writeBeanHelpers() {
     for (BeanReader beanReader : beanReaders) {
       try {
@@ -239,7 +274,7 @@ void writeModule() {
     }
     try {
       SimpleModuleWriter factoryWriter = new SimpleModuleWriter(ordering, this);
-      factoryWriter.write(type());
+      factoryWriter.write();
       moduleWritten = true;
     } catch (FilerException e) {
       logWarn("FilerException trying to write factory %s", e.getMessage());
@@ -255,6 +290,10 @@ Type type() {
     return annotationType == null ? Type.DEFAULT : Constants.TESTSCOPE.equals(annotationType.getQualifiedName().toString()) ? Type.TEST : Type.CUSTOM;
   }
 
+  UType scopeAnnotation() {
+    return UType.parse(annotationType.asType());
+  }
+
   /**
    * Merge the changed bean meta data into the existing (factory) metaData.
    */
@@ -492,7 +531,7 @@ private boolean providesDependencyRecursive(String dependency) {
     for (String require : requires) {
       final ScopeInfo requiredScope = scopes.get(require);
       // recursively search parent scope
-      if ((requiredScope != null) && requiredScope.providesDependencyRecursive(dependency)) {
+      if (requiredScope != null && requiredScope.providesDependencyRecursive(dependency)) {
         // logWarn("dependency " + dependency + " provided by other scope " + requiredScope.name);
         return true;
       }
@@ -557,8 +596,7 @@ Set<String> initModuleDependencies(Set<String> importTypes) {
       return importTypes;
     }
     for (String require : requires) {
-      final ScopeInfo otherScope = scopes.get(require);
-      if (otherScope == null) {
+      if (!ScopePrism.isPresent(APContext.typeElement(require))) {
         importTypes.add(require);
         final String type = Util.shortName(require);
         final String var = Util.initLower(type);
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
index 73bee4a8e..2fe9f5b78 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
@@ -1,24 +1,23 @@
 package io.avaje.inject.generator;
 
-import static io.avaje.inject.generator.APContext.logError;
-import static io.avaje.inject.generator.APContext.typeElement;
-import static io.avaje.inject.generator.ProcessingContext.createMetaInfWriterFor;
+import static io.avaje.inject.generator.APContext.*;
+import static io.avaje.inject.generator.ProcessingContext.*;
+import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
 import java.io.IOException;
 import java.io.Writer;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
+import java.util.*;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import javax.lang.model.element.ElementKind;
 import javax.lang.model.element.Modifier;
 import javax.lang.model.element.TypeElement;
 import javax.tools.FileObject;
 
+import io.avaje.inject.generator.ScopeInfo.Type;
+
 /**
  * Write the source code for the factory.
  */
@@ -55,6 +54,7 @@ final class SimpleModuleWriter {
   private final String fullName;
   private final ScopeInfo scopeInfo;
   private final MetaDataOrdering ordering;
+  private final Type scopeType;
   private final Set<String> duplicateTypes;
 
   private Append writer;
@@ -65,6 +65,8 @@ final class SimpleModuleWriter {
     this.modulePackage = scopeInfo.modulePackage();
     this.shortName = scopeInfo.moduleShortName();
     this.fullName = scopeInfo.moduleFullName();
+    this.scopeType = scopeInfo.type();
+
     final Set<String> seen = new HashSet<>();
     this.duplicateTypes =
       ordering.ordered().stream()
@@ -74,19 +76,88 @@ final class SimpleModuleWriter {
         .collect(toSet());
   }
 
-  void write(ScopeInfo.Type scopeType) throws IOException {
+  void write() throws IOException {
     writer = new Append(createFileWriter());
     writePackage();
     writeStartClass();
+
+    if (scopeType != ScopeInfo.Type.CUSTOM) {
+      writeServicesFile(scopeType);
+    } else {
+      writeRequiredModules();
+    }
     writeProvides();
     writeClassesMethod();
     writeBuildMethod();
     writeBuildMethods();
     writeEndClass();
     writer.close();
-    if (scopeType != ScopeInfo.Type.CUSTOM) {
-      writeServicesFile(scopeType);
+  }
+
+  private void writeRequiredModules() {
+    var directScopes =
+      scopeInfo.requires().stream()
+        .map(APContext::typeElement)
+        .filter(ScopePrism::isPresent)
+        .filter(e -> e.getKind() == ElementKind.ANNOTATION_TYPE)
+        .map(TypeElement::getQualifiedName)
+        .map(Object::toString)
+        .map(allScopes()::get)
+        .collect(toList());
+
+    var dependentScopes =
+      directScopes.stream()
+        .filter(Objects::nonNull)
+        .flatMap(scope -> scope.dependentScopes().stream())
+        .collect(toList());
+
+    if (hasExternalDependency(directScopes, dependentScopes)) {
+      // don't write if dependent scopes have constructor params or external module
+      return;
     }
+
+    final Set<String> requiredModules = new LinkedHashSet<>();
+    dependentScopes.stream()
+      .map(ScopeInfo::moduleFullName)
+      .filter(Objects::nonNull)
+      .filter(Predicate.not(String::isBlank))
+      .forEach(requiredModules::add);
+
+    final Map<String, String> dependencies = new LinkedHashMap<>(scopeInfo.constructorDependencies());
+
+    writer.append("  public static AvajeModule[] allRequiredModules(");
+    boolean comma = false;
+    for (Map.Entry<String, String> entry : dependencies.entrySet()) {
+      if (!comma) {
+        comma = true;
+      } else {
+        writer.append(", ");
+      }
+      writer.append(entry.getKey()).append(" ").append(entry.getValue());
+    }
+
+    writer.append(") {").eol();
+    writer.append("    return new AvajeModule[] {").eol();
+    for (String rawType : requiredModules) {
+      writer.append("      new %s(),", rawType).eol();
+    }
+    writer.append("      new %s(", shortName);
+    writer.append(String.join(", ", scopeInfo.constructorDependencies().values()));
+    writer.append(")").eol();
+    writer.append("    };").eol();
+    writer.append("  }").eol().eol();
+  }
+
+  private boolean hasExternalDependency(List<ScopeInfo> directScopes, List<ScopeInfo> dependentScopes) {
+    if (directScopes.contains(null)) {
+      return true;
+    }
+    for (var scope : dependentScopes) {
+      if (scope.requires().stream().map(allScopes()::get).anyMatch(Objects::isNull)) {
+        return true;
+      }
+    }
+    return false;
   }
 
   private void writeServicesFile(ScopeInfo.Type scopeType) {
@@ -110,6 +181,11 @@ private void writeProvides() {
     final Set<String> autoProvidesAspects = new TreeSet<>();
     final Set<String> autoProvides = new TreeSet<>();
 
+    if (scopeType == ScopeInfo.Type.CUSTOM) {
+      autoProvides.add(scopeInfo.scopeAnnotationFQN());
+      autoProvides.add(shortName);
+    }
+
     for (MetaData metaData : ordering.ordered()) {
       final String aspect = metaData.providesAspect();
       if (aspect != null && !aspect.isEmpty()) {
diff --git a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
index 2fc52a275..813ac0b94 100644
--- a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
@@ -251,8 +251,9 @@ public BeanScope build() {
 
     // sort factories by dependsOn
     ModuleOrdering factoryOrder = new FactoryOrder(parent, includeModules, !suppliedBeans.isEmpty());
-    var modules = serviceLoader.modules();
-    if (factoryOrder.isEmpty()) {
+
+    if (includeModules.isEmpty()) {
+      var modules = serviceLoader.modules();
       // prefer generated ModuleOrdering if provided
       factoryOrder = serviceLoader.moduleOrdering()
         .filter(o -> o.supportsExpected(modules))
@@ -308,15 +309,13 @@ static class FactoryOrder implements ModuleOrdering {
     private final List<AvajeModule> factories = new ArrayList<>();
     private final List<FactoryState> queue = new ArrayList<>();
     private final List<FactoryState> queueNoDependencies = new ArrayList<>();
-
     private final Map<String, FactoryList> providesMap = new HashMap<>();
 
     FactoryOrder(BeanScope parent, Set<AvajeModule> includeModules, boolean suppliedBeans) {
       this.parent = parent;
-      this.factories.addAll(includeModules);
       this.suppliedBeans = suppliedBeans;
       for (final AvajeModule includeModule : includeModules) {
-        moduleNames.add(includeModule.getClass().getName());
+        add(includeModule);
       }
     }
 
@@ -417,7 +416,7 @@ private boolean notProvided(String dependency) {
         return false;
       }
       final var factoryList = providesMap.get(dependency);
-      return (factoryList == null || !factoryList.allPushed());
+      return factoryList == null || !factoryList.allPushed();
     }
 
     /**


Reviews:
user: login: rbygrave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T10:39:27Z
user: login: SentryMan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T13:38:10Z
user: login: SentryMan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T13:41:02Z
user: login: rbygrave
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T10:45:15Z

Review comments:
diff_hunk: @@ -2,9 +2,9 @@
 
 import io.avaje.inject.InjectModule;
 import jakarta.inject.Scope;
-import org.multi.modb.BeanInModB;
 
 @Scope
-@InjectModule(requires = {ModAScope.class, ModBScope.class}, strictWiring = true)
-public @interface CrossCutScope {
-}
+@InjectModule(
+    requires = {ModAScope.class, ModBScope.class},
+    provides = CrossCutScope.class)
path: blackbox-multi-scope/src/main/java/org/multi/scope/CrossCutScope.java
user: login: rbygrave
body: To me the `provides` should **_not_** be needed ultimately. To me we've needed to add them here ... but ultimately that wouldn't be desirable. 
created_at: 2025-04-11T10:25:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,24 +1,97 @@
 package org.multi.crosscut;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import io.avaje.inject.BeanScope;
-import org.junit.jupiter.api.Test;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Named;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.multi.many.BeanInMany;
+import org.multi.many.ManyModule;
+import org.multi.moda.BeanInModA;
+import org.multi.moda.ModAModule;
+import org.multi.modb.BOther;
 import org.multi.modb.BeanInModB;
+import org.multi.modb.ModBModule;
+import org.multi.modc.BeanInModC;
+import org.multi.modc.COther;
+import org.multi.modc.ModCModule;
+import org.multi.modd.BeanInModD;
+import org.multi.modd.ModDModule;
+import org.multi.mode.BeanInModE;
+import org.multi.mode.ModEModule;
 
-import static org.assertj.core.api.Assertions.assertThat;
+import io.avaje.inject.BeanScope;
+import io.avaje.inject.spi.AvajeModule;
 
 class BeanCrossTest {
 
-  @Test
-  void bootstrap() {
+  private static Stream<Arguments> allModulesDefined() {
path: blackbox-multi-scope/src/test/java/org/multi/crosscut/BeanCrossTest.java
user: login: rbygrave
body: IMO these tests are not what we ergonomically desire/need.

That is, we want to say "Give me all the modules that I need for CrossCutModule [and ideally give them in the correct order]" ... and use those to create a BeanScope.

These tests are more just ... we can order the given list of modules correctly.
created_at: 2025-04-11T10:39:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,24 +1,97 @@
 package org.multi.crosscut;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import io.avaje.inject.BeanScope;
-import org.junit.jupiter.api.Test;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Named;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.multi.many.BeanInMany;
+import org.multi.many.ManyModule;
+import org.multi.moda.BeanInModA;
+import org.multi.moda.ModAModule;
+import org.multi.modb.BOther;
 import org.multi.modb.BeanInModB;
+import org.multi.modb.ModBModule;
+import org.multi.modc.BeanInModC;
+import org.multi.modc.COther;
+import org.multi.modc.ModCModule;
+import org.multi.modd.BeanInModD;
+import org.multi.modd.ModDModule;
+import org.multi.mode.BeanInModE;
+import org.multi.mode.ModEModule;
 
-import static org.assertj.core.api.Assertions.assertThat;
+import io.avaje.inject.BeanScope;
+import io.avaje.inject.spi.AvajeModule;
 
 class BeanCrossTest {
 
-  @Test
-  void bootstrap() {
+  private static Stream<Arguments> allModulesDefined() {
path: blackbox-multi-scope/src/test/java/org/multi/crosscut/BeanCrossTest.java
user: login: SentryMan
body: > These tests are more just ... we can order the given list of modules correctly.

do we have an existing test for that?
created_at: 2025-04-11T13:38:10Z
author_association: COLLABORATOR
diff_hunk: @@ -2,9 +2,9 @@
 
 import io.avaje.inject.InjectModule;
 import jakarta.inject.Scope;
-import org.multi.modb.BeanInModB;
 
 @Scope
-@InjectModule(requires = {ModAScope.class, ModBScope.class}, strictWiring = true)
-public @interface CrossCutScope {
-}
+@InjectModule(
+    requires = {ModAScope.class, ModBScope.class},
+    provides = CrossCutScope.class)
path: blackbox-multi-scope/src/main/java/org/multi/scope/CrossCutScope.java
user: login: SentryMan
body: Would auto-providing the scope annotation suffice?
created_at: 2025-04-11T13:41:02Z
author_association: COLLABORATOR

PR comments:
user: login: cbarlin
created_at: 2025-04-11T05:06:38Z
body: Looks great! Checking it out locally I can make it work with `strictWiring = true` on them as well (and make it work with a mix of some with and some without), if we wanted to add those to the test cases.

But yeah that solves the ordering problem nicely :)
author_association: CONTRIBUTOR
user: login: cbarlin
created_at: 2025-04-11T05:17:02Z
body: Thanks heaps @SentryMan! Did you want me to leave those other two PRs up?
author_association: CONTRIBUTOR
user: login: SentryMan
created_at: 2025-04-11T15:52:24Z
body: Now you need only the requires property
author_association: COLLABORATOR
user: login: cbarlin
created_at: 2025-04-12T20:12:45Z
body: Looks awesome - thank you so much! 
author_association: CONTRIBUTOR