Repository: avaje/avaje-inject
Pull request #259 (2023-01-12 01:47:02)

#258 - ENH: Add Plugin API for 'default providers' and BeanScopeBuilder.provideDefault() methods


Patch:
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/AllScopes.java b/inject-generator/src/main/java/io/avaje/inject/generator/AllScopes.java
index ca7c2464..5500e9e8 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/AllScopes.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/AllScopes.java
@@ -35,7 +35,7 @@ final class AllScopes {
     return data.scopeInfo;
   }
 
-  boolean providedByDefaultModule(String dependency) {
+  boolean providedByDefaultScope(String dependency) {
     return defaultScope.providesDependencyLocally(dependency);
   }
 
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java
index 71d70e67..b882e1da 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java
@@ -47,14 +47,14 @@ final class BeanReader {
     this.typeReader = new TypeReader(GenericType.parse(type), beanType, context, importTypes, factory);
     typeReader.process();
     this.requestParams = new BeanRequestParams(type);
-    this.name = typeReader.getName();
+    this.name = typeReader.name();
     this.aspects = typeReader.hasAspects();
-    this.injectMethods = typeReader.getInjectMethods();
-    this.injectFields = typeReader.getInjectFields();
-    this.factoryMethods = typeReader.getFactoryMethods();
-    this.postConstructMethod = typeReader.getPostConstructMethod();
-    this.preDestroyMethod = typeReader.getPreDestroyMethod();
-    this.constructor = typeReader.getConstructor();
+    this.injectMethods = typeReader.injectMethods();
+    this.injectFields = typeReader.injectFields();
+    this.factoryMethods = typeReader.factoryMethods();
+    this.postConstructMethod = typeReader.postConstructMethod();
+    this.preDestroyMethod = typeReader.preDestroyMethod();
+    this.constructor = typeReader.constructor();
   }
 
   @Override
@@ -62,7 +62,7 @@ final class BeanReader {
     return beanType.toString();
   }
 
-  TypeElement getBeanType() {
+  TypeElement beanType() {
     return beanType;
   }
 
@@ -93,11 +93,11 @@ final class BeanReader {
     return this;
   }
 
-  List<Dependency> getDependsOn() {
+  List<Dependency> dependsOn() {
     List<Dependency> list = new ArrayList<>();
     if (constructor != null) {
-      for (MethodReader.MethodParam param : constructor.getParams()) {
-        Dependency dependsOn = param.getDependsOn();
+      for (MethodReader.MethodParam param : constructor.params()) {
+        Dependency dependsOn = param.dependsOn();
         // BeanScope is always injectable with no impact on injection ordering
         if (!dependsOn.dependsOn().equals(Constants.BEANSCOPE)) {
           list.add(dependsOn);
@@ -107,23 +107,23 @@ final class BeanReader {
     return list;
   }
 
-  List<MethodReader> getFactoryMethods() {
+  List<MethodReader> factoryMethods() {
     return factoryMethods;
   }
 
-  List<String> getProvides() {
-    return typeReader.getProvides();
+  List<String> provides() {
+    return typeReader.provides();
   }
 
-  Set<GenericType> getGenericTypes() {
-    return typeReader.getGenericTypes();
+  String autoProvides() {
+    return typeReader.autoProvides();
   }
 
   Set<GenericType> allGenericTypes() {
     if (allGenericTypes != null) {
       return allGenericTypes;
     }
-    allGenericTypes = new LinkedHashSet<>(typeReader.getGenericTypes());
+    allGenericTypes = new LinkedHashSet<>(typeReader.genericTypes());
     for (FieldReader field : injectFields) {
       field.addDependsOnGeneric(allGenericTypes);
     }
@@ -133,7 +133,7 @@ final class BeanReader {
     if (constructor != null) {
       constructor.addDependsOnGeneric(allGenericTypes);
     }
-    for (MethodReader factoryMethod : getFactoryMethods()) {
+    for (MethodReader factoryMethod : factoryMethods()) {
       factoryMethod.addDependsOnGeneric(allGenericTypes);
     }
     return allGenericTypes;
@@ -149,7 +149,7 @@ final class BeanReader {
   /**
    * Return the key for meta data (type and name)
    */
-  String getMetaKey() {
+  String metaKey() {
     if (name != null) {
       return type + ":" + name;
     }
@@ -195,7 +195,7 @@ final class BeanReader {
     if (name != null && !name.isEmpty()) {
       writer.append("\"%s\", ", name);
     }
-    writer.append(typeReader.getTypesRegister());
+    writer.append(typeReader.typesRegister());
     writer.append(")) {").eol();
   }
 
@@ -249,7 +249,7 @@ final class BeanReader {
     aspects.extraImports(importTypes);
 
     for (MethodReader factoryMethod : factoryMethods) {
-      Set<GenericType> genericTypes = factoryMethod.getGenericTypes();
+      Set<GenericType> genericTypes = factoryMethod.genericTypes();
       if (!genericTypes.isEmpty()) {
         importTypes.add(Constants.TYPE);
         importTypes.add(Constants.GENERICTYPE);
@@ -292,7 +292,7 @@ final class BeanReader {
     writer.eol();
   }
 
-  MethodReader getConstructor() {
+  MethodReader constructor() {
     return constructor;
   }
 
@@ -349,7 +349,7 @@ final class BeanReader {
       field.writeRequestInject(writer);
     }
     for (MethodReader method : injectMethods) {
-      writer.append("    bean.%s(", method.getName());
+      writer.append("    bean.%s(", method.name());
       method.writeRequestConstructor(writer);
       writer.append(");").eol();
     }
@@ -357,12 +357,12 @@ final class BeanReader {
     writer.append("  }").eol();
   }
 
-  List<FieldReader> getInjectFields() {
-    return typeReader.getInjectFields();
+  List<FieldReader> injectFields() {
+    return typeReader.injectFields();
   }
 
-  List<MethodReader> getInjectMethods() {
-    return typeReader.getInjectMethods();
+  List<MethodReader> injectMethods() {
+    return typeReader.injectMethods();
   }
 
   boolean isGenerateProxy() {
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Dependency.java b/inject-generator/src/main/java/io/avaje/inject/generator/Dependency.java
index 5eb6bb76..c00fcb84 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Dependency.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Dependency.java
@@ -25,7 +25,7 @@ final class Dependency {
     return softDependency ? "soft:" + name : name;
   }
 
-  String getName() {
+  String name() {
     return name;
   }
 
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/ExternalProvide.java b/inject-generator/src/main/java/io/avaje/inject/generator/ExternalProvide.java
new file mode 100644
index 00000000..25cb0bc4
--- /dev/null
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ExternalProvide.java
@@ -0,0 +1,36 @@
+package io.avaje.inject.generator;
+
+import io.avaje.inject.spi.Module;
+
+import java.util.HashSet;
+import java.util.ServiceLoader;
+import java.util.Set;
+
+/**
+ * The types provided by other modules in the classpath at compile time.
+ * <p>
+ * When we depend on these types they add to the module autoRequires() classes.
+ */
+final class ExternalProvide {
+
+  private final Set<String> providedTypes = new HashSet<>();
+
+  void init() {
+    for (Module module : ServiceLoader.load(Module.class)) {
+      for (Class<?> provide : module.provides()) {
+        providedTypes.add(provide.getCanonicalName());
+      }
+      for (Class<?> provide : module.autoProvides()) {
+        providedTypes.add(provide.getCanonicalName());
+      }
+    }
+  }
+
+  /**
+   * Return true if this type is provided by another module in the classpath.
+   * We will add it to autoRequires().
+   */
+  boolean provides(String type) {
+    return providedTypes.contains(type);
+  }
+}
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/FieldReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/FieldReader.java
index 1de3927d..96f7e75c 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/FieldReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/FieldReader.java
@@ -34,7 +34,7 @@ final class FieldReader {
     }
   }
 
-  String getFieldName() {
+  String fieldName() {
     return element.getSimpleName().toString();
   }
 
@@ -77,7 +77,7 @@ final class FieldReader {
   void writeRequestDependency(Append writer) {
     if (!requestParam) {
       // just add as field dependency
-      requestParamName = writer.nextName(getFieldName().toLowerCase());
+      requestParamName = writer.nextName(fieldName().toLowerCase());
       writer.append("  @Inject").eol().append("  ");
       type.writeShort(writer);
       writer.append(" %s;", requestParamName).eol().eol();
@@ -88,7 +88,7 @@ final class FieldReader {
    * Generate code to set bean field dependencies as part of BeanFactory create().
    */
   void writeRequestInject(Append writer) {
-    writer.append("    bean.%s = %s;", getFieldName(), requestParamName).eol();
+    writer.append("    bean.%s = %s;", fieldName(), requestParamName).eol();
   }
 
 }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/GenericType.java b/inject-generator/src/main/java/io/avaje/inject/generator/GenericType.java
index 62c0d0b3..7a108ad2 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/GenericType.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/GenericType.java
@@ -3,7 +3,6 @@ package io.avaje.inject.generator;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 
 /**
  * A type with generic parameters and potentially nested.
@@ -158,14 +157,14 @@ final class GenericType {
   /**
    * Return the main type.
    */
-  String getMainType() {
+  String mainType() {
     return mainType;
   }
 
   /**
    * Return the parameter types.
    */
-  List<GenericType> getParams() {
+  List<GenericType> params() {
     return params;
   }
 
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java b/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
index 50192816..c0c00eee 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
@@ -32,6 +32,13 @@ final class MetaData {
    */
   private List<Dependency> dependsOn;
 
+  /**
+   * Type deemed to be candidate for providing to another external module.
+   */
+  private String autoProvides;
+  private boolean generateProxy;
+  private boolean usesExternalDependency;
+
   MetaData(DependencyMeta meta) {
     this.type = meta.type();
     this.name = trimName(meta.name());
@@ -39,6 +46,7 @@ final class MetaData {
     this.method = meta.method();
     this.provides = asList(meta.provides());
     this.dependsOn = Stream.of(meta.dependsOn()).map(Dependency::new).collect(Collectors.toList());
+    this.autoProvides = meta.autoProvides();
   }
 
   MetaData(String type, String name) {
@@ -54,11 +62,19 @@ final class MetaData {
     return (name == null) ? type : type + ":" + name;
   }
 
+  /**
+   * Return true if this is a component with Aspects applied to it.
+   * This means this type doesn't have a $DI but instead we have the $Proxy$DI.
+   */
+  boolean isGenerateProxy() {
+    return generateProxy;
+  }
+
   private String trimName(String name) {
     return "".equals(name) ? null : name;
   }
 
-  String getBuildName() {
+  String buildName() {
     if (Util.isVoid(type)) {
       return "void_" + Util.trimMethod(method);
     } else {
@@ -71,7 +87,7 @@ final class MetaData {
     }
   }
 
-  public String getKey() {
+  public String key() {
     if (Util.isVoid(type)) {
       return "method:" + method;
     }
@@ -102,26 +118,32 @@ final class MetaData {
   }
 
   void update(BeanReader beanReader) {
-    this.provides = beanReader.getProvides();
-    this.dependsOn = beanReader.getDependsOn();
+    this.provides = beanReader.provides();
+    this.dependsOn = beanReader.dependsOn();
+    this.autoProvides = beanReader.autoProvides();
+    this.generateProxy = beanReader.isGenerateProxy();
   }
 
-  String getType() {
+  String type() {
     return type;
   }
 
-  List<String> getProvides() {
+  List<String> provides() {
     return provides;
   }
 
-  List<Dependency> getDependsOn() {
+  List<Dependency> dependsOn() {
     return dependsOn;
   }
 
+  String autoProvides() {
+    return autoProvides;
+  }
+
   /**
    * Return the top level package for the bean and the interfaces it implements.
    */
-  String getTopPackage() {
+  String topPackage() {
     if (method == null || method.isEmpty()) {
       return Util.packageOf(type);
     }
@@ -132,43 +154,51 @@ final class MetaData {
   void addImportTypes(Set<String> importTypes) {
     if (hasMethod()) {
       importTypes.add(Util.classOfMethod(method));
-    } else {
+    } else if (!generateProxy) {
       importTypes.add(type + Constants.DI);
     }
   }
 
-  String buildMethod(MetaDataOrdering ordering) {
-    StringBuilder sb = new StringBuilder(200);
-    sb.append("  @DependencyMeta(type=\"").append(type).append("\"");
+  void buildMethod(Append append) {
+    if (generateProxy) {
+      return;
+    }
+    if (usesExternalDependency) {
+      append.append("  // uses external dependency").append(NEWLINE);
+    }
+    append.append("  @DependencyMeta(type=\"").append(type).append("\"");
     if (name != null) {
-      sb.append(", name=\"").append(name).append("\"");
+      append.append(", name=\"").append(name).append("\"");
     }
     if (hasMethod()) {
-      sb.append(", method=\"").append(method).append("\"");
+      append.append(", method=\"").append(method).append("\"");
     }
     if (!provides.isEmpty()) {
-      appendProvides(sb, "provides", provides);
+      appendProvides(append, "provides", provides);
     }
     if (!dependsOn.isEmpty()) {
-      appendProvides(sb, "dependsOn", dependsOn.stream().map(Dependency::dependsOn).collect(Collectors.toList()));
+      appendProvides(append, "dependsOn", dependsOn.stream().map(Dependency::dependsOn).collect(Collectors.toList()));
     }
-    sb.append(")").append(NEWLINE);
-    sb.append("  protected void build_").append(getBuildName()).append("() {").append(NEWLINE);
+    if (autoProvides != null && !autoProvides.isEmpty()) {
+      append.append(", autoProvides=\"").append(autoProvides).append("\"");
+    }
+    append.append(")").append(NEWLINE);
+    append.append("  private void build_").append(buildName()).append("() {").append(NEWLINE);
     if (hasMethod()) {
-      sb.append("    ").append(Util.shortMethod(method)).append("(builder");
+      append.append("    ").append(Util.shortMethod(method)).append("(builder");
     } else {
-      sb.append("    ").append(shortType).append(Constants.DI).append(".build(builder");
+      append.append("    ").append(shortType).append(Constants.DI).append(".build(builder");
     }
-    sb.append(");").append(NEWLINE);
-    sb.append("  }").append(NEWLINE);
-    return sb.toString();
+    append.append(");").append(NEWLINE);
+    append.append("  }").append(NEWLINE);
+    append.eol();
   }
 
   private boolean hasMethod() {
     return method != null && !method.isEmpty();
   }
 
-  private void appendProvides(StringBuilder sb, String attribute, List<String> types) {
+  private void appendProvides(Append sb, String attribute, List<String> types) {
     sb.append(", ").append(attribute).append("={");
     for (int i = 0; i < types.size(); i++) {
       if (i > 0) {
@@ -193,4 +223,14 @@ final class MetaData {
     this.method = method;
   }
 
+  void setAutoProvides(String autoProvides) {
+    this.autoProvides = autoProvides;
+  }
+
+  /**
+   * This depends on a dependency that comes from another module in the classpath.
+   */
+  void markWithExternalDependency() {
+    usesExternalDependency = true;
+  }
 }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MetaDataOrdering.java b/inject-generator/src/main/java/io/avaje/inject/generator/MetaDataOrdering.java
index 67f35b1a..47b76290 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MetaDataOrdering.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MetaDataOrdering.java
@@ -17,6 +17,7 @@ final class MetaDataOrdering {
   private final Map<String, ProviderList> providers = new HashMap<>();
   private final List<DependencyLink> circularDependencies = new ArrayList<>();
   private final Set<String> missingDependencyTypes = new LinkedHashSet<>();
+  private final Set<String> autoRequires = new TreeSet<>();
 
   MetaDataOrdering(Collection<MetaData> values, ProcessingContext context, ScopeInfo scopeInfo) {
     this.context = context;
@@ -29,9 +30,9 @@ final class MetaDataOrdering {
         queue.add(metaData);
       }
       // register into map keyed by provider
-      providers.computeIfAbsent(metaData.getType(), s -> new ProviderList()).add(metaData);
-      for (String provide : metaData.getProvides()) {
-        providers.computeIfAbsent(provide, s -> new ProviderList()).add(metaData);
+      providerAdd(metaData.type()).add(metaData);
+      for (String provide : metaData.provides()) {
+        providerAdd(provide).add(metaData);
       }
     }
     externallyRequiredDependencies();
@@ -42,14 +43,26 @@ final class MetaDataOrdering {
    */
   private void externallyRequiredDependencies() {
     for (String requireType : scopeInfo.requires()) {
-      providers.computeIfAbsent(requireType, s -> new ProviderList());
+      providerAdd(requireType);
     }
+    for (String requireType : scopeInfo.pluginProvided()) {
+      providerAdd(requireType);
+    }
+  }
+
+  private ProviderList providerAdd(String requireType) {
+    return providers.computeIfAbsent(requireType, s -> new ProviderList());
   }
 
   int processQueue() {
     int count;
     do {
-      count = processQueueRound();
+      // first run without external dependencies from other modules
+      count = processQueueRound(false);
+    } while (count > 0);
+    do {
+      // run again including externally provided dependencies from other modules
+      count = processQueueRound(true);
     } while (count > 0);
 
     int remaining = queue.size();
@@ -67,12 +80,12 @@ final class MetaDataOrdering {
   private void detectCircularDependency(List<MetaData> remainder) {
     final List<DependencyLink> dependencyLinks = new ArrayList<>();
     for (MetaData metaData : remainder) {
-      final List<Dependency> dependsOn = metaData.getDependsOn();
+      final List<Dependency> dependsOn = metaData.dependsOn();
       if (dependsOn != null) {
         for (Dependency dependency : dependsOn) {
           final MetaData provider = findCircularDependency(remainder, dependency);
           if (provider != null) {
-            dependencyLinks.add(new DependencyLink(metaData, provider, dependency.getName()));
+            dependencyLinks.add(new DependencyLink(metaData, provider, dependency.name()));
           }
         }
       }
@@ -85,11 +98,11 @@ final class MetaDataOrdering {
 
   private MetaData findCircularDependency(List<MetaData> remainder, Dependency dependency) {
     for (MetaData metaData : remainder) {
-      if (metaData.getType().equals(dependency.getName())) {
+      if (metaData.type().equals(dependency.name())) {
         return metaData;
       }
-      final List<String> provides = metaData.getProvides();
-      if (provides != null && provides.contains(dependency.getName())) {
+      final List<String> provides = metaData.provides();
+      if (provides != null && provides.contains(dependency.name())) {
         return metaData;
       }
     }
@@ -120,11 +133,11 @@ final class MetaDataOrdering {
   }
 
   private void checkMissingDependencies(MetaData metaData) {
-    for (Dependency dependency : metaData.getDependsOn()) {
-      if (providers.get(dependency.getName()) == null && !scopeInfo.providedByOtherModule(dependency.getName())) {
-        TypeElement element = context.elementMaybe(metaData.getType());
-        context.logError(element, "No dependency provided for " + dependency + " on " + metaData.getType());
-        missingDependencyTypes.add(dependency.getName());
+    for (Dependency dependency : metaData.dependsOn()) {
+      if (providers.get(dependency.name()) == null && !scopeInfo.providedByOtherScope(dependency.name())) {
+        TypeElement element = context.elementMaybe(metaData.type());
+        context.logError(element, "No dependency provided for " + dependency + " on " + metaData.type());
+        missingDependencyTypes.add(dependency.name());
       }
     }
   }
@@ -139,7 +152,7 @@ final class MetaDataOrdering {
       if (!queue.isEmpty()) {
         context.logWarn("There are " + queue.size() + " beans with unsatisfied dependencies (assuming external dependencies)");
         for (MetaData m : queue) {
-          context.logWarn("Unsatisfied dependencies on %s dependsOn %s", m, m.getDependsOn());
+          context.logWarn("Unsatisfied dependencies on %s dependsOn %s", m, m.dependsOn());
         }
       }
     }
@@ -153,13 +166,13 @@ final class MetaDataOrdering {
     }
   }
 
-  private int processQueueRound() {
+  private int processQueueRound(boolean includeExternal) {
     // loop queue looking for entry that has all provides marked as included
     int count = 0;
     Iterator<MetaData> iterator = queue.iterator();
     while (iterator.hasNext()) {
       MetaData queuedMeta = iterator.next();
-      if (allDependenciesWired(queuedMeta)) {
+      if (allDependenciesWired(queuedMeta, includeExternal)) {
         orderedList.add(queuedMeta);
         queuedMeta.setWired();
         iterator.remove();
@@ -169,14 +182,19 @@ final class MetaDataOrdering {
     return count;
   }
 
-  private boolean allDependenciesWired(MetaData queuedMeta) {
-    for (Dependency dependency : queuedMeta.getDependsOn()) {
-      if (!Util.isProvider(dependency.getName())) {
+  private boolean allDependenciesWired(MetaData queuedMeta, boolean includeExternal) {
+    for (Dependency dependency : queuedMeta.dependsOn()) {
+      if (!Util.isProvider(dependency.name())) {
         // check non-provider dependency is satisfied
-        ProviderList providerList = providers.get(dependency.getName());
+        ProviderList providerList = providers.get(dependency.name());
         if (providerList == null) {
           if (!scopeInfo.providedByOther(dependency)) {
-            return false;
+            if (includeExternal && context.externallyProvided(dependency.name())) {
+              autoRequires.add(dependency.name());
+              queuedMeta.markWithExternalDependency();
+            } else {
+              return false;
+            }
           }
         } else {
           if (!providerList.isAllWired()) {
@@ -188,6 +206,10 @@ final class MetaDataOrdering {
     return true;
   }
 
+  Set<String> autoRequires() {
+    return autoRequires;
+  }
+
   List<MetaData> ordered() {
     return orderedList;
   }
@@ -200,23 +222,6 @@ final class MetaDataOrdering {
     return importTypes;
   }
 
-  /**
-   * Return the MetaData for the bean that provides the (generic interface) dependency.
-   */
-  MetaData findProviderOf(String depend) {
-    for (MetaData metaData : orderedList) {
-      List<String> provides = metaData.getProvides();
-      if (provides != null) {
-        for (String provide : provides) {
-          if (provide.equals(depend)) {
-            return metaData;
-          }
-        }
-      }
-    }
-    return null;
-  }
-
   /**
    * Return true if the beans with unsatisfied dependencies seem
    * to form a circular dependency.
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MetaTopPackage.java b/inject-generator/src/main/java/io/avaje/inject/generator/MetaTopPackage.java
index c493e475..fba91187 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MetaTopPackage.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MetaTopPackage.java
@@ -16,7 +16,7 @@ final class MetaTopPackage {
 
   private MetaTopPackage(Collection<MetaData> values) {
     for (MetaData metaData : values) {
-      topPackage = Util.commonParent(topPackage, metaData.getTopPackage());
+      topPackage = Util.commonParent(topPackage, metaData.topPackage());
     }
   }
 }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
index 848f1e67..76eea093 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
@@ -103,7 +103,7 @@ final class MethodReader {
       set.add(genericType);
     }
     if (typeReader != null) {
-      set.addAll(typeReader.getGenericTypes());
+      set.addAll(typeReader.genericTypes());
     }
   }
 
@@ -115,11 +115,11 @@ final class MethodReader {
     return this;
   }
 
-  String getName() {
+  String name() {
     return methodName;
   }
 
-  List<MethodParam> getParams() {
+  List<MethodParam> params() {
     return params;
   }
 
@@ -133,7 +133,8 @@ final class MethodReader {
       dependsOn.add(GenericType.trimWildcard(param.paramType));
     }
     metaData.setDependsOn(dependsOn);
-    metaData.setProvides(typeReader == null ? Collections.emptyList() : typeReader.getProvides());
+    metaData.setProvides(typeReader == null ? Collections.emptyList() : typeReader.provides());
+    metaData.setAutoProvides(typeReader == null ? null : typeReader.autoProvides());
     return metaData;
   }
 
@@ -162,7 +163,7 @@ final class MethodReader {
     writer.append(");").eol();
   }
 
-  public void builderAddBeanProvider(Append writer) {
+  void builderAddBeanProvider(Append writer) {
     if (isVoid) {
       writer.append("Error - void @Prototype method ?").eol();
       return;
@@ -240,8 +241,8 @@ final class MethodReader {
     }
   }
 
-  Set<GenericType> getGenericTypes() {
-    return typeReader == null ? Collections.emptySet() : typeReader.getGenericTypes();
+  Set<GenericType> genericTypes() {
+    return typeReader == null ? Collections.emptySet() : typeReader.genericTypes();
   }
 
   void buildAddFor(Append writer) {
@@ -253,7 +254,7 @@ final class MethodReader {
         writer.append("\"%s\", ", name);
       }
       if (typeReader != null) {
-        writer.append(typeReader.getTypesRegister());
+        writer.append(typeReader.typesRegister());
       }
     }
     writer.append(")) {").eol();
@@ -406,7 +407,7 @@ final class MethodReader {
       return isGenericType() && !isProvider();
     }
 
-    Dependency getDependsOn() {
+    Dependency dependsOn() {
       return new Dependency(paramType, utilType.isCollection());
     }
 
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 0a6d9dfe..cd63e604 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
@@ -28,6 +28,7 @@ final class ProcessingContext {
   private final Elements elementUtils;
   private final Types typeUtils;
   private final Set<String> uniqueModuleNames = new HashSet<>();
+  private final ExternalProvide externalProvide = new ExternalProvide();
 
   ProcessingContext(ProcessingEnvironment processingEnv) {
     this.processingEnv = processingEnv;
@@ -35,6 +36,7 @@ final class ProcessingContext {
     this.filer = processingEnv.getFiler();
     this.elementUtils = processingEnv.getElementUtils();
     this.typeUtils = processingEnv.getTypeUtils();
+    externalProvide.init();
   }
 
   /**
@@ -141,4 +143,7 @@ final class ProcessingContext {
     return uniqueModuleNames.contains(moduleFullName);
   }
 
+  boolean externallyProvided(String type) {
+    return externalProvide.provides(type);
+  }
 }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java b/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java
index 3f64485b..91c9900d 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java
@@ -4,6 +4,7 @@ import io.avaje.inject.Component;
 import io.avaje.inject.Factory;
 import io.avaje.inject.InjectModule;
 import io.avaje.inject.Prototype;
+import io.avaje.inject.spi.Plugin;
 import io.avaje.inject.spi.Proxy;
 import jakarta.inject.Scope;
 import jakarta.inject.Singleton;
@@ -18,6 +19,7 @@ import javax.lang.model.element.ElementKind;
 import javax.lang.model.element.TypeElement;
 import javax.lang.model.util.Elements;
 import java.util.LinkedHashSet;
+import java.util.ServiceLoader;
 import java.util.Set;
 
 public final class Processor extends AbstractProcessor {
@@ -43,6 +45,19 @@ public final class Processor extends AbstractProcessor {
     this.elementUtils = processingEnv.getElementUtils();
     this.allScopes = new AllScopes(context);
     this.defaultScope = allScopes.defaultScope();
+    registerPluginProvidedTypes();
+  }
+
+  /**
+   * Register types provided by the plugin so no compiler error when we have a dependency
+   * on these types and the only thing providing them is the plugin.
+   */
+  private void registerPluginProvidedTypes() {
+    for (Plugin plugin : ServiceLoader.load(Plugin.class)) {
+      for (Class<?> provide : plugin.provides()) {
+        defaultScope.pluginProvided(provide.getCanonicalName());
+      }
+    }
   }
 
   @Override
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 f965514b..0d99cfd5 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
@@ -45,6 +45,7 @@ final class ScopeInfo {
   private final List<BeanReader> beanReaders = new ArrayList<>();
   private final Set<String> readBeans = new HashSet<>();
   private final ProcessingContext context;
+  private final Set<String> pluginProvided = new HashSet<>();
   private final Set<String> requires = new LinkedHashSet<>();
   private final Set<String> provides = new LinkedHashSet<>();
   private final Set<String> requiresPackages = new LinkedHashSet<>();
@@ -90,6 +91,10 @@ final class ScopeInfo {
       '}';
   }
 
+  void pluginProvided(String pluginProvides) {
+    pluginProvided.add(pluginProvides);
+  }
+
   boolean includeSingleton() {
     return !ignoreSingleton;
   }
@@ -132,10 +137,6 @@ final class ScopeInfo {
     }
   }
 
-  TypeElement annotationType() {
-    return annotationType;
-  }
-
   JavaFileObject moduleFile() {
     return moduleFile;
   }
@@ -152,10 +153,6 @@ final class ScopeInfo {
     return moduleShortName;
   }
 
-  boolean isDefaultScope() {
-    return defaultScope;
-  }
-
   String name() {
     return name;
   }
@@ -172,24 +169,29 @@ final class ScopeInfo {
     return requires;
   }
 
+  Set<String> pluginProvided() {
+    return pluginProvided;
+  }
+
   void writeBeanHelpers() {
     for (BeanReader beanReader : beanReaders) {
       try {
         if (!beanReader.isWrittenToFile()) {
-          SimpleBeanWriter writer = new SimpleBeanWriter(beanReader, context);
-          writer.write();
           if (beanReader.isGenerateProxy()) {
             SimpleBeanProxyWriter proxyWriter = new SimpleBeanProxyWriter(beanReader, context);
             proxyWriter.write();
+          } else {
+            SimpleBeanWriter writer = new SimpleBeanWriter(beanReader, context);
+            writer.write();
           }
           beanReader.setWrittenToFile();
         }
       } catch (FilerException e) {
-        context.logWarn("FilerException to write $DI class " + beanReader.getBeanType() + " " + e.getMessage());
+        context.logWarn("FilerException to write $DI class " + beanReader.beanType() + " " + e.getMessage());
 
       } catch (IOException e) {
         e.printStackTrace();
-        context.logError(beanReader.getBeanType(), "Failed to write $DI class");
+        context.logError(beanReader.beanType(), "Failed to write $DI class");
       }
     }
   }
@@ -259,7 +261,7 @@ final class ScopeInfo {
   void mergeMetaData() {
     for (BeanReader beanReader : beanReaders) {
       if (!beanReader.isRequestScopedController()) {
-        MetaData metaData = this.metaData.get(beanReader.getMetaKey());
+        MetaData metaData = this.metaData.get(beanReader.metaKey());
         if (metaData == null) {
           addMeta(beanReader);
         } else {
@@ -274,9 +276,9 @@ final class ScopeInfo {
    */
   private void addMeta(BeanReader beanReader) {
     MetaData meta = beanReader.createMeta();
-    metaData.put(meta.getKey(), meta);
+    metaData.put(meta.key(), meta);
     for (MetaData methodMeta : beanReader.createFactoryMethodMeta()) {
-      metaData.put(methodMeta.getKey(), methodMeta);
+      metaData.put(methodMeta.key(), methodMeta);
     }
   }
 
@@ -307,7 +309,7 @@ final class ScopeInfo {
         context.logError("Missing @DependencyMeta on method " + simpleName);
       } else {
         final MetaData metaData = new MetaData(meta);
-        this.metaData.put(metaData.getKey(), metaData);
+        this.metaData.put(metaData.key(), metaData);
       }
     }
   }
@@ -369,32 +371,47 @@ final class ScopeInfo {
     writer.append("}");
   }
 
-  private void buildClassArray(Append writer, Set<String> values) {
-    writer.append("new Class<?>[]");
-    writer.append("{");
-    if (!values.isEmpty()) {
-      int c = 0;
-      for (String value : values) {
-        if (c++ > 0) {
-          writer.append(",");
-        }
-        writer.append(value).append(".class");
-      }
+  void buildProvides(Append writer) {
+    if (!provides.isEmpty()) {
+      buildProvidesMethod(writer, "provides", provides);
+    }
+    if (!requires.isEmpty()) {
+      buildProvidesMethod(writer, "requires", requires);
+    }
+    if (!requiresPackages.isEmpty()) {
+      buildProvidesMethod(writer, "requiresPackages", requiresPackages);
     }
-    writer.append("}");
   }
 
-  void buildFields(Append writer) {
-    writer.append("  private final Class<?>[] provides = ");
-    buildClassArray(writer, provides);
-    writer.append(";").eol();
-    writer.append("  private final Class<?>[] requires = ");
-    buildClassArray(writer, requires);
-    writer.append(";").eol();
-    writer.append("  private final Class<?>[] requiresPackages = ");
-    buildClassArray(writer, requiresPackages);
-    writer.append(";").eol();
-    writer.append("  private Builder builder;").eol().eol();
+  private void buildProvidesMethod(Append writer, String fieldName, Set<String> types) {
+    writer.append("  @Override").eol();
+    writer.append("  public Class<?>[] %s() { return %s; }", fieldName, fieldName).eol();
+    writer.append("  private final Class<?>[] %s = new Class<?>[]{", fieldName).eol();
+    for (String rawType : types) {
+      writer.append("    %s.class,", rawType).eol();
+    }
+    writer.append("  };").eol().eol();
+  }
+
+  void buildAutoProvides(Append writer, Set<String> autoProvides) {
+    autoProvides.removeAll(provides);
+    if (!autoProvides.isEmpty()) {
+      buildProvidesMethod(writer, "autoProvides", autoProvides);
+    }
+  }
+
+  void buildAutoProvidesAspects(Append writer, Set<String> autoProvidesAspects) {
+    autoProvidesAspects.removeAll(provides);
+    if (!autoProvidesAspects.isEmpty()) {
+      buildProvidesMethod(writer, "autoProvidesAspects", autoProvidesAspects);
+    }
+  }
+
+  void buildAutoRequires(Append writer, Set<String> autoRequires) {
+    autoRequires.removeAll(requires);
+    if (!autoRequires.isEmpty()) {
+      buildProvidesMethod(writer, "autoRequires", autoRequires);
+    }
   }
 
   void readModuleMetaData(TypeElement moduleType) {
@@ -418,11 +435,11 @@ final class ScopeInfo {
    * Return true if the scope is a custom scope and the dependency is provided
    * by the "default" module. We could/should move to be tighter here at some point.
    */
-  boolean providedByOtherModule(String dependency) {
+  boolean providedByOtherScope(String dependency) {
     if (defaultScope) {
       return false;
     }
-    if (scopes.providedByDefaultModule(dependency)) {
+    if (scopes.providedByDefaultScope(dependency)) {
       return true;
     }
     return providesDependencyRecursive(dependency);
@@ -453,14 +470,14 @@ final class ScopeInfo {
    * Return true if this module provides the dependency (non-recursive, local only).
    */
   boolean providesDependencyLocally(String dependency) {
-    if (requires.contains(dependency)) {
+    if (requires.contains(dependency) || pluginProvided.contains(dependency)) {
       return true;
     }
     for (MetaData meta : metaData.values()) {
-      if (dependency.equals(meta.getType())) {
+      if (dependency.equals(meta.type())) {
         return true;
       }
-      final List<String> provides = meta.getProvides();
+      final List<String> provides = meta.provides();
       if (provides != null && !provides.isEmpty()) {
         for (String provide : provides) {
           if (dependency.equals(provide)) {
@@ -483,8 +500,8 @@ final class ScopeInfo {
 
   boolean providedByOther(Dependency dependency) {
     return dependency.isSoftDependency()
-      || providedByPackage(dependency.getName())
-      || providedByOtherModule(dependency.getName());
+      || providedByPackage(dependency.name())
+      || providedByOtherScope(dependency.name());
   }
 
   Set<String> initModuleDependencies(Set<String> importTypes) {
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanProxyWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanProxyWriter.java
index a9c13406..00602687 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanProxyWriter.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanProxyWriter.java
@@ -20,7 +20,7 @@ final class SimpleBeanProxyWriter {
     this.beanReader = beanReader;
     this.context = context;
 
-    TypeElement origin = beanReader.getBeanType();
+    TypeElement origin = beanReader.beanType();
     this.originName = origin.getQualifiedName().toString();
     this.shortName = origin.getSimpleName().toString();
     this.packageName = Util.packageOf(originName);
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
index 77b72b8b..5e5ec5ad 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
@@ -29,7 +29,7 @@ final class SimpleBeanWriter {
   SimpleBeanWriter(BeanReader beanReader, ProcessingContext context) {
     this.beanReader = beanReader;
     this.context = context;
-    TypeElement origin = beanReader.getBeanType();
+    TypeElement origin = beanReader.beanType();
     this.originName = origin.getQualifiedName().toString();
     if (origin.getNestingKind().isNested()) {
       this.packageName = Util.nestedPackageOf(originName);
@@ -44,7 +44,7 @@ final class SimpleBeanWriter {
 
   private Writer createFileWriter() throws IOException {
     String originName = this.originName;
-    if (beanReader.getBeanType().getNestingKind().isNested()) {
+    if (beanReader.beanType().getNestingKind().isNested()) {
       originName = originName.replace(shortName, shortName.replace(".", "$"));
     }
     JavaFileObject jfo = context.createWriter(originName + suffix);
@@ -90,14 +90,14 @@ final class SimpleBeanWriter {
   }
 
   private void writeStaticFactoryBeanMethods() {
-    for (MethodReader factoryMethod : beanReader.getFactoryMethods()) {
+    for (MethodReader factoryMethod : beanReader.factoryMethods()) {
       writeFactoryBeanMethod(factoryMethod);
     }
   }
 
   private void writeFactoryBeanMethod(MethodReader method) {
     method.commentBuildMethod(writer);
-    writer.append("  public static void build_%s(%s builder) {", method.getName(), beanReader.builderType()).eol();
+    writer.append("  public static void build_%s(%s builder) {", method.name(), beanReader.builderType()).eol();
     method.buildAddFor(writer);
     writer.append(method.builderGetFactory()).eol();
     if (method.isProtoType()) {
@@ -113,9 +113,9 @@ final class SimpleBeanWriter {
   }
 
   private void writeStaticFactoryMethod() {
-    MethodReader constructor = beanReader.getConstructor();
+    MethodReader constructor = beanReader.constructor();
     if (constructor == null) {
-      context.logError(beanReader.getBeanType(), "Unable to determine constructor to use for %s? Add explicit @Inject to one of the constructors.", beanReader.getBeanType());
+      context.logError(beanReader.beanType(), "Unable to determine constructor to use for %s? Add explicit @Inject to one of the constructors.", beanReader.beanType());
       return;
     }
     writeBuildMethodStart();
@@ -178,8 +178,8 @@ final class SimpleBeanWriter {
   private void injectFields() {
     String bean = beanReader.prototype() ? "bean" : "$bean";
     String builder = beanReader.prototype() ? "builder" : "b";
-    for (FieldReader fieldReader : beanReader.getInjectFields()) {
-      String fieldName = fieldReader.getFieldName();
+    for (FieldReader fieldReader : beanReader.injectFields()) {
+      String fieldName = fieldReader.fieldName();
       String getDependency = fieldReader.builderGetDependency(builder);
       writer.append("        %s.%s = %s;", bean, fieldName, getDependency).eol();
     }
@@ -188,14 +188,14 @@ final class SimpleBeanWriter {
   private void injectMethods() {
     String bean = beanReader.prototype() ? "bean" : "$bean";
     String builder = beanReader.prototype() ? "builder" : "b";
-    for (MethodReader methodReader : beanReader.getInjectMethods()) {
-      writer.append("        %s.%s(", bean, methodReader.getName());
+    for (MethodReader methodReader : beanReader.injectMethods()) {
+      writer.append("        %s.%s(", bean, methodReader.name());
       writeMethodParams(builder, methodReader);
     }
   }
 
   private void writeMethodParams(String builderRef, MethodReader methodReader) {
-    List<MethodReader.MethodParam> methodParams = methodReader.getParams();
+    List<MethodReader.MethodParam> methodParams = methodReader.params();
     for (int i = 0; i < methodParams.size(); i++) {
       if (i > 0) {
         writer.append(", ");
@@ -224,7 +224,7 @@ final class SimpleBeanWriter {
       writer.append(Constants.AT_SINGLETON).eol();
     }
     String shortName = this.shortName;
-    if (beanReader.getBeanType().getNestingKind().isNested()) {
+    if (beanReader.beanType().getNestingKind().isNested()) {
       shortName = shortName.replace(".", "$");
     }
     writer.append("public final class ").append(shortName).append(suffix).append(" ");
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 9835da20..d0f955e3 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
@@ -65,6 +65,7 @@ final class SimpleModuleWriter {
     writer = new Append(createFileWriter());
     writePackage();
     writeStartClass();
+    writeProvides();
     writeClassesMethod();
     writeBuildMethod();
     writeBuildMethods();
@@ -89,12 +90,38 @@ final class SimpleModuleWriter {
     }
   }
 
+  private void writeProvides() {
+    Set<String> autoProvidesAspects = new TreeSet<>();
+    Set<String> autoProvides = new TreeSet<>();
+
+    for (MetaData metaData : ordering.ordered()) {
+      String forExternal = metaData.autoProvides();
+      if (forExternal != null && !forExternal.isEmpty()) {
+        if (Util.isAspectProvider(forExternal)) {
+          autoProvidesAspects.add(Util.extractAspectType(forExternal));
+        } else if (!forExternal.contains("<")) {
+          autoProvides.add(forExternal);
+        }
+      }
+    }
+    if (!autoProvides.isEmpty()) {
+      scopeInfo.buildAutoProvides(writer, autoProvides);
+    }
+    if (!autoProvidesAspects.isEmpty()) {
+      scopeInfo.buildAutoProvidesAspects(writer, autoProvidesAspects);
+    }
+    Set<String> autoRequires = ordering.autoRequires();
+    if (!autoRequires.isEmpty()) {
+      scopeInfo.buildAutoRequires(writer, autoRequires);
+    }
+  }
+
   private void writeClassesMethod() {
     Set<String> allClasses = distinctPublicClasses();
     writer.append("  @Override").eol();
     writer.append("  public Class<?>[] classes() {").eol();
     writer.append("    return new Class<?>[]{").eol();
-    for (String rawType : allClasses) {
+    for (String rawType : new TreeSet<>(allClasses)) {
       writer.append("      %s.class,", rawType).eol();
     }
     writer.append("    };").eol();
@@ -107,7 +134,7 @@ final class SimpleModuleWriter {
   private Set<String> distinctPublicClasses() {
     Set<String> publicClasses = new LinkedHashSet<>();
     for (MetaData metaData : ordering.ordered()) {
-      String rawType = metaData.getType();
+      String rawType = metaData.type();
       if (!"void".equals(rawType)) {
         String type = GenericType.parse(rawType).topType();
         TypeElement element = context.element(type);
@@ -130,7 +157,9 @@ final class SimpleModuleWriter {
     writer.append("    // create beans in order based on constructor dependencies").eol();
     writer.append("    // i.e. \"provides\" followed by \"dependsOn\"").eol();
     for (MetaData metaData : ordering.ordered()) {
-      writer.append("    build_%s();", metaData.getBuildName()).eol();
+      if (!metaData.isGenerateProxy()) {
+        writer.append("    build_%s();", metaData.buildName()).eol();
+      }
     }
     writer.append("  }").eol();
     writer.eol();
@@ -138,7 +167,7 @@ final class SimpleModuleWriter {
 
   private void writeBuildMethods() {
     for (MetaData metaData : ordering.ordered()) {
-      writer.append(metaData.buildMethod(ordering)).eol();
+      metaData.buildMethod(writer);
     }
   }
 
@@ -172,24 +201,11 @@ final class SimpleModuleWriter {
 
     String interfaceType = scopeInfo.type().type();
     writer.append("public final class %s implements %s {", shortName, interfaceType).eol().eol();
-    scopeInfo.buildFields(writer);
+    writer.append("  private Builder builder;").eol().eol();
     if (scopeInfo.addModuleConstructor()) {
       writeConstructor();
     }
-    writer.append("  @Override").eol();
-    writer.append("  public Class<?>[] provides() {").eol();
-    writer.append("    return provides;").eol();
-    writer.append("  }").eol().eol();
-
-    writer.append("  @Override").eol();
-    writer.append("  public Class<?>[] requires() {").eol();
-    writer.append("    return requires;").eol();
-    writer.append("  }").eol().eol();
-
-    writer.append("  @Override").eol();
-    writer.append("  public Class<?>[] requiresPackages() {").eol();
-    writer.append("    return requiresPackages;").eol();
-    writer.append("  }").eol().eol();
+    scopeInfo.buildProvides(writer);
   }
 
   private void writeWithBeans() {
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeAnnotationReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeAnnotationReader.java
index 3ebc6276..5a3f1c24 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeAnnotationReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeAnnotationReader.java
@@ -23,7 +23,7 @@ final class TypeAnnotationReader {
     this.context = context;
   }
 
-  List<String> getAnnotationTypes() {
+  List<String> annotationTypes() {
     return annotationTypes;
   }
 
@@ -31,7 +31,7 @@ final class TypeAnnotationReader {
     return qualifierName != null;
   }
 
-  String getQualifierName() {
+  String qualifierName() {
     return qualifierName;
   }
 
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsInjection.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsInjection.java
index 77818e6d..4ba423cc 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsInjection.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsInjection.java
@@ -155,7 +155,7 @@ final class TypeExtendsInjection {
   }
 
   void removeFromProvides(List<String> provides) {
-    MethodReader constructor = getConstructor();
+    MethodReader constructor = constructor();
     if (constructor != null) {
       constructor.removeFromProvides(provides);
     }
@@ -167,31 +167,31 @@ final class TypeExtendsInjection {
     }
   }
 
-  List<FieldReader> getInjectFields() {
+  List<FieldReader> injectFields() {
     List<FieldReader> list = new ArrayList<>(injectFields);
     Collections.reverse(list);
     return list;
   }
 
-  List<MethodReader> getInjectMethods() {
+  List<MethodReader> injectMethods() {
     List<MethodReader> list = new ArrayList<>(injectMethods.values());
     Collections.reverse(list);
     return list;
   }
 
-  List<MethodReader> getFactoryMethods() {
+  List<MethodReader> factoryMethods() {
     return factoryMethods;
   }
 
-  Element getPostConstructMethod() {
+  Element postConstructMethod() {
     return postConstructMethod;
   }
 
-  Element getPreDestroyMethod() {
+  Element preDestroyMethod() {
     return preDestroyMethod;
   }
 
-  MethodReader getConstructor() {
+  MethodReader constructor() {
     if (injectConstructor != null) {
       return injectConstructor;
     }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java
index 5c172619..966d9209 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java
@@ -1,6 +1,7 @@
 package io.avaje.inject.generator;
 
 import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
 import javax.lang.model.element.Modifier;
 import javax.lang.model.element.TypeElement;
 import javax.lang.model.type.TypeMirror;
@@ -22,6 +23,7 @@ final class TypeExtendsReader {
   private final List<String> providesTypes = new ArrayList<>();
   private final String beanSimpleName;
   private final String baseTypeRaw;
+  private final boolean baseTypeIsInterface;
   private boolean closeable;
   /**
    * The implied qualifier name based on naming convention.
@@ -35,13 +37,14 @@ final class TypeExtendsReader {
     this.extendsInjection = new TypeExtendsInjection(baseType, context, factory, importTypes);
     this.beanSimpleName = baseType.getSimpleName().toString();
     this.baseTypeRaw = Util.unwrapProvider(baseGenericType.toString());
+    this.baseTypeIsInterface = baseType.getKind() == ElementKind.INTERFACE;
   }
 
-  GenericType getBaseType() {
+  GenericType baseType() {
     return baseGenericType;
   }
 
-  String getQualifierName() {
+  String qualifierName() {
     return qualifierName;
   }
 
@@ -49,31 +52,41 @@ final class TypeExtendsReader {
     return extendsInjection.hasAspects();
   }
 
-  List<FieldReader> getInjectFields() {
-    return extendsInjection.getInjectFields();
+  List<FieldReader> injectFields() {
+    return extendsInjection.injectFields();
   }
 
-  List<MethodReader> getInjectMethods() {
-    return extendsInjection.getInjectMethods();
+  List<MethodReader> injectMethods() {
+    return extendsInjection.injectMethods();
   }
 
-  List<MethodReader> getFactoryMethods() {
-    return extendsInjection.getFactoryMethods();
+  List<MethodReader> factoryMethods() {
+    return extendsInjection.factoryMethods();
   }
 
-  Element getPostConstructMethod() {
-    return extendsInjection.getPostConstructMethod();
+  Element postConstructMethod() {
+    return extendsInjection.postConstructMethod();
   }
 
-  Element getPreDestroyMethod() {
-    return extendsInjection.getPreDestroyMethod();
+  Element preDestroyMethod() {
+    return extendsInjection.preDestroyMethod();
   }
 
-  MethodReader getConstructor() {
-    return extendsInjection.getConstructor();
+  MethodReader constructor() {
+    return extendsInjection.constructor();
   }
 
-  List<String> getProvides() {
+  String autoProvides() {
+    if (baseTypeIsInterface) {
+      return baseTypeRaw;
+    }
+    if (!interfaceTypes.isEmpty()) {
+      return interfaceTypes.get(0);
+    }
+    return null;
+  }
+
+  List<String> provides() {
     return providesTypes;
   }
 
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java
index d6ce68cd..7c8c2b7c 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java
@@ -33,12 +33,16 @@ final class TypeReader {
     this.annotationReader = new TypeAnnotationReader(beanType, context);
   }
 
-  String getTypesRegister() {
+  String typesRegister() {
     return typesRegister;
   }
 
-  List<String> getProvides() {
-    return extendsReader.getProvides();
+  List<String> provides() {
+    return extendsReader.provides();
+  }
+
+  String autoProvides() {
+    return extendsReader.autoProvides();
   }
 
   boolean isClosable() {
@@ -49,31 +53,31 @@ final class TypeReader {
     return extendsReader.hasAspects();
   }
 
-  List<FieldReader> getInjectFields() {
-    return extendsReader.getInjectFields();
+  List<FieldReader> injectFields() {
+    return extendsReader.injectFields();
   }
 
-  List<MethodReader> getInjectMethods() {
-    return extendsReader.getInjectMethods();
+  List<MethodReader> injectMethods() {
+    return extendsReader.injectMethods();
   }
 
-  List<MethodReader> getFactoryMethods() {
-    return extendsReader.getFactoryMethods();
+  List<MethodReader> factoryMethods() {
+    return extendsReader.factoryMethods();
   }
 
-  Element getPostConstructMethod() {
-    return extendsReader.getPostConstructMethod();
+  Element postConstructMethod() {
+    return extendsReader.postConstructMethod();
   }
 
-  Element getPreDestroyMethod() {
-    return extendsReader.getPreDestroyMethod();
+  Element preDestroyMethod() {
+    return extendsReader.preDestroyMethod();
   }
 
-  MethodReader getConstructor() {
-    return extendsReader.getConstructor();
+  MethodReader constructor() {
+    return extendsReader.constructor();
   }
 
-  Set<GenericType> getGenericTypes() {
+  Set<GenericType> genericTypes() {
     return genericTypes;
   }
 
@@ -85,23 +89,23 @@ final class TypeReader {
     initRegistrationTypes();
   }
 
-  String getName() {
+  String name() {
     Named named = beanType.getAnnotation(Named.class);
     if (named != null) {
       return named.value().toLowerCase();
     }
     if (annotationReader.hasQualifierName()) {
-      return annotationReader.getQualifierName();
+      return annotationReader.qualifierName();
     }
-    return extendsReader.getQualifierName();
+    return extendsReader.qualifierName();
   }
 
   private void initRegistrationTypes() {
     TypeAppender appender = new TypeAppender(importTypes);
-    appender.add(extendsReader.getBaseType());
-    appender.add(extendsReader.getProvides());
+    appender.add(extendsReader.baseType());
+    appender.add(extendsReader.provides());
     if (forBean) {
-      appender.add(annotationReader.getAnnotationTypes());
+      appender.add(annotationReader.annotationTypes());
     }
     this.genericTypes = appender.genericTypes();
     this.typesRegister = appender.asString();
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Util.java b/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
index da914cda..b256b397 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
@@ -9,10 +9,12 @@ import javax.lang.model.type.TypeMirror;
 
 final class Util {
 
+  static final String ASPECT_PROVIDER_PREFIX = "io.avaje.inject.aop.AspectProvider<";
   static final String PROVIDER_PREFIX = "jakarta.inject.Provider<";
   private static final String OPTIONAL_PREFIX = "java.util.Optional<";
   private static final String NULLABLE = "Nullable";
   private static final int PROVIDER_LENGTH = PROVIDER_PREFIX.length();
+  private static final int ASPECT_PROVIDER_LENGTH = ASPECT_PROVIDER_PREFIX.length();
 
   static boolean isVoid(String type) {
     return "void".equalsIgnoreCase(type);
@@ -141,6 +143,10 @@ final class Util {
     return UtilType.of(rawType.toString());
   }
 
+  static boolean isAspectProvider(String rawType) {
+    return rawType.startsWith(ASPECT_PROVIDER_PREFIX);
+  }
+
   static boolean isProvider(String rawType) {
     return rawType.startsWith(PROVIDER_PREFIX);
   }
@@ -149,11 +155,14 @@ final class Util {
     return rawType.substring(PROVIDER_LENGTH, rawType.length() - 1);
   }
 
+  static String extractAspectType(String rawType) {
+    return rawType.substring(ASPECT_PROVIDER_LENGTH, rawType.length() - 1);
+  }
+
   /**
    * Return the common parent package.
    */
   static String commonParent(String currentTop, String aPackage) {
-
     if (aPackage == null) return currentTop;
     if (currentTop == null) return aPackage;
     if (aPackage.startsWith(currentTop)) {
diff --git a/inject-generator/src/main/java/module-info.java b/inject-generator/src/main/java/module-info.java
index 6d386114..016c0f79 100644
--- a/inject-generator/src/main/java/module-info.java
+++ b/inject-generator/src/main/java/module-info.java
@@ -3,5 +3,8 @@ module io.avaje.inject.generator {
   requires java.compiler;
   requires io.avaje.inject;
 
+  uses io.avaje.inject.spi.Plugin;
+  uses io.avaje.inject.spi.Module;
+
   provides javax.annotation.processing.Processor with io.avaje.inject.generator.Processor;
 }
diff --git a/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java
index 8dcaa9be..a50914ec 100644
--- a/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java
@@ -2,6 +2,8 @@ package io.avaje.inject;
 
 import io.avaje.inject.spi.Module;
 import io.avaje.lang.NonNullApi;
+import io.avaje.lang.Nullable;
+import jakarta.inject.Provider;
 
 import java.lang.reflect.Type;
 import java.util.function.Consumer;
@@ -204,6 +206,31 @@ public interface BeanScopeBuilder {
    */
   <D> BeanScopeBuilder bean(Type type, D bean);
 
+  /**
+   * Add a supplied bean provider that acts as a default fallback for a dependency.
+   * <p>
+   * This provider is only called if nothing else provides the dependency. It effectively
+   * uses `@Secondary` priority.
+   *
+   * @param type     The type of the dependency
+   * @param provider The provider of the dependency.
+   */
+  default <D> BeanScopeBuilder provideDefault(Type type, Provider<D> provider) {
+    return provideDefault(null, type, provider);
+  }
+
+  /**
+   * Add a supplied bean provider that acts as a default fallback for a dependency.
+   * <p>
+   * This provider is only called if nothing else provides the dependency. It effectively
+   * uses `@Secondary` priority.
+   *
+   * @param name     The name qualifier
+   * @param type     The type of the dependency
+   * @param provider The provider of the dependency.
+   */
+  <D> BeanScopeBuilder provideDefault(@Nullable String name, Type type, Provider<D> provider);
+
   /**
    * Deprecated - migrate to bean().
    */
@@ -314,7 +341,7 @@ public interface BeanScopeBuilder {
      */
     @Deprecated
     default BeanScopeBuilder.ForTesting withMock(Class<?> type) {
-      return  mock(type);
+      return mock(type);
     }
 
     /**
diff --git a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
index 841c6410..0bc056b3 100644
--- a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
@@ -1,12 +1,11 @@
 package io.avaje.inject;
 
 import io.avaje.applog.AppLog;
-import io.avaje.inject.spi.Builder;
-import io.avaje.inject.spi.EnrichBean;
+import io.avaje.inject.spi.*;
 import io.avaje.inject.spi.Module;
-import io.avaje.inject.spi.SuppliedBean;
 import io.avaje.lang.NonNullApi;
 import io.avaje.lang.Nullable;
+import jakarta.inject.Provider;
 
 import java.lang.System.Logger.Level;
 import java.lang.reflect.Type;
@@ -17,11 +16,10 @@ import java.util.function.Consumer;
  * Build a bean scope with options for shutdown hook and supplying test doubles.
  */
 @NonNullApi
-class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
+final class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
 
   private static final System.Logger log = AppLog.getLogger("io.avaje.inject");
 
-  @SuppressWarnings("rawtypes")
   private final List<SuppliedBean> suppliedBeans = new ArrayList<>();
   @SuppressWarnings("rawtypes")
   private final List<EnrichBean> enrichBeans = new ArrayList<>();
@@ -84,6 +82,12 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
     return this;
   }
 
+  @Override
+  public <D> BeanScopeBuilder provideDefault(String name, Type type, Provider<D> provider) {
+    suppliedBeans.add(SuppliedBean.secondary(name, type, provider));
+    return this;
+  }
+
   @Override
   public BeanScopeBuilder classLoader(ClassLoader classLoader) {
     this.classLoader = classLoader;
@@ -143,10 +147,12 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
 
   @Override
   public BeanScope build() {
+    // load and apply plugins first
+    var loader = classLoader != null ? classLoader : Thread.currentThread().getContextClassLoader();
+    ServiceLoader.load(Plugin.class, loader).forEach(plugin -> plugin.apply(this));
     // sort factories by dependsOn
     FactoryOrder factoryOrder = new FactoryOrder(parent, includeModules, !suppliedBeans.isEmpty());
     if (factoryOrder.isEmpty()) {
-      var loader = classLoader != null ? classLoader : Thread.currentThread().getContextClassLoader();
       ServiceLoader.load(Module.class, loader).forEach(factoryOrder::add);
     }
 
@@ -205,17 +211,16 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
     void add(Module module) {
       FactoryState factoryState = new FactoryState(module);
       providesMap.computeIfAbsent(module.getClass().getTypeName(), s -> new FactoryList()).add(factoryState);
-      if (!factoryState.isProvidesEmpty()) {
-        for (Class<?> feature : module.provides()) {
-          providesMap.computeIfAbsent(feature.getTypeName(), s -> new FactoryList()).add(factoryState);
-        }
-      }
+      addFactoryProvides(factoryState, module.provides());
+      addFactoryProvides(factoryState, module.autoProvides());
+      addFactoryProvides(factoryState, module.autoProvidesAspects());
+
       if (factoryState.isRequiresEmpty()) {
-        if (factoryState.isProvidesEmpty()) {
-          // only has 'provides' so we can push this
+        if (factoryState.explicitlyProvides()) {
+          // push immediately when explicitly 'provides' with no 'requires'
           push(factoryState);
         } else {
-          // hold until after all the 'provides only' modules are added
+          // hold until after all the explicitly 'provides' modules are added
           queueNoDependencies.add(factoryState);
         }
       } else {
@@ -224,6 +229,12 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
       }
     }
 
+    private void addFactoryProvides(FactoryState factoryState, Class<?>[] provides) {
+      for (Class<?> feature : provides) {
+        providesMap.computeIfAbsent(feature.getTypeName(), s -> new FactoryList()).add(factoryState);
+      }
+    }
+
     /**
      * Push the factory onto the build order (the wiring order for modules).
      */
@@ -272,16 +283,9 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
         StringBuilder sb = new StringBuilder();
         for (FactoryState factory : queue) {
           sb.append("Module [").append(factory).append("] has unsatisfied");
-          for (Class<?> depModuleName : factory.requires()) {
-            if (notProvided(depModuleName.getTypeName())) {
-              sb.append(String.format(" requires [%s]", depModuleName.getTypeName()));
-            }
-          }
-          for (Class<?> depModuleName : factory.requiresPackages()) {
-            if (notProvided(depModuleName.getTypeName())) {
-              sb.append(String.format(" requiresPackages [%s]", depModuleName.getTypeName()));
-            }
-          }
+          unsatisfiedRequires(sb, factory.requires(), "requires");
+          unsatisfiedRequires(sb, factory.requiresPackages(), "requiresPackages");
+          unsatisfiedRequires(sb, factory.autoRequires(), "autoRequires");
         }
         sb.append(" - none of the loaded modules ").append(moduleNames).append(" have this in their @InjectModule( provides = ... ). ");
         if (parent != null) {
@@ -292,6 +296,14 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
       }
     }
 
+    private void unsatisfiedRequires(StringBuilder sb, Class<?>[] requiredType, String requires) {
+      for (Class<?> depModuleName : requiredType) {
+        if (notProvided(depModuleName.getTypeName())) {
+          sb.append(String.format(" %s [%s]", requires, depModuleName.getTypeName()));
+        }
+      }
+    }
+
     private boolean notProvided(String dependency) {
       if (parent != null && parent.contains(dependency)) {
         return false;
@@ -322,15 +334,16 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
     }
 
     /**
-     * Return true if the (module) dependencies are satisfied for this factory.
+     * Return true if the (module) requires dependencies are satisfied for this factory.
      */
     private boolean satisfiedDependencies(FactoryState factory) {
-      for (Class<?> dependency : factory.requires()) {
-        if (notProvided(dependency.getTypeName())) {
-          return false;
-        }
-      }
-      for (Class<?> dependency : factory.requiresPackages()) {
+      return satisfiedDependencies(factory.requires())
+        && satisfiedDependencies(factory.requiresPackages())
+        && satisfiedDependencies(factory.autoRequires());
+    }
+
+    private boolean satisfiedDependencies(Class<?>[] requires) {
+      for (Class<?> dependency : requires) {
         if (notProvided(dependency.getTypeName())) {
           return false;
         }
@@ -378,17 +391,21 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
       return factory.requiresPackages();
     }
 
+    Class<?>[] autoRequires() {
+      return factory.autoRequires();
+    }
+
     @Override
     public String toString() {
       return factory.getClass().getTypeName();
     }
 
     boolean isRequiresEmpty() {
-      return isEmpty(factory.requires()) && isEmpty(factory.requiresPackages());
+      return isEmpty(factory.requires()) && isEmpty(factory.requiresPackages()) && isEmpty(factory.autoRequires());
     }
 
-    boolean isProvidesEmpty() {
-      return isEmpty(factory.provides());
+    boolean explicitlyProvides() {
+      return !isEmpty(factory.provides());
     }
 
     private boolean isEmpty(@Nullable Class<?>[] values) {
diff --git a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
index 6bce8a17..f8836954 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
@@ -51,7 +51,7 @@ final class DBeanMap {
 
   private void addSuppliedBean(SuppliedBean supplied) {
     Type suppliedType = supplied.type();
-    DContextEntryBean entryBean = DContextEntryBean.of(supplied.source(), supplied.name(), supplied.priority());
+    DContextEntryBean entryBean = DContextEntryBean.supplied(supplied.source(), supplied.name(), supplied.priority());
     beans.computeIfAbsent(suppliedType.getTypeName(), s -> new DContextEntry()).add(entryBean);
     for (Class<?> anInterface : supplied.interfaces()) {
       beans.computeIfAbsent(anInterface.getTypeName(), s -> new DContextEntry()).add(entryBean);
diff --git a/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java b/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
index 376d3c48..ac2e6fa4 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
@@ -12,10 +12,6 @@ class DContextEntryBean {
 
   /**
    * Create taking into account if it is a Provider or the bean itself.
-   *
-   * @param source The bean itself or provider of the bean
-   * @param name The optional name for the bean
-   * @param flag The flag for primary, secondary or normal
    */
   static DContextEntryBean of(Object source, String name, int flag) {
     if (source instanceof Provider) {
@@ -25,6 +21,17 @@ class DContextEntryBean {
     }
   }
 
+  /**
+   * Create an entry with supplied Providers using a 'Once' / 'one instance' provider.
+   */
+  static DContextEntryBean supplied(Object source, String name, int flag) {
+    if (source instanceof Provider) {
+      return new OnceProvider((Provider<?>)source, name, flag);
+    } else {
+      return new DContextEntryBean(source, name, flag);
+    }
+  }
+
   static DContextEntryBean provider(boolean prototype, Provider<?> provider, String name, int flag) {
     return prototype ? new ProtoProvider(provider, name, flag) : new OnceProvider(provider, name, flag);
   }
diff --git a/inject/src/main/java/io/avaje/inject/spi/DependencyMeta.java b/inject/src/main/java/io/avaje/inject/spi/DependencyMeta.java
index 99fb06a5..3b934d0c 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DependencyMeta.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DependencyMeta.java
@@ -37,4 +37,11 @@ public @interface DependencyMeta {
    */
   String[] dependsOn() default {};
 
+  /**
+   * Type deemed to be reasonable to provide to external module.
+   * <p>
+   * Used to support multiple module wiring automatically (as alternative to using explicit InjectModule annotation).
+   */
+  String autoProvides() default "";
+
 }
diff --git a/inject/src/main/java/io/avaje/inject/spi/Module.java b/inject/src/main/java/io/avaje/inject/spi/Module.java
index dec3c0e4..5ceb70c4 100644
--- a/inject/src/main/java/io/avaje/inject/spi/Module.java
+++ b/inject/src/main/java/io/avaje/inject/spi/Module.java
@@ -1,24 +1,68 @@
 package io.avaje.inject.spi;
 
+import io.avaje.inject.InjectModule;
+
 /**
  * A Module that can be included in BeanScope.
  */
 public interface Module {
 
+  /**
+   * Empty array of classes.
+   */
+  Class<?>[] EMPTY_CLASSES = new Class<?>[]{};
+
   /**
    * Return the set of types this module explicitly provides to other modules.
    */
-  Class<?>[] provides();
+  default Class<?>[] provides() {
+    return EMPTY_CLASSES;
+  }
 
   /**
    * Return the types this module needs to be provided externally or via other modules.
    */
-  Class<?>[] requires();
+  default Class<?>[] requires() {
+    return EMPTY_CLASSES;
+  }
 
   /**
    * Return the packages this module needs to be provided via other modules.
    */
-  Class<?>[] requiresPackages();
+  default Class<?>[] requiresPackages() {
+    return EMPTY_CLASSES;
+  }
+
+  /**
+   * Return the classes that this module provides that we allow other modules to auto depend on.
+   * <p>
+   * This is a convenience when using multiple modules that is otherwise controlled manually by
+   * explicitly using {@link InjectModule#provides()}.
+   */
+  default Class<?>[] autoProvides() {
+    return EMPTY_CLASSES;
+  }
+
+  /**
+   * Return the aspects that this module provides.
+   * <p>
+   * This is a convenience when using multiple modules that we otherwise manually specify via
+   * {@link InjectModule#provides()}.
+   */
+  default Class<?>[] autoProvidesAspects() {
+    return EMPTY_CLASSES;
+  }
+
+  /**
+   * These are the classes that this module requires for wiring that are provided by other
+   * external modules (that are in the classpath at compile time).
+   * <p>
+   * This is a convenience when using multiple modules that is otherwise controlled manually by
+   * explicitly using {@link InjectModule#requires()} or {@link InjectModule#requiresPackages()}.
+   */
+  default Class<?>[] autoRequires() {
+    return EMPTY_CLASSES;
+  }
 
   /**
    * Return public classes of the beans that would be registered by this module.
diff --git a/inject/src/main/java/io/avaje/inject/spi/Plugin.java b/inject/src/main/java/io/avaje/inject/spi/Plugin.java
new file mode 100644
index 00000000..9ef3004a
--- /dev/null
+++ b/inject/src/main/java/io/avaje/inject/spi/Plugin.java
@@ -0,0 +1,24 @@
+package io.avaje.inject.spi;
+
+import io.avaje.inject.BeanScopeBuilder;
+import jakarta.inject.Provider;
+
+import java.lang.reflect.Type;
+
+/**
+ * A Plugin that can be applied when creating a bean scope.
+ * <p>
+ * Typically, a plugin might provide a default dependency via {@link BeanScopeBuilder#provideDefault(Type, Provider)}.
+ */
+public interface Plugin {
+
+  /**
+   * Return the classes that the plugin provides.
+   */
+  Class<?>[] provides();
+
+  /**
+   * Apply the plugin to the scope builder.
+   */
+  void apply(BeanScopeBuilder builder);
+}
diff --git a/inject/src/main/java/io/avaje/inject/spi/SuppliedBean.java b/inject/src/main/java/io/avaje/inject/spi/SuppliedBean.java
index 225dc267..694490fb 100644
--- a/inject/src/main/java/io/avaje/inject/spi/SuppliedBean.java
+++ b/inject/src/main/java/io/avaje/inject/spi/SuppliedBean.java
@@ -50,6 +50,14 @@ public class SuppliedBean {
     return new SuppliedBean(BeanEntry.SUPPLIED, name, type, source);
   }
 
+  /**
+   * Create a supplied bean with SECONDARY priority as a default fallback dependency that is
+   * only used when no other matching one is provided.
+   */
+  public static SuppliedBean secondary(String name, Type type, Object source) {
+    return new SuppliedBean(BeanEntry.SECONDARY, name, type, source);
+  }
+
   private SuppliedBean(int priority, String name, Type type, Object source) {
     this.priority = priority;
     this.name = name;
diff --git a/inject/src/main/java/module-info.java b/inject/src/main/java/module-info.java
index 2200e2b6..e1d6c35f 100644
--- a/inject/src/main/java/module-info.java
+++ b/inject/src/main/java/module-info.java
@@ -10,5 +10,6 @@ module io.avaje.inject {
   requires static org.mockito;
 
   uses io.avaje.inject.spi.Module;
+  uses io.avaje.inject.spi.Plugin;
 
 }


Test patch:
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/config/AppConfig.java b/blackbox-test-inject/src/main/java/org/example/myapp/config/AppConfig.java
index bb080824..c2e006eb 100644
--- a/blackbox-test-inject/src/main/java/org/example/myapp/config/AppConfig.java
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/config/AppConfig.java
@@ -65,7 +65,12 @@ public class AppConfig {
 
   @Bean
   public MyGen<String> myGen() {
-    return new MyGen<String>();
+    return new MyGen<>();
+  }
+
+  @Bean
+  MyInterface myInterface2() {
+    return new MyInterface() {};
   }
 
   public static class Builder {
@@ -88,6 +93,10 @@ public class AppConfig {
 
   }
 
+  public interface MyInterface {
+
+  }
+
   public static class MyPrim {
     public final String val;
     public MyPrim(String val) {
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_setupMethodStatic_Test.java b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_setupMethodStatic_Test.java
new file mode 100644
index 00000000..af22e151
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_setupMethodStatic_Test.java
@@ -0,0 +1,39 @@
+package org.example.myapp;
+
+import io.avaje.inject.BeanScopeBuilder;
+import io.avaje.inject.test.InjectExtension;
+import io.avaje.inject.test.Setup;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(InjectExtension.class)
+class InjectExtension_setupMethodStatic_Test {
+
+  static @Inject HelloService helloService;
+
+  static HelloData myTestDouble;
+
+  @Setup
+  static void setup(BeanScopeBuilder builder) {
+    myTestDouble = mock(HelloData.class);
+    when(myTestDouble.helloData()).thenReturn("MockedViaSetupMethod");
+    builder.bean(HelloData.class, myTestDouble);
+  }
+
+  @Test
+  void hello_1() {
+    assertEquals("hello+MockedViaSetupMethod", helloService.hello());
+  }
+
+  @Test
+  void hello_2() {
+    when(myTestDouble.helloData()).thenReturn("LocalMock_2");
+    assertEquals("hello+LocalMock_2", helloService.hello());
+  }
+
+}
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_setupMethod_Test.java b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_setupMethod_Test.java
new file mode 100644
index 00000000..30ab0df9
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_setupMethod_Test.java
@@ -0,0 +1,43 @@
+package org.example.myapp;
+
+import io.avaje.inject.BeanScopeBuilder;
+import io.avaje.inject.test.InjectExtension;
+import io.avaje.inject.test.Setup;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(InjectExtension.class)
+class InjectExtension_setupMethod_Test {
+
+  @Inject
+  HelloService helloService;
+
+  HelloData myTestDouble;
+
+  @Setup
+  void setup(BeanScopeBuilder builder) {
+    myTestDouble = mock(HelloData.class);
+    when(myTestDouble.helloData()).thenReturn("MockedViaSetupMethod");
+    builder.bean(HelloData.class, myTestDouble);
+  }
+
+  @Test
+  void hello_1() {
+    assertEquals("hello+MockedViaSetupMethod", helloService.hello());
+  }
+
+  @Test
+  void hello_2() {
+    when(myTestDouble.helloData()).thenReturn("LocalMock_2");
+    assertEquals("hello+LocalMock_2", helloService.hello());
+  }
+
+}
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaInjectStatic_Test.java b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaInjectStatic_Test.java
new file mode 100644
index 00000000..89beb613
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaInjectStatic_Test.java
@@ -0,0 +1,22 @@
+package org.example.myapp;
+
+import io.avaje.inject.test.InjectTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@InjectTest
+class InjectExtension_testDoubleViaInjectStatic_Test {
+
+  static @Inject HelloService helloService;
+
+  static @Inject HelloData myTestDouble = () -> "ImActuallyATestDouble";
+
+  @Test
+  void hello_1() {
+    assertEquals("hello+ImActuallyATestDouble", helloService.hello());
+    assertEquals("ImActuallyATestDouble", myTestDouble.helloData());
+  }
+
+}
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaInject_Test.java b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaInject_Test.java
new file mode 100644
index 00000000..2c14b719
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaInject_Test.java
@@ -0,0 +1,23 @@
+package org.example.myapp;
+
+import io.avaje.inject.test.InjectExtension;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@ExtendWith(InjectExtension.class)
+class InjectExtension_testDoubleViaInject_Test {
+
+  @Inject HelloService helloService;
+
+  @Inject HelloData myTestDouble = () -> "ImActuallyATestDouble";
+
+  @Test
+  void hello_1() {
+    assertEquals("hello+ImActuallyATestDouble", helloService.hello());
+    assertEquals("ImActuallyATestDouble", myTestDouble.helloData());
+  }
+
+}
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaMock2_Test.java b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaMock2_Test.java
new file mode 100644
index 00000000..87e9eeff
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaMock2_Test.java
@@ -0,0 +1,37 @@
+package org.example.myapp;
+
+import io.avaje.inject.test.InjectExtension;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(InjectExtension.class)
+class InjectExtension_testDoubleViaMock2_Test {
+
+  @Inject HelloService helloService;
+
+  @Mock HelloData mockData = initMe();
+
+  private HelloData initMe() {
+    HelloData helloData = Mockito.mock(HelloData.class);
+    when(helloData.helloData()).thenReturn("CreatedByInitMe");
+    return helloData;
+  }
+
+  @Test
+  void hello_1() {
+    assertEquals("hello+CreatedByInitMe", helloService.hello());
+  }
+
+  @Test
+  void hello_2() {
+    when(mockData.helloData()).thenReturn("LocalMock_2");
+    assertEquals("hello+LocalMock_2", helloService.hello());
+  }
+
+}
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaMockStatic_Test.java b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaMockStatic_Test.java
new file mode 100644
index 00000000..f747803b
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaMockStatic_Test.java
@@ -0,0 +1,37 @@
+package org.example.myapp;
+
+import io.avaje.inject.test.InjectExtension;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(InjectExtension.class)
+class InjectExtension_testDoubleViaMockStatic_Test {
+
+  static @Inject HelloService helloService;
+
+  static @Mock HelloData mockData = initMe();
+
+  private static HelloData initMe() {
+    HelloData helloData = Mockito.mock(HelloData.class);
+    when(helloData.helloData()).thenReturn("CreatedByInitMe");
+    return helloData;
+  }
+
+  @Test
+  void hello_1() {
+    assertEquals("hello+CreatedByInitMe", helloService.hello());
+  }
+
+  @Test
+  void hello_2() {
+    when(mockData.helloData()).thenReturn("LocalMock_2");
+    assertEquals("hello+LocalMock_2", helloService.hello());
+  }
+
+}
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaMock_Test.java b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaMock_Test.java
new file mode 100644
index 00000000..c3e1c19c
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/InjectExtension_testDoubleViaMock_Test.java
@@ -0,0 +1,25 @@
+package org.example.myapp;
+
+import io.avaje.inject.test.InjectExtension;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@ExtendWith(InjectExtension.class)
+class InjectExtension_testDoubleViaMock_Test {
+
+  @Inject HelloService helloService;
+
+  @Mock HelloData mockData = () -> "ImAConcreteTestDouble";
+
+  @Test
+  void hello_1() {
+    assertEquals("hello+ImAConcreteTestDouble", helloService.hello());
+    assertEquals("ImAConcreteTestDouble", mockData.helloData());
+  }
+
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/DependencyTest.java b/inject-generator/src/test/java/io/avaje/inject/generator/DependencyTest.java
index 562d6dcf..e7bac312 100644
--- a/inject-generator/src/test/java/io/avaje/inject/generator/DependencyTest.java
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/DependencyTest.java
@@ -11,13 +11,13 @@ class DependencyTest {
     Dependency dependency = new Dependency("org.foo.Bar", true);
 
     assertThat(dependency.isSoftDependency()).isTrue();
-    assertThat(dependency.getName()).isEqualTo("org.foo.Bar");
+    assertThat(dependency.name()).isEqualTo("org.foo.Bar");
     assertThat(dependency.dependsOn()).isEqualTo("soft:org.foo.Bar");
 
     Dependency dependencyFromMeta = new Dependency("soft:org.foo.Bar");
-    assertThat(dependencyFromMeta.getName()).isEqualTo("org.foo.Bar");
+    assertThat(dependencyFromMeta.name()).isEqualTo("org.foo.Bar");
     assertThat(dependencyFromMeta.isSoftDependency()).isTrue();
-    assertThat(dependencyFromMeta.getName()).isEqualTo(dependency.getName());
+    assertThat(dependencyFromMeta.name()).isEqualTo(dependency.name());
     assertThat(dependencyFromMeta.dependsOn()).isEqualTo(dependency.dependsOn());
   }
 
@@ -27,6 +27,6 @@ class DependencyTest {
 
     assertThat(dependency.dependsOn()).isEqualTo("org.foo.Bar");
     assertThat(dependency.isSoftDependency()).isFalse();
-    assertThat(dependency.getName()).isEqualTo("org.foo.Bar");
+    assertThat(dependency.name()).isEqualTo("org.foo.Bar");
   }
 }
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/GenericTypeTest.java b/inject-generator/src/test/java/io/avaje/inject/generator/GenericTypeTest.java
index 4299b5c2..463b0798 100644
--- a/inject-generator/src/test/java/io/avaje/inject/generator/GenericTypeTest.java
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/GenericTypeTest.java
@@ -43,10 +43,10 @@ class GenericTypeTest {
   void parse() {
     GenericType type = new GenericTypeParser("my.exa.Repo<T,K>").parse();
 
-    assertThat(type.getMainType()).isEqualTo("my.exa.Repo");
-    assertThat(type.getParams()).hasSize(2);
-    assertThat(type.getParams().get(0).getMainType()).isEqualTo("T");
-    assertThat(type.getParams().get(1).getMainType()).isEqualTo("K");
+    assertThat(type.mainType()).isEqualTo("my.exa.Repo");
+    assertThat(type.params()).hasSize(2);
+    assertThat(type.params().get(0).mainType()).isEqualTo("T");
+    assertThat(type.params().get(1).mainType()).isEqualTo("K");
   }
 
   @Test
@@ -54,8 +54,8 @@ class GenericTypeTest {
     GenericType type = GenericType.parse("my.exa.Repo");
     assertThat(type.shortName()).isEqualTo("Repo");
     assertThat(type.topType()).isEqualTo("my.exa.Repo");
-    assertThat(type.getMainType()).isNull();
-    assertThat(type.getParams()).isEmpty();
+    assertThat(type.mainType()).isNull();
+    assertThat(type.params()).isEmpty();
     assertThat(type.isGenericType()).isFalse();
     assertThat(type.isProviderType()).isFalse();
 
@@ -92,8 +92,8 @@ class GenericTypeTest {
   private void assertsForRepo(GenericType type) {
     assertThat(type.shortName()).isEqualTo("Repo");
     assertThat(type.topType()).isEqualTo("my.exa.Repo");
-    assertThat(type.getMainType()).isNull();
-    assertThat(type.getParams()).isEmpty();
+    assertThat(type.mainType()).isNull();
+    assertThat(type.params()).isEmpty();
     assertThat(type.isGenericType()).isFalse();
 
     ImportTypeMap importSet = new ImportTypeMap();
@@ -108,10 +108,10 @@ class GenericTypeTest {
     GenericType type = GenericType.parse("my.exa.Repo<my.Other>");
     assertThat(type.shortName()).isEqualTo("RepoOther");
     assertThat(type.topType()).isEqualTo("my.exa.Repo");
-    assertThat(type.getMainType()).isEqualTo("my.exa.Repo");
+    assertThat(type.mainType()).isEqualTo("my.exa.Repo");
     assertThat(type.isGenericType()).isTrue();
-    assertThat(type.getParams()).hasSize(1);
-    assertThat(type.getParams().get(0).getMainType()).isEqualTo("my.Other");
+    assertThat(type.params()).hasSize(1);
+    assertThat(type.params().get(0).mainType()).isEqualTo("my.Other");
 
     ImportTypeMap importSet = new ImportTypeMap();
     type.addImports(importSet);
@@ -124,22 +124,22 @@ class GenericTypeTest {
   void parse_withParams() {
     GenericType type = GenericType.parse("my.exa.Repo<my.d.Haz,java.lang.Long>");
 
-    assertThat(type.getMainType()).isEqualTo("my.exa.Repo");
+    assertThat(type.mainType()).isEqualTo("my.exa.Repo");
     assertThat(type.isGenericType()).isTrue();
-    assertThat(type.getParams()).hasSize(2);
-    assertThat(type.getParams().get(0).getMainType()).isEqualTo("my.d.Haz");
-    assertThat(type.getParams().get(1).getMainType()).isEqualTo("java.lang.Long");
+    assertThat(type.params()).hasSize(2);
+    assertThat(type.params().get(0).mainType()).isEqualTo("my.d.Haz");
+    assertThat(type.params().get(1).mainType()).isEqualTo("java.lang.Long");
   }
 
   @Test
   void parse_withParams2() {
     GenericType type = GenericType.parse("java.util.concurrent.ConcurrentMap<java.lang.String,my.d.Has>");
 
-    assertThat(type.getMainType()).isEqualTo("java.util.concurrent.ConcurrentMap");
+    assertThat(type.mainType()).isEqualTo("java.util.concurrent.ConcurrentMap");
     assertThat(type.isGenericType()).isTrue();
-    assertThat(type.getParams()).hasSize(2);
-    assertThat(type.getParams().get(0).getMainType()).isEqualTo("java.lang.String");
-    assertThat(type.getParams().get(1).getMainType()).isEqualTo("my.d.Has");
+    assertThat(type.params()).hasSize(2);
+    assertThat(type.params().get(0).mainType()).isEqualTo("java.lang.String");
+    assertThat(type.params().get(1).mainType()).isEqualTo("my.d.Has");
 
     ImportTypeMap importSet = new ImportTypeMap();
     type.addImports(importSet);
@@ -152,27 +152,27 @@ class GenericTypeTest {
   void parse_withExtendsParams() {
     GenericType type = new GenericTypeParser("my.exa.Repo<? extends my.d.Haz,java.lang.Long>").parse();
 
-    assertThat(type.getMainType()).isEqualTo("my.exa.Repo");
+    assertThat(type.mainType()).isEqualTo("my.exa.Repo");
     assertThat(type.isGenericType()).isTrue();
-    assertThat(type.getParams()).hasSize(2);
-    assertThat(type.getParams().get(0).getMainType()).isEqualTo("? extends my.d.Haz");
-    assertThat(type.getParams().get(1).getMainType()).isEqualTo("java.lang.Long");
+    assertThat(type.params()).hasSize(2);
+    assertThat(type.params().get(0).mainType()).isEqualTo("? extends my.d.Haz");
+    assertThat(type.params().get(1).mainType()).isEqualTo("java.lang.Long");
   }
 
   @Test
   void parse_withNestedParams() {
     GenericType type = new GenericTypeParser("my.exa.Repo<my.a.Prov<my.b.Haz>,my.a.Key<java.util.UUID>>").parse();
 
-    assertThat(type.getMainType()).isEqualTo("my.exa.Repo");
+    assertThat(type.mainType()).isEqualTo("my.exa.Repo");
     assertThat(type.isGenericType()).isTrue();
-    assertThat(type.getParams()).hasSize(2);
-    assertThat(type.getParams().get(0).getMainType()).isEqualTo("my.a.Prov");
-    assertThat(type.getParams().get(0).getParams()).hasSize(1);
-    assertThat(type.getParams().get(0).getParams().get(0).getMainType()).isEqualTo("my.b.Haz");
-
-    assertThat(type.getParams().get(1).getMainType()).isEqualTo("my.a.Key");
-    assertThat(type.getParams().get(1).getParams()).hasSize(1);
-    assertThat(type.getParams().get(1).getParams().get(0).getMainType()).isEqualTo("java.util.UUID");
+    assertThat(type.params()).hasSize(2);
+    assertThat(type.params().get(0).mainType()).isEqualTo("my.a.Prov");
+    assertThat(type.params().get(0).params()).hasSize(1);
+    assertThat(type.params().get(0).params().get(0).mainType()).isEqualTo("my.b.Haz");
+
+    assertThat(type.params().get(1).mainType()).isEqualTo("my.a.Key");
+    assertThat(type.params().get(1).params()).hasSize(1);
+    assertThat(type.params().get(1).params().get(0).mainType()).isEqualTo("java.util.UUID");
   }
 
   @Test
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/UtilTest.java b/inject-generator/src/test/java/io/avaje/inject/generator/UtilTest.java
index e77ab4f0..a0e75957 100644
--- a/inject-generator/src/test/java/io/avaje/inject/generator/UtilTest.java
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/UtilTest.java
@@ -34,6 +34,26 @@ class UtilTest {
     assertEquals(Util.commonParent("org.foo.web.foo", "org.foo.service.blah"), "org.foo");
   }
 
+  @Test
+  void isAspectProvider() {
+    assertTrue(Util.isAspectProvider("io.avaje.inject.aop.AspectProvider<org.Foo>"));
+    assertTrue(Util.isAspectProvider("io.avaje.inject.aop.AspectProvider<org.one.Bar"));
+  }
+
+  @Test
+  void isAspectProvider_not() {
+    assertFalse(Util.isAspectProvider("not.avaje.inject.aop.AspectProvider<org.Foo>"));
+    assertFalse(Util.isAspectProvider("io.avaje.inject.not.AspectProvider<org.one.Bar"));
+    assertFalse(Util.isAspectProvider("io.avaje.inject.aop.NotAspectProvider<org.one.Bar"));
+    assertFalse(Util.isAspectProvider("io.avaje.inject.aop.NotAspectProvider"));
+  }
+
+  @Test
+  void extractAspectType() {
+    assertEquals(Util.extractAspectType("io.avaje.inject.aop.AspectProvider<org.Foo>"), "org.Foo");
+    assertEquals(Util.extractAspectType("io.avaje.inject.aop.AspectProvider<org.one.Bar>"), "org.one.Bar");
+  }
+
   @Test
   void unwrapProvider() {
     assertEquals(Util.unwrapProvider("jakarta.inject.Provider<org.Foo<com.Bazz>>"), "org.Foo<com.Bazz>");
diff --git a/inject-test/src/main/java/io/avaje/inject/test/GlobalTestScope.java b/inject-test/src/main/java/io/avaje/inject/test/GlobalTestScope.java
new file mode 100644
index 00000000..40f03734
--- /dev/null
+++ b/inject-test/src/main/java/io/avaje/inject/test/GlobalTestScope.java
@@ -0,0 +1,60 @@
+package io.avaje.inject.test;
+
+import io.avaje.applog.AppLog;
+import io.avaje.inject.BeanScope;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import java.util.concurrent.locks.ReentrantLock;
+
+import static java.lang.System.Logger.Level.DEBUG;
+import static java.lang.System.Logger.Level.TRACE;
+
+/**
+ * Holds the global BeanScope used for all tests.
+ */
+final class GlobalTestScope implements  ExtensionContext.Store.CloseableResource {
+
+  private static final System.Logger log = AppLog.getLogger("io.avaje.inject");
+
+  private final ReentrantLock lock = new ReentrantLock();
+  private boolean started;
+  private BeanScope globalBeanScope;
+
+  BeanScope obtain(ExtensionContext context) {
+    lock.lock();
+    try {
+      if (!started) {
+        initialise(context);
+        started = true;
+      }
+      return globalBeanScope;
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  private void initialise(ExtensionContext context) {
+    globalBeanScope = TestBeanScope.init(false);
+    if (globalBeanScope != null) {
+      log.log(TRACE, "register global test BeanScope with beans {0}", globalBeanScope);
+      context.getRoot().getStore(ExtensionContext.Namespace.GLOBAL).put(InjectExtension.class.getCanonicalName(), this);
+    }
+  }
+
+  /**
+   * Global shutdown of JUnit.
+   */
+  @Override
+  public void close() {
+    lock.lock();
+    try {
+      if (globalBeanScope != null) {
+        log.log(DEBUG, "Closing global test BeanScope");
+        globalBeanScope.close();
+      }
+    } finally {
+      lock.unlock();
+    }
+  }
+
+}
diff --git a/inject-test/src/main/java/io/avaje/inject/test/InjectExtension.java b/inject-test/src/main/java/io/avaje/inject/test/InjectExtension.java
index 16db27b2..a01c71fd 100644
--- a/inject-test/src/main/java/io/avaje/inject/test/InjectExtension.java
+++ b/inject-test/src/main/java/io/avaje/inject/test/InjectExtension.java
@@ -7,65 +7,30 @@ import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
 
 import java.lang.System.Logger.Level;
 import java.lang.reflect.Method;
-import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * Junit 5 extension for avaje inject.
  * <p>
  * Supports injection for fields annotated with <code>@Mock, @Spy, @Captor, @Inject</code>.
  */
-public class InjectExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, ExtensionContext.Store.CloseableResource {
+public final class InjectExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback {
 
   private static final System.Logger log = AppLog.getLogger("io.avaje.inject");
   private static final Namespace INJECT_NS = Namespace.create("io.avaje.inject.InjectTest");
   private static final String BEAN_SCOPE = "BEAN_SCOPE";
   private static final String META = "META";
-  private static final ReentrantLock lock = new ReentrantLock();
-  private static boolean started;
-  private static BeanScope globalTestScope;
+  private static final GlobalTestScope GLOBAL = new GlobalTestScope();
 
-  /**
-   * Global shutdown of JUnit.
-   */
-  @Override
-  public void close() {
-    lock.lock();
-    try {
-      if (globalTestScope != null) {
-        log.log(Level.DEBUG, "Closing global test BeanScope");
-        globalTestScope.close();
-      }
-    } finally {
-      lock.unlock();
-    }
-  }
-
-  private void initialiseGlobalTestScope(ExtensionContext context) {
-    globalTestScope = TestBeanScope.init(false);
-    if (globalTestScope != null) {
-      log.log(Level.TRACE, "register global test BeanScope with beans {0}", globalTestScope);
-      context.getRoot().getStore(Namespace.GLOBAL).put(InjectExtension.class.getCanonicalName(), this);
-    }
-  }
+  private BeanScope globalBeanScope;
 
   @Override
   public void beforeAll(ExtensionContext context) {
-    lock.lock();
-    try {
-      if (!started) {
-        initialiseGlobalTestScope(context);
-        started = true;
-      }
-
-      final MetaInfo metaInfo = createMetaInfo(context);
-      putMetaInfo(context, metaInfo);
+    globalBeanScope = GLOBAL.obtain(context);
 
-      if (metaInfo.hasStaticInjection()) {
-        MetaInfo.Scope testClassBeanScope = metaInfo.buildForClass(globalTestScope);
-        putClassScope(context, testClassBeanScope);
-      }
-    } finally {
-      lock.unlock();
+    final MetaInfo metaInfo = createMetaInfo(context);
+    putMetaInfo(context, metaInfo);
+    if (metaInfo.hasStaticInjection()) {
+      putClassScope(context, metaInfo.buildForClass(globalBeanScope));
     }
   }
 
@@ -96,10 +61,10 @@ public class InjectExtension implements BeforeAllCallback, AfterAllCallback, Bef
   @Override
   public void beforeEach(final ExtensionContext context) {
     final MetaInfo metaInfo = getMetaInfo(context);
-    if (metaInfo.hasNormalInjection()) {
+    if (metaInfo.hasInstanceInjection()) {
 
       // if (static fields) then (class scope) else (globalTestScope)
-      final BeanScope parent = metaInfo.hasStaticInjection() ? getClassScope(context) : globalTestScope;
+      final BeanScope parent = metaInfo.hasStaticInjection() ? getClassScope(context) : globalBeanScope;
 
       AutoCloseable beanScope = metaInfo.buildForInstance(parent, context.getRequiredTestInstance());
 
diff --git a/inject-test/src/main/java/io/avaje/inject/test/MetaInfo.java b/inject-test/src/main/java/io/avaje/inject/test/MetaInfo.java
index 3656b089..d28f1361 100644
--- a/inject-test/src/main/java/io/avaje/inject/test/MetaInfo.java
+++ b/inject-test/src/main/java/io/avaje/inject/test/MetaInfo.java
@@ -6,7 +6,7 @@ import io.avaje.inject.BeanScopeBuilder;
 /**
  * Wraps the underlying metadata (fields with annotations @Mock, @Spy, @Inject, @Captor).
  */
-class MetaInfo {
+final class MetaInfo {
 
   private final MetaReader reader;
 
@@ -18,7 +18,7 @@ class MetaInfo {
     return reader.hasClassInjection();
   }
 
-  boolean hasNormalInjection() {
+  boolean hasInstanceInjection() {
     return reader.hasInstanceInjection();
   }
 
diff --git a/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java b/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java
index 6fd4bea0..1f0d11ae 100644
--- a/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java
+++ b/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java
@@ -11,7 +11,6 @@ import org.mockito.Mock;
 import org.mockito.Spy;
 import org.mockito.internal.configuration.plugins.Plugins;
 import org.mockito.internal.util.reflection.GenericMaster;
-import org.mockito.plugins.MemberAccessor;
 
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Field;
@@ -19,8 +18,9 @@ import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.List;
 
-class MetaReader {
+final class MetaReader {
 
+  private final SetupMethods methodFinder;
   final List<Field> captors = new ArrayList<>();
   final List<FieldTarget> mocks = new ArrayList<>();
   final List<FieldTarget> spies = new ArrayList<>();
@@ -37,17 +37,18 @@ class MetaReader {
 
   MetaReader(Class<?> testClass, Plugin plugin) {
     this.plugin = plugin;
+    this.methodFinder = new SetupMethods(testClass);
     for (Field field : testClass.getDeclaredFields()) {
       readField(field);
     }
   }
 
   boolean hasClassInjection() {
-    return classInjection;
+    return classInjection || methodFinder.hasStaticMethods();
   }
 
   boolean hasInstanceInjection() {
-    return instanceInjection;
+    return instanceInjection || methodFinder.hasInstanceMethods();
   }
 
   @Override
@@ -197,35 +198,69 @@ class MetaReader {
 
   void build(BeanScopeBuilder builder, Object testInstance) {
     if (testInstance != null) {
-      buildForInstance(builder);
+      buildForInstance(builder, testInstance);
     } else {
       buildForStatics(builder);
     }
   }
 
-  void buildForInstance(BeanScopeBuilder builder) {
-    final BeanScopeBuilder.ForTesting forTesting = builder.forTesting();
+  void buildForInstance(BeanScopeBuilder builder, Object testInstance) {
     for (FieldTarget target : mocks) {
-      forTesting.mock(target.type(), target.name());
+      registerMock(testInstance, builder, target);
     }
     for (FieldTarget target : spies) {
-      forTesting.spy(target.type(), target.name());
+      registerSpy(testInstance, builder, target);
     }
+    for (FieldTarget target : injection) {
+      Object existingValue = target.get(testInstance);
+      if (existingValue != null) {
+        registerAsTestDouble(builder, target, existingValue);
+      }
+    }
+    methodFinder.invokeInstance(builder, testInstance);
   }
 
   void buildForStatics(BeanScopeBuilder builder) {
-    final BeanScopeBuilder.ForTesting forTesting = builder.forTesting();
     for (FieldTarget target : staticMocks) {
-      forTesting.mock(target.type(), target.name());
+      registerMock(null, builder, target);
     }
     for (FieldTarget target : staticSpies) {
-      forTesting.spy(target.type(), target.name());
+      registerSpy(null, builder, target);
+    }
+    for (FieldTarget target : staticInjection) {
+      Object existingValue = target.get(null);
+      if (existingValue != null) {
+        registerAsTestDouble(builder, target, existingValue);
+      }
+    }
+    methodFinder.invokeStatics(builder);
+  }
+
+  private static void registerMock(Object testInstance, BeanScopeBuilder builder, FieldTarget target) {
+    Object existingValue = target.get(testInstance);
+    if (existingValue != null) {
+      registerAsTestDouble(builder, target, existingValue);
+    } else {
+      builder.forTesting().mock(target.type(), target.name());
+    }
+  }
+
+  private static void registerSpy(Object testInstance, BeanScopeBuilder builder, FieldTarget target) {
+    Object existingValue = target.get(testInstance);
+    if (existingValue != null) {
+      registerAsTestDouble(builder, target, existingValue);
+    } else {
+      builder.forTesting().spy(target.type(), target.name());
     }
   }
 
+  private static void registerAsTestDouble(BeanScopeBuilder builder, FieldTarget target, Object value) {
+    target.markAsProvided();
+    builder.bean(target.name(), target.type(), value);
+  }
+
   void set(Field field, Object val, Object testInstance) throws IllegalAccessException {
-    final MemberAccessor memberAccessor = Plugins.getMemberAccessor();
-    memberAccessor.set(field, testInstance, val);
+    Plugins.getMemberAccessor().set(field, testInstance, val);
   }
 
   class FieldTarget {
@@ -234,6 +269,7 @@ class MetaReader {
     private final String name;
     private final boolean isStatic;
     private boolean pluginInjection;
+    private boolean valueAlreadyProvided;
 
     FieldTarget(Field field, String name) {
       this.field = field;
@@ -258,8 +294,18 @@ class MetaReader {
       return isStatic;
     }
 
+    Object get(Object instance) {
+      try {
+        return Plugins.getMemberAccessor().get(field, instance);
+      } catch (IllegalAccessException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
     void setFromScope(BeanScope beanScope, Object testInstance) throws IllegalAccessException {
-      set(field, beanScope.get(type(), name), testInstance);
+      if (!valueAlreadyProvided) {
+        set(field, beanScope.get(type(), name), testInstance);
+      }
     }
 
     void setFromPlugin(Object value, Object testInstance) throws IllegalAccessException {
@@ -269,6 +315,10 @@ class MetaReader {
     void markForPluginInjection() {
       pluginInjection = true;
     }
+
+    void markAsProvided() {
+      valueAlreadyProvided = true;
+    }
   }
 
 }
diff --git a/inject-test/src/main/java/io/avaje/inject/test/PluginInitialise.java b/inject-test/src/main/java/io/avaje/inject/test/PluginInitialise.java
index d2c7f911..fa10eb3c 100644
--- a/inject-test/src/main/java/io/avaje/inject/test/PluginInitialise.java
+++ b/inject-test/src/main/java/io/avaje/inject/test/PluginInitialise.java
@@ -6,14 +6,14 @@ import java.util.ServiceLoader;
 
 final class PluginInitialise {
 
-  static final Plugin plugin = init();
+  private static final Plugin plugin = init();
 
-  static Plugin init() {
+  private static Plugin init() {
     return ServiceLoader.load(Plugin.class).findFirst().orElse(null);
   }
 
   @Nullable
   static Plugin plugin() {
-    return plugin == null ? null : plugin;
+    return plugin;
   }
 }
diff --git a/inject-test/src/main/java/io/avaje/inject/test/Setup.java b/inject-test/src/main/java/io/avaje/inject/test/Setup.java
new file mode 100644
index 00000000..84ec5056
--- /dev/null
+++ b/inject-test/src/main/java/io/avaje/inject/test/Setup.java
@@ -0,0 +1,18 @@
+package io.avaje.inject.test;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Specify a method in a test that takes a BeanScopeBuilder. The method is used to setup the test
+ * using the BeanScopeBuilder prior to the BeanScope being built.
+ * <p>
+ * The method is expected to only have a single parameter that is of type BeanScopeBuilder.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Setup {
+
+}
diff --git a/inject-test/src/main/java/io/avaje/inject/test/SetupMethods.java b/inject-test/src/main/java/io/avaje/inject/test/SetupMethods.java
new file mode 100644
index 00000000..1007d045
--- /dev/null
+++ b/inject-test/src/main/java/io/avaje/inject/test/SetupMethods.java
@@ -0,0 +1,68 @@
+package io.avaje.inject.test;
+
+import io.avaje.inject.BeanScopeBuilder;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+final class SetupMethods {
+
+  private final List<Method> staticMethods = new ArrayList<>();
+  private final List<Method> instanceMethods = new ArrayList<>();
+
+  SetupMethods(Class<?> testClass) {
+    var analyzedClass = testClass;
+    var hierarchy = new LinkedList<Class<?>>();
+    while (analyzedClass != null && !analyzedClass.equals(Object.class)) {
+      hierarchy.addFirst(analyzedClass);
+      analyzedClass = analyzedClass.getSuperclass();
+    }
+    for (Class<?> aClass : hierarchy) {
+      for (Method method : aClass.getDeclaredMethods()) {
+        if (method.getDeclaredAnnotation(Setup.class) != null) {
+          // should be our method
+          if (Modifier.isStatic(method.getModifiers())) {
+            method.setAccessible(true);
+            staticMethods.add(method);
+          } else {
+            method.setAccessible(true);
+            instanceMethods.add(method);
+          }
+        }
+      }
+    }
+  }
+
+  boolean hasStaticMethods() {
+    return !staticMethods.isEmpty();
+  }
+
+  boolean hasInstanceMethods() {
+    return !instanceMethods.isEmpty();
+  }
+
+  void invokeStatics(BeanScopeBuilder builder) {
+    try {
+      for (Method method : staticMethods) {
+        method.invoke(null, builder);
+      }
+    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+      throw new RuntimeException("Expecting method with single argument of BeanScopeBuilder but ... ", e);
+    }
+  }
+
+  void invokeInstance(BeanScopeBuilder builder, Object testInstance) {
+    try {
+      for (Method method : instanceMethods) {
+        method.invoke(testInstance, builder);
+      }
+    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+      throw new RuntimeException("Expecting method with single argument of BeanScopeBuilder but ... ", e);
+    }
+  }
+
+}
diff --git a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
index 1e454d3b..0ba4a5b6 100644
--- a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
+++ b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
@@ -12,6 +12,7 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Collectors;
 
+import static io.avaje.inject.spi.Module.EMPTY_CLASSES;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
@@ -20,7 +21,7 @@ class BeanScopeBuilderTest {
   @Test
   void depends_providedByParent() {
     DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(new TDBeanScope(MyFeature.class), Collections.emptySet(), false);
-    factoryOrder.add(bc("1", null, of(MyFeature.class)));
+    factoryOrder.add(bc("1", EMPTY_CLASSES, of(MyFeature.class)));
     factoryOrder.orderFactories();
 
     assertThat(names(factoryOrder.factories())).containsExactly("1");
@@ -29,7 +30,7 @@ class BeanScopeBuilderTest {
   @Test
   void depends_notProvidedByParent_expect_IllegalStateException() {
     DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(new TDBeanScope(FeatureA.class), Collections.emptySet(), false);
-    factoryOrder.add(bc("1", null, of(MyFeature.class)));
+    factoryOrder.add(bc("1", EMPTY_CLASSES, of(MyFeature.class)));
     assertThatThrownBy(factoryOrder::orderFactories)
       .isInstanceOf(IllegalStateException.class)
       .hasMessageContaining("Module [io.avaje.inject.BeanScopeBuilderTest$TDModule] has unsatisfied requires [io.avaje.inject.BeanScopeBuilderTest$MyFeature]");
@@ -38,19 +39,29 @@ class BeanScopeBuilderTest {
   @Test
   void noDepends() {
     DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(null, Collections.emptySet(), true);
-    factoryOrder.add(bc("1", null, null));
-    factoryOrder.add(bc("2", null, null));
-    factoryOrder.add(bc("3", null, null));
+    factoryOrder.add(bc("1", EMPTY_CLASSES, EMPTY_CLASSES));
+    factoryOrder.add(bc("2", EMPTY_CLASSES, EMPTY_CLASSES));
+    factoryOrder.add(bc("3", EMPTY_CLASSES, EMPTY_CLASSES));
     factoryOrder.orderFactories();
 
     assertThat(names(factoryOrder.factories())).containsExactly("1", "2", "3");
   }
 
+  @Test
+  void providedFirst() {
+    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(null, Collections.emptySet(), true);
+    factoryOrder.add(bc("two", EMPTY_CLASSES, EMPTY_CLASSES));
+    factoryOrder.add(bc("one", of(Mod3.class), EMPTY_CLASSES));
+    factoryOrder.orderFactories();
+
+    assertThat(names(factoryOrder.factories())).containsExactly("one", "two");
+  }
+
   @Test
   void name_depends() {
     DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(null, Collections.emptySet(), true);
-    factoryOrder.add(bc("two", null, of(Mod3.class)));
-    factoryOrder.add(bc("one", null, null));
+    factoryOrder.add(bc("two", EMPTY_CLASSES, of(Mod3.class)));
+    factoryOrder.add(bc("one", EMPTY_CLASSES, EMPTY_CLASSES));
     factoryOrder.orderFactories();
 
     assertThat(names(factoryOrder.factories())).containsExactly("one", "two");
@@ -59,8 +70,8 @@ class BeanScopeBuilderTest {
   @Test
   void name_depends4() {
     DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(null, Collections.emptySet(), true);
-    factoryOrder.add(bc("1", null, of(Mod3.class)));
-    factoryOrder.add(bc("2", null, of(Mod4.class)));
+    factoryOrder.add(bc("1", EMPTY_CLASSES, of(Mod3.class)));
+    factoryOrder.add(bc("2", EMPTY_CLASSES, of(Mod4.class)));
     factoryOrder.add(bc("3", of(Mod3.class), of(Mod4.class)));
     factoryOrder.add(bc("4", of(Mod4.class), null));
 
@@ -73,7 +84,7 @@ class BeanScopeBuilderTest {
   void nameFeature_depends() {
     DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(null, Collections.emptySet(), true);
     factoryOrder.add(bc("1", of(FeatureA.class), of(Mod3.class)));
-    factoryOrder.add(bc("2", null, of(Mod4.class, FeatureA.class)));
+    factoryOrder.add(bc("2", EMPTY_CLASSES, of(Mod4.class, FeatureA.class)));
     factoryOrder.add(bc("3", of(Mod3.class), of(Mod4.class)));
     factoryOrder.add(bc("4", of(Mod4.class), null));
 
@@ -85,7 +96,7 @@ class BeanScopeBuilderTest {
   @Test
   void feature_depends() {
     DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(null, Collections.emptySet(), true);
-    factoryOrder.add(bc("two", null, of(MyFeature.class)));
+    factoryOrder.add(bc("two", EMPTY_CLASSES, of(MyFeature.class)));
     factoryOrder.add(bc("one", of(MyFeature.class), null));
     factoryOrder.orderFactories();
 
@@ -95,9 +106,9 @@ class BeanScopeBuilderTest {
   @Test
   void feature_depends2() {
     DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(null, Collections.emptySet(), true);
-    factoryOrder.add(bc("two", null, of(MyFeature.class)));
-    factoryOrder.add(bc("one", of(MyFeature.class), null));
-    factoryOrder.add(bc("three", of(MyFeature.class), null));
+    factoryOrder.add(bc("two", EMPTY_CLASSES, of(MyFeature.class)));
+    factoryOrder.add(bc("one", of(MyFeature.class), EMPTY_CLASSES));
+    factoryOrder.add(bc("three", of(MyFeature.class), EMPTY_CLASSES));
     factoryOrder.orderFactories();
 
     assertThat(names(factoryOrder.factories())).containsExactly("one", "three", "two");
@@ -106,8 +117,8 @@ class BeanScopeBuilderTest {
   @Test
   void name_requiresPackage() {
     DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(null, Collections.emptySet(), true);
-    factoryOrder.add(bc("1", null, new Class[0], of(Mod3.class)));
-    factoryOrder.add(bc("2", null, new Class[0], of(Mod4.class)));
+    factoryOrder.add(bc("1", EMPTY_CLASSES, new Class[0], of(Mod3.class)));
+    factoryOrder.add(bc("2", EMPTY_CLASSES, new Class[0], of(Mod4.class)));
     factoryOrder.add(bc("3", of(Mod3.class), new Class[0], of(Mod4.class)));
     factoryOrder.add(bc("4", of(Mod4.class), new Class[0]));
 
@@ -119,8 +130,8 @@ class BeanScopeBuilderTest {
   @Test
   void name_requiresPackage_mixed() {
     DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(null, Collections.emptySet(), true);
-    factoryOrder.add(bc("1", null, new Class[0], of(Mod3.class)));
-    factoryOrder.add(bc("2", null, of(Mod4.class), new Class[0]));
+    factoryOrder.add(bc("1", EMPTY_CLASSES, new Class[0], of(Mod3.class)));
+    factoryOrder.add(bc("2", EMPTY_CLASSES, of(Mod4.class), new Class[0]));
     factoryOrder.add(bc("3", of(Mod3.class), new Class[0], of(Mod4.class)));
     factoryOrder.add(bc("4", of(Mod4.class), new Class[0]));
 
@@ -132,8 +143,8 @@ class BeanScopeBuilderTest {
   @Test
   void missingRequiresPackage_expect_unsatisfiedRequiresPackages() {
     DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(null, Collections.emptySet(), false);
-    factoryOrder.add(bc("1", null, new Class[0], of(Mod3.class)));
-    factoryOrder.add(bc("2", null, of(Mod4.class), new Class[0]));
+    factoryOrder.add(bc("1", EMPTY_CLASSES, new Class[0], of(Mod3.class)));
+    factoryOrder.add(bc("2", EMPTY_CLASSES, of(Mod4.class), new Class[0]));
     factoryOrder.add(bc("4", of(Mod4.class), new Class[0]));
 
     assertThatThrownBy(factoryOrder::orderFactories)
@@ -144,8 +155,8 @@ class BeanScopeBuilderTest {
   @Test
   void missingRequires_expect_unsatisfiedRequires() {
     DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(null, Collections.emptySet(), false);
-    factoryOrder.add(bc("1", null, of(Mod3.class), new Class[0]));
-    factoryOrder.add(bc("2", null, of(Mod4.class), new Class[0]));
+    factoryOrder.add(bc("1", EMPTY_CLASSES, of(Mod3.class), new Class[0]));
+    factoryOrder.add(bc("2", EMPTY_CLASSES, of(Mod4.class), new Class[0]));
     factoryOrder.add(bc("4", of(Mod4.class), new Class[0]));
 
     assertThatThrownBy(factoryOrder::orderFactories)