Repository: avaje/avaje-inject
Pull request #134 (2021-07-19 05:49:15)

Refactor scopes - Removes ApplicationScope & RequestScope in favour of "custom scopes"
- Removes ApplicationScope  - Migrate to BeanScope.newBuilder().build()
- Removes RequestScope - Migrate to using custom scopes
- Removes deprecated methods
- Moves `withSpy()` `withMock()` methods of `BeanScopeBuilder` to only be available after `beanScopeBuilder.forTesting()` 
- Changes `@InjectModule` to use `Class<?>` for provides and requires (not strings)
- Changes BeanScopeFactory to instead be `Module` and allow us to specify explicitly which modules to include in BeanScope
- Adds support for "custom scopes", each scope generates it's own `Module`
- Allows BeanScope to have a parent BeanScope



Patch:
diff --git a/README.md b/README.md
index 2dc34e4c..d730401f 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,7 @@ module org.example {
 
   requires io.avaje.inject;
 
-  // register org.example._DI$BeanScopeFactory from generated sources
-  provides io.avaje.inject.spi.BeanScopeFactory with org.example._DI$BeanScopeFactory;
+  provides io.avaje.inject.spi.Module with org.example.ExampleModule;
 }
 ```
 
diff --git a/inject-generator/pom.xml b/inject-generator/pom.xml
index c01c8c93..509f6c30 100644
--- a/inject-generator/pom.xml
+++ b/inject-generator/pom.xml
@@ -4,7 +4,7 @@
   <parent>
     <groupId>io.avaje</groupId>
     <artifactId>avaje-inject-parent</artifactId>
-    <version>6.2</version>
+    <version>6.5-RC0</version>
   </parent>
 
   <artifactId>avaje-inject-generator</artifactId>
@@ -16,7 +16,7 @@
     <dependency>
       <groupId>io.avaje</groupId>
       <artifactId>avaje-inject</artifactId>
-      <version>6.2</version>
+      <version>6.5-RC0</version>
     </dependency>
 
     <!-- test dependencies -->
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
new file mode 100644
index 00000000..e9ce5634
--- /dev/null
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/AllScopes.java
@@ -0,0 +1,124 @@
+package io.avaje.inject.generator;
+
+import io.avaje.inject.InjectModule;
+
+import javax.annotation.processing.RoundEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import javax.tools.FileObject;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class AllScopes {
+
+  private final Map<String, Data> scopeAnnotations = new HashMap<>();
+  private final ProcessingContext context;
+  private final ScopeInfo defaultScope;
+
+  AllScopes(ProcessingContext context) {
+    this.context = context;
+    this.defaultScope = new ScopeInfo(context);
+  }
+
+  ScopeInfo defaultScope() {
+    return defaultScope;
+  }
+
+  ScopeInfo addScopeAnnotation(TypeElement type) {
+    final Data data = new Data(type, context, this);
+    scopeAnnotations.put(type.getQualifiedName().toString(), data);
+    return data.scopeInfo;
+  }
+
+  boolean providedByDefaultModule(String dependency) {
+    return defaultScope.providesDependency(dependency);
+  }
+
+  void readBeans(RoundEnvironment roundEnv) {
+    for (Data data : scopeAnnotations.values()) {
+      for (Element customBean : roundEnv.getElementsAnnotatedWith(data.type)) {
+        // context.logWarn("read custom scope bean " + customBean + " for scope " + entry.getKey());
+        data.scopeInfo.read((TypeElement) customBean, false);
+      }
+    }
+  }
+
+  void write(boolean processingOver) {
+    for (Data value : scopeAnnotations.values()) {
+      value.write(processingOver);
+    }
+    if (processingOver) {
+      writeModuleCustomServicesFile();
+    }
+  }
+
+  private void writeModuleCustomServicesFile() {
+    if (scopeAnnotations.isEmpty()) {
+      return;
+    }
+    try {
+      FileObject jfo = context.createMetaInfModuleCustom();
+      if (jfo != null) {
+        Writer writer = jfo.openWriter();
+        for (Data value : scopeAnnotations.values()) {
+          writer.write(value.moduleFullName());
+          writer.write("\n");
+        }
+        writer.close();
+      }
+
+    } catch (IOException e) {
+      e.printStackTrace();
+      context.logError("Failed to write services file " + e.getMessage());
+    }
+  }
+
+  void readModules(List<String> customScopeModules) {
+    for (String customScopeModule : customScopeModules) {
+      final TypeElement module = context.element(customScopeModule);
+      if (module != null) {
+        final InjectModule injectModule = module.getAnnotation(InjectModule.class);
+        if (injectModule != null) {
+          final String customScopeType = injectModule.customScopeType();
+          final TypeElement scopeType = context.element(customScopeType);
+          if (scopeType == null) {
+            context.logError(module, "customScopeType [" + customScopeType + "] is invalid? on " + module);
+          } else {
+            final ScopeInfo scopeInfo = addScopeAnnotation(scopeType);
+            scopeInfo.readModuleMetaData(module);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Find the scope by scope annotation type.
+   */
+  ScopeInfo get(String fullType) {
+    final Data data = scopeAnnotations.get(fullType);
+    return data == null ? null : data.scopeInfo;
+  }
+
+  static class Data {
+    final TypeElement type;
+    final ScopeInfo scopeInfo;
+
+    Data(TypeElement type, ProcessingContext context, AllScopes allScopes) {
+      this.type = type;
+      this.scopeInfo = new ScopeInfo(context, type, allScopes);
+      this.scopeInfo.details(null, type);
+    }
+
+    void write(boolean processingOver) {
+      scopeInfo.write(processingOver);
+    }
+
+    String moduleFullName() {
+      return scopeInfo.moduleFullName();
+    }
+  }
+}
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 a63fb772..b3c82d17 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
@@ -148,9 +148,6 @@ class BeanReader {
   }
 
   void buildAddFor(Append writer) {
-    if (requestParams.isRequestParam()) {
-      context.logError(beanType, "@Singleton %s is not allowed to have a @Request scope dependency %s", shortName, requestParams.getRequestParamType());
-    }
     writer.append("    if (builder.isAddBeanFor(");
     if (name != null && !name.isEmpty()) {
       writer.append("\"%s\", ", name);
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/BeanRequestParams.java b/inject-generator/src/main/java/io/avaje/inject/generator/BeanRequestParams.java
index a15b5614..3c878ec1 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/BeanRequestParams.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/BeanRequestParams.java
@@ -1,8 +1,6 @@
 package io.avaje.inject.generator;
 
-import io.avaje.inject.Request;
 
-import javax.lang.model.element.TypeElement;
 import java.util.Set;
 
 /**
@@ -15,7 +13,6 @@ class BeanRequestParams {
   private final boolean requestScopedBean;
 
   private RequestScope.Handler reqScopeHandler;
-  private String requestParamType;
 
   BeanRequestParams(ProcessingContext context, String parentType, boolean requestScopedBean) {
     this.context = context;
@@ -23,14 +20,6 @@ class BeanRequestParams {
     this.requestScopedBean = requestScopedBean;
   }
 
-  boolean isRequestParam() {
-    return requestParamType != null;
-  }
-
-  String getRequestParamType() {
-    return requestParamType;
-  }
-
   /**
    * Return true if this type is a request scoped type (e.g. Javalin Context).
    */
@@ -45,14 +34,6 @@ class BeanRequestParams {
       }
       return true;
     }
-    if (paramType != null && requestParamType == null) {
-      final TypeElement element = context.element(paramType);
-      if (element != null) {
-        if (element.getAnnotation(Request.class) != null) {
-          requestParamType = paramType;
-        }
-      }
-    }
     return false;
   }
 
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Constants.java b/inject-generator/src/main/java/io/avaje/inject/generator/Constants.java
index ec898ce9..492cfcc8 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Constants.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Constants.java
@@ -20,7 +20,8 @@ class Constants {
 
   static final String AT_SINGLETON = "@Singleton";
   static final String AT_GENERATED = "@Generated(\"io.avaje.inject.generator\")";
-  static final String META_INF_FACTORY = "META-INF/services/io.avaje.inject.spi.BeanScopeFactory";
+  static final String META_INF_FACTORY = "META-INF/services/io.avaje.inject.spi.Module";
+  static final String META_INF_CUSTOM = "META-INF/services/io.avaje.inject.spi.Module.Custom";
 
   static final String REQUESTSCOPE = "io.avaje.inject.RequestScope";
   static final String BEANCONTEXT = "io.avaje.inject.BeanScope";
@@ -31,5 +32,5 @@ class Constants {
   static final String BEAN_FACTORY2 = "io.avaje.inject.spi.BeanFactory2";
   static final String BUILDER = "io.avaje.inject.spi.Builder";
   static final String DEPENDENCYMETA = "io.avaje.inject.spi.DependencyMeta";
-  static final String BEANSCOPEFACTORY = "io.avaje.inject.spi.BeanScopeFactory";
+  static final String MODULE = "io.avaje.inject.spi.Module";
 }
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 c331cda2..303b0b6d 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
@@ -17,7 +17,7 @@ class MetaData {
   private final String type;
   private final String shortType;
   private final String name;
-
+  private final List<String> externallyProvided = new ArrayList<>();
   private String method;
   private boolean wired;
   private boolean requestScope;
@@ -229,4 +229,7 @@ class MetaData {
     this.method = method;
   }
 
+  void externallyProvided(String dependency) {
+    externallyProvided.add(dependency);
+  }
 }
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 162408fe..6fbd6cc3 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
@@ -11,21 +11,17 @@ class MetaDataOrdering {
       "\n See https://avaje.io/inject/#circular";
 
   private final ProcessingContext context;
-
+  private final ScopeInfo scopeInfo;
   private final List<MetaData> orderedList = new ArrayList<>();
   private final List<MetaData> requestScope = new ArrayList<>();
   private final List<MetaData> queue = new ArrayList<>();
-
   private final Map<String, ProviderList> providers = new HashMap<>();
-
   private final List<DependencyLink> circularDependencies = new ArrayList<>();
-
   private final Set<String> missingDependencyTypes = new LinkedHashSet<>();
 
-  private String topPackage;
-
-  MetaDataOrdering(Collection<MetaData> values, ProcessingContext context) {
+  MetaDataOrdering(Collection<MetaData> values, ProcessingContext context, ScopeInfo scopeInfo) {
     this.context = context;
+    this.scopeInfo = scopeInfo;
     for (MetaData metaData : values) {
       if (metaData.isRequestScope()) {
         // request scoped expected to have externally provided dependencies
@@ -37,21 +33,20 @@ class MetaDataOrdering {
       } else {
         queue.add(metaData);
       }
-      topPackage = Util.commonParent(topPackage, metaData.getTopPackage());
       // 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);
       }
     }
-    externallyRequiredDependencies(context);
+    externallyRequiredDependencies();
   }
 
   /**
    * These if defined are expected to be required at wiring time probably via another module.
    */
-  private void externallyRequiredDependencies(ProcessingContext context) {
-    for (String requireType : context.contextRequires()) {
+  private void externallyRequiredDependencies() {
+    for (String requireType : scopeInfo.requires()) {
       providers.computeIfAbsent(requireType, s -> new ProviderList());
     }
   }
@@ -120,14 +115,8 @@ class MetaDataOrdering {
    * Build list of specific dependencies that are missing.
    */
   void missingDependencies() {
-    for (MetaData m : queue) {
-      for (String dependency : m.getDependsOn()) {
-        if (providers.get(dependency) == null) {
-          TypeElement element = context.elementMaybe(m.getType());
-          context.logError(element, "No dependency provided for " + dependency);
-          missingDependencyTypes.add(dependency);
-        }
-      }
+    for (MetaData metaData : queue) {
+      checkMissingDependencies(metaData);
     }
     if (missingDependencyTypes.isEmpty()) {
       // only look for circular dependencies if there are no missing dependencies
@@ -135,17 +124,29 @@ class MetaDataOrdering {
     }
   }
 
+  private void checkMissingDependencies(MetaData metaData) {
+    for (String dependency : metaData.getDependsOn()) {
+      if (providers.get(dependency) == null && !scopeInfo.providedByOtherModule(dependency)) {
+        TypeElement element = context.elementMaybe(metaData.getType());
+        context.logError(element, "No dependency provided for " + dependency + " on " + metaData.getType());
+        missingDependencyTypes.add(dependency);
+      }
+    }
+  }
+
   /**
    * Log a warning on unsatisfied dependencies that are expected to be provided by another module.
    */
   private void warnOnDependencies() {
-    if (missingDependencyTypes.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());
-      }
-    } else {
+    if (!missingDependencyTypes.isEmpty()) {
       context.logError("Dependencies %s are not provided - missing @Singleton or @Factory/@Bean or specify external dependency via @InjectModule requires attribute", missingDependencyTypes);
+    } else {
+      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());
+        }
+      }
     }
   }
 
@@ -157,10 +158,6 @@ class MetaDataOrdering {
     }
   }
 
-  String getTopPackage() {
-    return topPackage;
-  }
-
   private int processQueueRound() {
     // loop queue looking for entry that has all provides marked as included
     int count = 0;
@@ -183,8 +180,7 @@ class MetaDataOrdering {
         // check non-provider dependency is satisfied
         ProviderList providerList = providers.get(dependency);
         if (providerList == null) {
-          // dependency not yet satisfied
-          return false;
+          return scopeInfo.providedByOtherModule(dependency);
         } else {
           if (!providerList.isAllWired()) {
             return false;
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
new file mode 100644
index 00000000..844154a9
--- /dev/null
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MetaTopPackage.java
@@ -0,0 +1,22 @@
+package io.avaje.inject.generator;
+
+import java.util.Collection;
+
+class MetaTopPackage {
+
+  private String topPackage;
+
+  static String of(Collection<MetaData> values) {
+    return new MetaTopPackage(values).value();
+  }
+
+  private String value() {
+    return topPackage;
+  }
+
+  private MetaTopPackage(Collection<MetaData> values) {
+    for (MetaData metaData : values) {
+      topPackage = Util.commonParent(topPackage, metaData.getTopPackage());
+    }
+  }
+}
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 d9ed79d2..93c81061 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
@@ -19,9 +19,7 @@ import java.io.IOException;
 import java.io.LineNumberReader;
 import java.io.Reader;
 import java.nio.file.NoSuchFileException;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 
 class ProcessingContext {
 
@@ -31,13 +29,6 @@ class ProcessingContext {
   private final Elements elementUtils;
   private final Types typeUtils;
 
-  private String contextName;
-  private String[] contextProvides;
-  private String[] contextDependsOn;
-  private Set<String> contextRequires = new LinkedHashSet<>();
-  private String contextPackage;
-  private String metaInfServicesLine;
-
   ProcessingContext(ProcessingEnvironment processingEnv) {
     this.processingEnv = processingEnv;
     this.messager = processingEnv.getMessager();
@@ -66,23 +57,29 @@ class ProcessingContext {
   }
 
   String loadMetaInfServices() {
-    if (metaInfServicesLine == null) {
-      metaInfServicesLine = loadMetaInf();
-    }
-    return metaInfServicesLine;
+    final List<String> lines = loadMetaInf(Constants.META_INF_FACTORY);
+    return lines.isEmpty() ? null : lines.get(0);
   }
 
-  private String loadMetaInf() {
-    // logDebug("loading metaInfServicesLine ...");
+  List<String> loadMetaInfCustom() {
+    return loadMetaInf(Constants.META_INF_CUSTOM);
+  }
+
+  private List<String> loadMetaInf(String fullName) {
     try {
-      FileObject fileObject = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", Constants.META_INF_FACTORY);
+      FileObject fileObject = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", fullName);
       if (fileObject != null) {
+        List<String> lines = new ArrayList<>();
         Reader reader = fileObject.openReader(true);
         LineNumberReader lineReader = new LineNumberReader(reader);
-        String line = lineReader.readLine();
-        if (line != null) {
-          return line.trim();
+        String line;
+        while ((line = lineReader.readLine()) != null) {
+          line = line.trim();
+          if (!line.isEmpty()) {
+            lines.add(line);
+          }
         }
+        return lines;
       }
 
     } catch (FileNotFoundException | NoSuchFileException e) {
@@ -95,7 +92,7 @@ class ProcessingContext {
       e.printStackTrace();
       logWarn("Error reading services file: " + e.getMessage());
     }
-    return null;
+    return Collections.emptyList();
   }
 
   /**
@@ -105,44 +102,16 @@ class ProcessingContext {
     return filer.createSourceFile(cls);
   }
 
-  /**
-   * Create a file writer for the given class name.
-   */
   FileObject createMetaInfWriter() throws IOException {
-    return filer.createResource(StandardLocation.CLASS_OUTPUT, "", Constants.META_INF_FACTORY);
-  }
-
-  void setContextDetails(String name, String[] provides, String[] dependsOn, Element contextElement) {
-    this.contextName = name;
-    this.contextProvides = provides;
-    this.contextDependsOn = dependsOn;
-
-    // determine the context package (that we put the DI Factory class into)
-    PackageElement pkg = elementUtils.getPackageOf(contextElement);
-    logDebug("using package from element " + pkg);
-    this.contextPackage = (pkg == null) ? null : pkg.getQualifiedName().toString();
-  }
-
-  void setContextRequires(List<String> contextRequires) {
-    this.contextRequires.addAll(contextRequires);
-  }
-
-  Set<String> contextRequires() {
-    return contextRequires;
-  }
-
-  void deriveContextName(String factoryPackage) {
-    if (contextName == null) {
-      contextName = factoryPackage;
-    }
+    return createMetaInfWriterFor(Constants.META_INF_FACTORY);
   }
 
-  String contextName() {
-    return contextName;
+  FileObject createMetaInfModuleCustom() throws IOException {
+    return createMetaInfWriterFor(Constants.META_INF_CUSTOM);
   }
 
-  String getContextPackage() {
-    return contextPackage;
+  private FileObject createMetaInfWriterFor(String interfaceType) throws IOException {
+    return filer.createResource(StandardLocation.CLASS_OUTPUT, "", interfaceType);
   }
 
   TypeElement element(String rawType) {
@@ -161,61 +130,7 @@ class ProcessingContext {
     return typeUtils.asElement(returnType);
   }
 
-  void buildNewBuilder(Append writer) {
-    writer.append("    this.name = \"%s\";", contextName).eol();
-    writer.append("    this.provides = ", contextProvides);
-    buildStringArray(writer, contextProvides, true);
-    writer.append(";").eol();
-    writer.append("    this.dependsOn = ", contextDependsOn);
-    buildStringArray(writer, contextDependsOn, true);
-    writer.append(";").eol();
-  }
-
-  void buildAtInjectModule(Append writer) {
-    writer.append(Constants.AT_GENERATED).eol();
-    writer.append("@InjectModule(name=\"%s\"", contextName);
-    if (!isEmpty(contextProvides)) {
-      writer.append(", provides=");
-      buildStringArray(writer, contextProvides, false);
-    }
-    if (!isEmpty(contextDependsOn)) {
-      writer.append(", dependsOn=");
-      buildStringArray(writer, contextDependsOn, false);
-    }
-    if (!contextRequires.isEmpty()) {
-      writer.append(", requires={");
-      int c = 0;
-      for (String value : contextRequires) {
-        if (c++ > 0) {
-          writer.append(",");
-        }
-        writer.append(value).append(".class");
-      }
-      writer.append("}");
-    }
-    writer.append(")").eol();
-  }
-
-  private boolean isEmpty(String[] strings) {
-    return strings == null || strings.length == 0;
-  }
-
-  private void buildStringArray(Append writer, String[] values, boolean asArray) {
-    if (isEmpty(values)) {
-      writer.append("null");
-    } else {
-      if (asArray) {
-        writer.append("new String[]");
-      }
-      writer.append("{");
-      int c = 0;
-      for (String value : values) {
-        if (c++ > 0) {
-          writer.append(",");
-        }
-        writer.append("\"").append(value).append("\"");
-      }
-      writer.append("}");
-    }
+  PackageElement getPackageOf(Element element) {
+    return elementUtils.getPackageOf(element);
   }
 }
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 f23d410d..cd9cee4f 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
@@ -1,45 +1,25 @@
 package io.avaje.inject.generator;
 
-import io.avaje.inject.InjectModule;
 import io.avaje.inject.Factory;
-import io.avaje.inject.Request;
-import io.avaje.inject.spi.DependencyMeta;
+import io.avaje.inject.InjectModule;
+import jakarta.inject.Scope;
+import jakarta.inject.Singleton;
 
 import javax.annotation.processing.AbstractProcessor;
-import javax.annotation.processing.FilerException;
 import javax.annotation.processing.ProcessingEnvironment;
 import javax.annotation.processing.RoundEnvironment;
-import jakarta.inject.Singleton;
 import javax.lang.model.SourceVersion;
 import javax.lang.model.element.*;
 import javax.lang.model.util.Elements;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 public class Processor extends AbstractProcessor {
 
-  private static final String INJECT_MODULE = "io.avaje.inject.InjectModule";
-
   private ProcessingContext context;
-
   private Elements elementUtils;
-
-  /**
-   * Map to merge the existing meta data with partially compiled code. Keyed by type and qualifier/name.
-   */
-  private final Map<String, MetaData> metaData = new LinkedHashMap<>();
-
-  private final List<BeanReader> beanReaders = new ArrayList<>();
-
-  private final Set<String> readBeans = new HashSet<>();
+  private ScopeInfo defaultScope;
+  private AllScopes allScopes;
+  private boolean readModuleInfo;
 
   public Processor() {
   }
@@ -54,15 +34,17 @@ public class Processor extends AbstractProcessor {
     super.init(processingEnv);
     this.context = new ProcessingContext(processingEnv);
     this.elementUtils = processingEnv.getElementUtils();
+    this.allScopes = new AllScopes(context);
+    this.defaultScope = allScopes.defaultScope();
   }
 
   @Override
   public Set<String> getSupportedAnnotationTypes() {
-
     Set<String> annotations = new LinkedHashSet<>();
     annotations.add(InjectModule.class.getCanonicalName());
     annotations.add(Factory.class.getCanonicalName());
     annotations.add(Singleton.class.getCanonicalName());
+    annotations.add(Scope.class.getCanonicalName());
     annotations.add(Constants.CONTROLLER);
     return annotations;
   }
@@ -78,58 +60,29 @@ public class Processor extends AbstractProcessor {
 
     Set<? extends Element> factoryBeans = roundEnv.getElementsAnnotatedWith(Factory.class);
     Set<? extends Element> beans = roundEnv.getElementsAnnotatedWith(Singleton.class);
-    Set<? extends Element> requestBeans = roundEnv.getElementsAnnotatedWith(Request.class);
-
+    Set<? extends Element> scopes = roundEnv.getElementsAnnotatedWith(Scope.class);
+    readScopes(scopes);
     readModule(roundEnv);
     readChangedBeans(factoryBeans, true);
     readChangedBeans(beans, false);
     readChangedBeans(controllers, false);
-    readChangedBeans(requestBeans, false);
+    allScopes.readBeans(roundEnv);
 
-    mergeMetaData();
-
-    writeBeanHelpers();
-    if (roundEnv.processingOver()) {
-      writeBeanFactory();
-    }
+    defaultScope.write(roundEnv.processingOver());
+    allScopes.write(roundEnv.processingOver());
     return false;
   }
 
-  private void writeBeanHelpers() {
-    for (BeanReader beanReader : beanReaders) {
-      try {
-        if (!beanReader.isWrittenToFile()) {
-          SimpleBeanWriter writer = new SimpleBeanWriter(beanReader, context);
-          writer.write();
-          beanReader.setWrittenToFile();
-        }
-      } catch (FilerException e) {
-        context.logWarn("FilerException to write $DI class " + beanReader.getBeanType() + " " + e.getMessage());
-
-      } catch (IOException e) {
-        e.printStackTrace();
-        context.logError(beanReader.getBeanType(), "Failed to write $DI class");
+  private void readScopes(Set<? extends Element> scopes) {
+    for (Element element : scopes) {
+      if (element.getKind() == ElementKind.ANNOTATION_TYPE) {
+        // context.logDebug("detected scope annotation " + element);
+        TypeElement type = (TypeElement) element;
+        allScopes.addScopeAnnotation(type);
       }
     }
   }
 
-  private void writeBeanFactory() {
-    MetaDataOrdering ordering = new MetaDataOrdering(metaData.values(), context);
-    int remaining = ordering.processQueue();
-    if (remaining > 0) {
-      ordering.logWarnings();
-    }
-
-    try {
-      SimpleFactoryWriter factoryWriter = new SimpleFactoryWriter(ordering, context);
-      factoryWriter.write();
-    } catch (FilerException e) {
-      context.logWarn("FilerException trying to write factory " + e.getMessage());
-    } catch (IOException e) {
-      context.logError("Failed to write factory " + e.getMessage());
-    }
-  }
-
   /**
    * Read the beans that have changed.
    */
@@ -138,138 +91,75 @@ public class Processor extends AbstractProcessor {
       if (!(element instanceof TypeElement)) {
         context.logError("unexpected type [" + element + "]");
       } else {
-        if (readBeans.add(element.toString())) {
-          readBeanMeta((TypeElement) element, factory);
+        TypeElement typeElement = (TypeElement) element;
+        if (!factory) {
+          defaultScope.read(typeElement, factory);
         } else {
-          context.logDebug("skipping already processed bean " + element);
+          final ScopeInfo scope = findScope(typeElement);
+          if (scope != null) {
+            // context.logWarn("Adding factory to custom scope "+element+" scope: "+scope);
+            scope.read(typeElement, true);
+          } else {
+            defaultScope.read(typeElement, true);
+          }
         }
       }
     }
   }
 
   /**
-   * Merge the changed bean meta data into the existing (factory) metaData.
+   * Find the scope if the Factory has a scope annotation.
    */
-  private void mergeMetaData() {
-    for (BeanReader beanReader : beanReaders) {
-      if (!beanReader.isRequestScopedController()) {
-        MetaData metaData = this.metaData.get(beanReader.getMetaKey());
-        if (metaData == null) {
-          addMeta(beanReader);
-        } else {
-          updateMeta(metaData, beanReader);
-        }
+  private ScopeInfo findScope(Element element) {
+    for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
+      final ScopeInfo scopeInfo = allScopes.get(annotationMirror.getAnnotationType().toString());
+      if (scopeInfo != null) {
+        return scopeInfo;
       }
     }
-  }
-
-  /**
-   * Add a new previously unknown bean.
-   */
-  private void addMeta(BeanReader beanReader) {
-    MetaData meta = beanReader.createMeta();
-    metaData.put(meta.getKey(), meta);
-    for (MetaData methodMeta : beanReader.createFactoryMethodMeta()) {
-      metaData.put(methodMeta.getKey(), methodMeta);
-    }
-  }
-
-  /**
-   * Update the meta data on a previously known bean.
-   */
-  private void updateMeta(MetaData metaData, BeanReader beanReader) {
-    metaData.update(beanReader);
-  }
-
-  /**
-   * Read the dependency injection meta data for the given bean.
-   */
-  private void readBeanMeta(TypeElement typeElement, boolean factory) {
-    if (typeElement.getKind() == ElementKind.ANNOTATION_TYPE) {
-      context.logDebug("skipping annotation type " + typeElement);
-      return;
-    }
-    beanReaders.add(new BeanReader(typeElement, context, factory).read());
+    return null;
   }
 
   /**
    * Read the existing meta data from InjectModule (if found) and the factory bean (if exists).
    */
   private void readModule(RoundEnvironment roundEnv) {
+    if (readModuleInfo) {
+      // only read the module meta data once
+      return;
+    }
+    readModuleInfo = true;
     String factory = context.loadMetaInfServices();
     if (factory != null) {
-      TypeElement factoryType = elementUtils.getTypeElement(factory);
-      if (factoryType != null) {
-        readFactory(factoryType);
+      TypeElement moduleType = elementUtils.getTypeElement(factory);
+      if (moduleType != null) {
+        defaultScope.readModuleMetaData(moduleType);
       }
     }
+    allScopes.readModules(context.loadMetaInfCustom());
+    readInjectModule(roundEnv);
+  }
 
+  /**
+   * Read InjectModule for things like package-info etc (not for custom scopes)
+   */
+  private void readInjectModule(RoundEnvironment roundEnv) {
+    // read other that are annotated with InjectModule
     Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(InjectModule.class);
     if (!elementsAnnotatedWith.isEmpty()) {
       Iterator<? extends Element> iterator = elementsAnnotatedWith.iterator();
       if (iterator.hasNext()) {
         Element element = iterator.next();
-        InjectModule annotation = element.getAnnotation(InjectModule.class);
-        if (annotation != null) {
-          context.setContextDetails(annotation.name(), annotation.provides(), annotation.dependsOn(), element);
-          context.setContextRequires(readRequires(element));
-        }
-      }
-    }
-  }
-
-  /**
-   * Read the list of required class names.
-   */
-  private List<String> readRequires(Element element) {
-    List<String> requiresList = new ArrayList<>();
-    for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
-      if (INJECT_MODULE.equals(annotationMirror.getAnnotationType().toString())) {
-        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : annotationMirror.getElementValues().entrySet()) {
-          if (entry.getKey().toString().startsWith("requires")) {
-            for (Object requiresType : (List<?>) entry.getValue().getValue()) {
-              String fullName = requiresType.toString();
-              fullName = fullName.substring(0, fullName.length() - 6);
-              requiresList.add(fullName);
-            }
+        Scope scope = element.getAnnotation(Scope.class);
+        if (scope == null) {
+          // it it not a custom scope annotation
+          InjectModule annotation = element.getAnnotation(InjectModule.class);
+          if (annotation != null) {
+            defaultScope.details(annotation.name(), element);
           }
         }
       }
     }
-    return requiresList;
-  }
-
-
-  /**
-   * Read the existing factory bean. Each of the build methods is annotated with <code>@DependencyMeta</code>
-   * which holds the information we need (to regenerate the factory with any changes).
-   */
-  private void readFactory(TypeElement factoryType) {
-    InjectModule module = factoryType.getAnnotation(InjectModule.class);
-    context.setContextDetails(module.name(), module.provides(), module.dependsOn(), factoryType);
-    context.setContextRequires(readRequires(factoryType));
-
-    List<? extends Element> elements = factoryType.getEnclosedElements();
-    if (elements != null) {
-      for (Element element : elements) {
-        if (ElementKind.METHOD == element.getKind()) {
-          readBuildMethodDependencyMeta(element);
-        }
-      }
-    }
   }
 
-  private void readBuildMethodDependencyMeta(Element element) {
-    Name simpleName = element.getSimpleName();
-    if (simpleName.toString().startsWith("build_")) {
-      // read a build method - DependencyMeta
-      DependencyMeta meta = element.getAnnotation(DependencyMeta.class);
-      if (meta == null) {
-        context.logError("Missing @DependencyMeta on method " + simpleName);
-      } else {
-        final MetaData metaData = new MetaData(meta);
-        this.metaData.put(metaData.getKey(), metaData);
-      }
-    }
-  }
 }
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
new file mode 100644
index 00000000..d0fb279c
--- /dev/null
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java
@@ -0,0 +1,384 @@
+package io.avaje.inject.generator;
+
+import io.avaje.inject.InjectModule;
+import io.avaje.inject.spi.DependencyMeta;
+
+import javax.annotation.processing.FilerException;
+import javax.lang.model.element.*;
+import javax.tools.JavaFileObject;
+import java.io.IOException;
+import java.util.*;
+
+class ScopeInfo {
+
+  /**
+   * Map to merge the existing meta data with partially compiled code. Keyed by type and qualifier/name.
+   */
+  private final Map<String, MetaData> metaData = new LinkedHashMap<>();
+  private final List<BeanReader> beanReaders = new ArrayList<>();
+  private final Set<String> readBeans = new HashSet<>();
+  private final ProcessingContext context;
+  private final Set<String> requires = new LinkedHashSet<>();
+  private final Set<String> provides = new LinkedHashSet<>();
+  private final boolean defaultScope;
+  private final TypeElement annotationType;
+  private final AllScopes scopes;
+  private boolean moduleInitialised;
+  private boolean moduleWritten;
+  private String name;
+  private String modulePackage;
+  private String moduleFullName;
+  private String moduleShortName;
+  private JavaFileObject moduleFile;
+  private boolean emptyModule;
+
+  /**
+   * Create for the main/global module scope.
+   */
+  ScopeInfo(ProcessingContext context) {
+    this.scopes = null;
+    this.context = context;
+    this.defaultScope = true;
+    this.annotationType = null;
+  }
+
+  /**
+   * Create for custom scope.
+   */
+  ScopeInfo(ProcessingContext context, TypeElement type, AllScopes scopes) {
+    this.scopes = scopes;
+    this.context = context;
+    this.defaultScope = false;
+    this.annotationType = type;
+  }
+
+  void details(String name, Element contextElement) {
+    if (name == null || name.isEmpty()) {
+      final String simpleName = contextElement.getSimpleName().toString();
+      this.name = ScopeUtil.name(simpleName);
+    } else {
+      this.name = ScopeUtil.name(name);
+    }
+    read(contextElement);
+  }
+
+  private void read(Element element) {
+    requires(ScopeUtil.readRequires(element));
+    provides(ScopeUtil.readProvides(element));
+  }
+
+  private String initName(String topPackage) {
+    if (name == null || name.isEmpty()) {
+      name = ScopeUtil.name(topPackage);
+    }
+    return name;
+  }
+
+  void initialiseName(String topPackage) throws IOException {
+    emptyModule = topPackage == null;
+    if (!emptyModule) {
+      modulePackage = topPackage;
+      final String name = initName(modulePackage);
+      moduleShortName = name + "Module";
+      moduleFullName = modulePackage + "." + moduleShortName;
+      moduleFile = context.createWriter(moduleFullName);
+    }
+  }
+
+  JavaFileObject moduleFile() {
+    return moduleFile;
+  }
+
+  String modulePackage() {
+    return modulePackage;
+  }
+
+  String moduleFullName() {
+    return moduleFullName;
+  }
+
+  String moduleShortName() {
+    return moduleShortName;
+  }
+
+  boolean isDefaultScope() {
+    return defaultScope;
+  }
+
+  String name() {
+    return name;
+  }
+
+  private void provides(List<String> provides) {
+    this.provides.addAll(provides);
+  }
+
+  private void requires(List<String> contextRequires) {
+    this.requires.addAll(contextRequires);
+  }
+
+  Set<String> requires() {
+    return requires;
+  }
+
+  Set<String> provides() {
+    return provides;
+  }
+
+  void writeBeanHelpers() {
+    for (BeanReader beanReader : beanReaders) {
+      try {
+        if (!beanReader.isWrittenToFile()) {
+          SimpleBeanWriter writer = new SimpleBeanWriter(beanReader, context);
+          writer.write();
+          beanReader.setWrittenToFile();
+        }
+      } catch (FilerException e) {
+        context.logWarn("FilerException to write $DI class " + beanReader.getBeanType() + " " + e.getMessage());
+
+      } catch (IOException e) {
+        e.printStackTrace();
+        context.logError(beanReader.getBeanType(), "Failed to write $DI class");
+      }
+    }
+  }
+
+  private void initialiseModule() {
+    if (!moduleInitialised) {
+      try {
+        initialiseName(MetaTopPackage.of(metaData.values()));
+        moduleInitialised = true;
+      } catch (IOException e) {
+        context.logError("Failed to create module filer " + e.getMessage());
+      }
+    }
+  }
+
+  void writeModule() {
+    if (moduleWritten) {
+      context.logError("already written module " + name);
+      return;
+    }
+    final Collection<MetaData> meta = metaData.values();
+    if (emptyModule) {
+      // typically nothing in the default scope, only custom scopes
+      if (meta.size() > 0) {
+        context.logWarn("Empty module but meta is not empty? " + meta);
+      }
+      return;
+    }
+    MetaDataOrdering ordering = new MetaDataOrdering(meta, context, this);
+    int remaining = ordering.processQueue();
+    if (remaining > 0) {
+      ordering.logWarnings();
+    }
+    try {
+      SimpleModuleWriter factoryWriter = new SimpleModuleWriter(ordering, context, this);
+      factoryWriter.write(defaultScope);
+      moduleWritten = true;
+    } catch (FilerException e) {
+      context.logWarn("FilerException trying to write factory " + e.getMessage());
+    } catch (IOException e) {
+      context.logError("Failed to write factory " + e.getMessage());
+    }
+  }
+
+  /**
+   * Merge the changed bean meta data into the existing (factory) metaData.
+   */
+  void mergeMetaData() {
+    for (BeanReader beanReader : beanReaders) {
+      if (!beanReader.isRequestScopedController()) {
+        MetaData metaData = this.metaData.get(beanReader.getMetaKey());
+        if (metaData == null) {
+          addMeta(beanReader);
+        } else {
+          updateMeta(metaData, beanReader);
+        }
+      }
+    }
+  }
+
+  /**
+   * Add a new previously unknown bean.
+   */
+  private void addMeta(BeanReader beanReader) {
+    MetaData meta = beanReader.createMeta();
+    metaData.put(meta.getKey(), meta);
+    for (MetaData methodMeta : beanReader.createFactoryMethodMeta()) {
+      metaData.put(methodMeta.getKey(), methodMeta);
+    }
+  }
+
+  /**
+   * Update the meta data on a previously known bean.
+   */
+  private void updateMeta(MetaData metaData, BeanReader beanReader) {
+    metaData.update(beanReader);
+  }
+
+  /**
+   * Read the dependency injection meta data for the given bean.
+   */
+  private void readBeanMeta(TypeElement typeElement, boolean factory) {
+    if (typeElement.getKind() == ElementKind.ANNOTATION_TYPE) {
+      context.logDebug("skipping annotation type " + typeElement);
+      return;
+    }
+    beanReaders.add(new BeanReader(typeElement, context, factory).read());
+  }
+
+  void readBuildMethodDependencyMeta(Element element) {
+    Name simpleName = element.getSimpleName();
+    if (simpleName.toString().startsWith("build_")) {
+      // read a build method - DependencyMeta
+      DependencyMeta meta = element.getAnnotation(DependencyMeta.class);
+      if (meta == null) {
+        context.logError("Missing @DependencyMeta on method " + simpleName);
+      } else {
+        final MetaData metaData = new MetaData(meta);
+        this.metaData.put(metaData.getKey(), metaData);
+      }
+    }
+  }
+
+  void read(TypeElement element, boolean factory) {
+    if (readBeans.add(element.toString())) {
+      readBeanMeta(element, factory);
+    } else {
+      context.logDebug("skipping already processed bean " + element);
+    }
+  }
+
+  void write(boolean processingOver) {
+    mergeMetaData();
+    writeBeanHelpers();
+    initialiseModule();
+    if (processingOver) {
+      writeModule();
+    }
+  }
+
+  void buildAtInjectModule(Append writer) {
+    writer.append(Constants.AT_GENERATED).eol();
+    writer.append("@InjectModule(");
+    boolean leadingComma = false;
+    if (!provides.isEmpty()) {
+      attributeClasses(leadingComma, writer, "provides", provides);
+      leadingComma = true;
+    }
+    if (!requires.isEmpty()) {
+      attributeClasses(leadingComma, writer, "requires", requires);
+      leadingComma = true;
+    }
+    if (annotationType != null) {
+      if (leadingComma) {
+        writer.append(", ");
+      }
+      writer.append("customScopeType=\"%s\"", annotationType.getQualifiedName().toString());
+    }
+    writer.append(")").eol();
+  }
+
+  private void attributeClasses(boolean leadingComma, Append writer, String prefix, Set<String> classNames) {
+    if (leadingComma) {
+      writer.append(", ");
+    }
+    writer.append("%s={", prefix);
+    int c = 0;
+    for (String value : classNames) {
+      if (c++ > 0) {
+        writer.append(",");
+      }
+      writer.append(value).append(".class");
+    }
+    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");
+      }
+    }
+    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 Builder builder;").eol().eol();
+  }
+
+  void readModuleMetaData(TypeElement moduleType) {
+    context.logDebug("Reading module info for " + moduleType);
+    InjectModule module = moduleType.getAnnotation(InjectModule.class);
+    details(module.name(), moduleType);
+    readFactoryMetaData(moduleType);
+  }
+
+  private void readFactoryMetaData(TypeElement moduleType) {
+    List<? extends Element> elements = moduleType.getEnclosedElements();
+    if (elements != null) {
+      for (Element element : elements) {
+        if (ElementKind.METHOD == element.getKind()) {
+          readBuildMethodDependencyMeta(element);
+        }
+      }
+    }
+  }
+
+  /**
+   * 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) {
+    if (defaultScope) {
+      return false;
+    }
+    if (scopes.providedByDefaultModule(dependency)) {
+      return true;
+    }
+    // look for required scopes ...
+    for (String require : requires) {
+      final ScopeInfo requiredScope = scopes.get(require);
+      if (requiredScope != null) {
+        if (requiredScope.providesDependency(dependency)) {
+          // context.logWarn("dependency "+dependency+" provided by other scope "+requiredScope.name);
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Return true if this module provides the dependency.
+   */
+  boolean providesDependency(String dependency) {
+    for (MetaData meta : metaData.values()) {
+      if (dependency.equals(meta.getType())) {
+        return true;
+      }
+      final List<String> provides = meta.getProvides();
+      if (provides != null && !provides.isEmpty()) {
+        for (String provide : provides) {
+          if (dependency.equals(provide)) {
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+}
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/ScopeUtil.java b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeUtil.java
new file mode 100644
index 00000000..61d39d39
--- /dev/null
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeUtil.java
@@ -0,0 +1,79 @@
+package io.avaje.inject.generator;
+
+import javax.lang.model.element.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+class ScopeUtil {
+
+  private static final String INJECT_MODULE = "io.avaje.inject.InjectModule";
+
+  static List<String> readProvides(Element element) {
+    return readClasses(element, "provides");
+  }
+
+  static List<String> readRequires(Element element) {
+    return readClasses(element, "requires");
+  }
+
+  static List<String> readClasses(Element element, String attributeName) {
+    if (element == null) {
+      return Collections.emptyList();
+    }
+    List<String> requiresList = new ArrayList<>();
+    for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
+      if (INJECT_MODULE.equals(annotationMirror.getAnnotationType().toString())) {
+        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : annotationMirror.getElementValues().entrySet()) {
+          if (entry.getKey().toString().startsWith(attributeName)) {
+            for (Object requiresType : (List<?>) entry.getValue().getValue()) {
+              String fullName = requiresType.toString();
+              fullName = fullName.substring(0, fullName.length() - 6);
+              requiresList.add(fullName);
+            }
+          }
+        }
+      }
+    }
+    return requiresList;
+  }
+
+  static String name(String name) {
+    if (name == null) {
+      return null;
+    }
+    final int pos = name.lastIndexOf('.');
+    if (pos > -1) {
+      name = name.substring(pos + 1);
+    }
+    if (name.endsWith("Scope")) {
+      name = name.substring(0, name.length() - 5);
+    }
+    if (name.endsWith("Module")) {
+      name = name.substring(0, name.length() - 6);
+    }
+    return camelCase(name);
+  }
+
+  private static String camelCase(String name) {
+    StringBuilder sb = new StringBuilder(name.length());
+    boolean upper = true;
+    for (char aChar : name.toCharArray()) {
+      if (Character.isLetterOrDigit(aChar)) {
+        if (upper) {
+          aChar = Character.toUpperCase(aChar);
+          upper = false;
+        }
+        sb.append(aChar);
+      } else if (toUpperOn(aChar)) {
+        upper = true;
+      }
+    }
+    return sb.toString();
+  }
+
+  private static boolean toUpperOn(char aChar) {
+    return aChar == ' ' || aChar == '-' || aChar == '_';
+  }
+}
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleFactoryWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
similarity index 64%
rename from inject-generator/src/main/java/io/avaje/inject/generator/SimpleFactoryWriter.java
rename to inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
index 4122e41b..a555b8a1 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleFactoryWriter.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
@@ -1,7 +1,6 @@
 package io.avaje.inject.generator;
 
 import javax.tools.FileObject;
-import javax.tools.JavaFileObject;
 import java.io.IOException;
 import java.io.Writer;
 import java.util.List;
@@ -11,11 +10,11 @@ import java.util.TreeSet;
 /**
  * Write the source code for the factory.
  */
-class SimpleFactoryWriter {
+class SimpleModuleWriter {
 
   private static final String CODE_COMMENT_FACTORY =
     "/**\n" +
-      " * Generated source - Creates the BeanScope for the %s module.\n" +
+      " * Generated source - avaje inject module for %s.\n" +
       " * \n" +
       " * With JPMS Java module system this generated class should be explicitly\n" +
       " * registered in module-info via a <code>provides</code> clause like:\n" +
@@ -25,7 +24,7 @@ class SimpleFactoryWriter {
       " *   module example {\n" +
       " *     requires io.avaje.inject;\n" +
       " *     \n" +
-      " *     provides io.avaje.inject.spi.BeanScopeFactory with %s._DI$BeanScopeFactory;\n" +
+      " *     provides io.avaje.inject.spi.Module with %s.%s;\n" +
       " *     \n" +
       " *   }\n" +
       " * \n" +
@@ -36,43 +35,41 @@ class SimpleFactoryWriter {
     "  /**\n" +
       "   * Create the beans.\n" +
       "   * <p>\n" +
-      "   * Creates all the beans in order based on constuctor dependencies.\n" +
+      "   * Creates all the beans in order based on constructor dependencies.\n" +
       "   * The beans are registered into the builder along with callbacks for\n" +
       "   * field injection, method injection and lifecycle support.\n" +
       "   * <p>\n" +
       "   */";
 
-  private final MetaDataOrdering ordering;
   private final ProcessingContext context;
-  private final String factoryPackage;
-  private final String factoryShortName;
-  private final String factoryFullName;
+  private final String modulePackage;
+  private final String shortName;
+  private final String fullName;
+  private final ScopeInfo scopeInfo;
+  private final MetaDataOrdering ordering;
 
   private Append writer;
 
-  SimpleFactoryWriter(MetaDataOrdering ordering, ProcessingContext context) {
+  SimpleModuleWriter(MetaDataOrdering ordering, ProcessingContext context, ScopeInfo scopeInfo) {
     this.ordering = ordering;
     this.context = context;
-
-    String pkg = context.getContextPackage();
-    this.factoryPackage = (pkg != null) ? pkg : ordering.getTopPackage();
-    context.deriveContextName(factoryPackage);
-    this.factoryShortName = "_DI$BeanScopeFactory";
-    this.factoryFullName = factoryPackage + "." + factoryShortName;
+    this.scopeInfo = scopeInfo;
+    this.modulePackage = scopeInfo.modulePackage();
+    this.shortName = scopeInfo.moduleShortName();
+    this.fullName = scopeInfo.moduleFullName();
   }
 
-  void write() throws IOException {
+  void write(boolean includeServicesFile) throws IOException {
     writer = new Append(createFileWriter());
     writePackage();
     writeStartClass();
-
-    writeCreateMethod();
+    writeBuildMethod();
     writeBuildMethods();
-
     writeEndClass();
     writer.close();
-
-    writeServicesFile();
+    if (includeServicesFile) {
+      writeServicesFile();
+    }
   }
 
   private void writeServicesFile() {
@@ -80,26 +77,16 @@ class SimpleFactoryWriter {
       FileObject jfo = context.createMetaInfWriter();
       if (jfo != null) {
         Writer writer = jfo.openWriter();
-        writer.write(factoryFullName);
+        writer.write(fullName);
         writer.close();
       }
-
     } catch (IOException e) {
       e.printStackTrace();
       context.logError("Failed to write services file " + e.getMessage());
     }
   }
 
-  private void writeBuildMethods() {
-    for (MetaData metaData : ordering.getOrdered()) {
-      writer.append(metaData.buildMethod(ordering)).eol();
-    }
-    for (MetaData metaData : ordering.getRequestScope()) {
-      writer.append(metaData.buildMethod(ordering)).eol();
-    }
-  }
-
-  private void writeCreateMethod() {
+  private void writeBuildMethod() {
     writer.append(CODE_COMMENT_CREATE_CONTEXT).eol();
     writer.append("  @Override").eol();
     writer.append("  public void build(Builder builder) {").eol();
@@ -121,8 +108,17 @@ class SimpleFactoryWriter {
     writer.eol();
   }
 
+  private void writeBuildMethods() {
+    for (MetaData metaData : ordering.getOrdered()) {
+      writer.append(metaData.buildMethod(ordering)).eol();
+    }
+    for (MetaData metaData : ordering.getRequestScope()) {
+      writer.append(metaData.buildMethod(ordering)).eol();
+    }
+  }
+
   private void writePackage() {
-    writer.append("package %s;", factoryPackage).eol().eol();
+    writer.append("package %s;", modulePackage).eol().eol();
     for (String type : factoryImportTypes()) {
       writer.append("import %s;", type).eol();
     }
@@ -140,38 +136,27 @@ class SimpleFactoryWriter {
     importTypes.add(Constants.BEANCONTEXT);
     importTypes.add(Constants.INJECTMODULE);
     importTypes.add(Constants.DEPENDENCYMETA);
-    importTypes.add(Constants.BEANSCOPEFACTORY);
+    importTypes.add(Constants.MODULE);
     importTypes.add(Constants.BUILDER);
     return importTypes;
   }
 
   private void writeStartClass() {
-    writer.append(CODE_COMMENT_FACTORY, context.contextName(), factoryPackage).eol();
-    context.buildAtInjectModule(writer);
+    writer.append(CODE_COMMENT_FACTORY, scopeInfo.name(), modulePackage, shortName).eol();
+    scopeInfo.buildAtInjectModule(writer);
 
-    writer.append("public class %s implements BeanScopeFactory {", factoryShortName).eol().eol();
-    writer.append("  private final String name;").eol();
-    writer.append("  private final String[] provides;").eol();
-    writer.append("  private final String[] dependsOn;").eol();
-    writer.append("  private Builder builder;").eol().eol();
-
-    writer.append("  public %s() {", factoryShortName).eol();
-    context.buildNewBuilder(writer);
-    writer.append("  }").eol().eol();
-
-    writer.append("  @Override").eol();
-    writer.append("  public String getName() {").eol();
-    writer.append("    return name;").eol();
-    writer.append("  }").eol().eol();
+    String custom = scopeInfo.isDefaultScope() ? "" : ".Custom";
+    writer.append("public class %s implements Module%s {", shortName, custom).eol().eol();
+    scopeInfo.buildFields(writer);
 
     writer.append("  @Override").eol();
-    writer.append("  public String[] getProvides() {").eol();
+    writer.append("  public Class<?>[] provides() {").eol();
     writer.append("    return provides;").eol();
     writer.append("  }").eol().eol();
 
     writer.append("  @Override").eol();
-    writer.append("  public String[] getDependsOn() {").eol();
-    writer.append("    return dependsOn;").eol();
+    writer.append("  public Class<?>[] requires() {").eol();
+    writer.append("    return requires;").eol();
     writer.append("  }").eol().eol();
   }
 
@@ -180,8 +165,7 @@ class SimpleFactoryWriter {
   }
 
   private Writer createFileWriter() throws IOException {
-    JavaFileObject jfo = context.createWriter(factoryFullName);
-    return jfo.openWriter();
+    return scopeInfo.moduleFile().openWriter();
   }
 
 }
diff --git a/inject/src/main/java/io/avaje/inject/ApplicationScope.java b/inject-test/src/test/java/io/avaje/inject/ApplicationScope.java
similarity index 78%
rename from inject/src/main/java/io/avaje/inject/ApplicationScope.java
rename to inject-test/src/test/java/io/avaje/inject/ApplicationScope.java
index 0d6901bb..7d722d75 100644
--- a/inject/src/main/java/io/avaje/inject/ApplicationScope.java
+++ b/inject-test/src/test/java/io/avaje/inject/ApplicationScope.java
@@ -137,41 +137,4 @@ public class ApplicationScope {
     return appScope.listByAnnotation(annotation);
   }
 
-  /**
-   * Start building a RequestScope.
-   *
-   * <pre>{@code
-   *
-   *   try (RequestScope requestScope = ApplicationScope.newRequestScope()
-   *       // supply some instances
-   *       .withBean(HttpRequest.class, request)
-   *       .withBean(HttpResponse.class, response)
-   *       .build()) {
-   *
-   *       MyController controller = requestScope.get(MyController.class);
-   *       controller.process();
-   *
-   *   }
-   *
-   *   ...
-   *
-   *   // define request scoped beans
-   *   @Request
-   *   MyController {
-   *
-   *     // can depend on supplied instances, singletons and other request scope beans
-   *     @Inject
-   *     MyController(HttpRequest request, HttpResponse response, MyService myService) {
-   *       ...
-   *     }
-   *
-   *   }
-   *
-   * }</pre>
-   *
-   * @return The request scope builder
-   */
-  public static RequestScopeBuilder newRequestScope() {
-    return appScope.newRequestScope();
-  }
 }
diff --git a/inject/README.md b/inject/README.md
index 7c3f05d9..7872466a 100644
--- a/inject/README.md
+++ b/inject/README.md
@@ -4,14 +4,13 @@ APT based dependency injection for server side developers - https://avaje.io/inj
 ### Example module use
 
 ```java
-import io.avaje.inject.spi.BeanScopeFactory;
+import io.avaje.inject.spi.Module;
 
 module org.example {
 
     requires io.avaje.inject;
 
-    // register org.example._DI$BeanScopeFactory from generated sources
-    provides io.avaje.inject.spi.BeanScopeFactory with org.example._DI$BeanScopeFactory;
+    provides io.avaje.inject.spi.Module with org.example.ExampleModule;
 }
 ```
 
diff --git a/inject/pom.xml b/inject/pom.xml
index a629775a..79c1bc2c 100644
--- a/inject/pom.xml
+++ b/inject/pom.xml
@@ -4,7 +4,7 @@
   <parent>
     <groupId>io.avaje</groupId>
     <artifactId>avaje-inject-parent</artifactId>
-    <version>6.2</version>
+    <version>6.5-RC0</version>
   </parent>
 
   <artifactId>avaje-inject</artifactId>
diff --git a/inject/src/main/java/io/avaje/inject/BeanEntry.java b/inject/src/main/java/io/avaje/inject/BeanEntry.java
index cc614fa0..889757e4 100644
--- a/inject/src/main/java/io/avaje/inject/BeanEntry.java
+++ b/inject/src/main/java/io/avaje/inject/BeanEntry.java
@@ -1,107 +1,68 @@
 package io.avaje.inject;
 
+import java.util.Set;
+
 /**
  * A bean entry with priority and optional name.
+ *
+ * @see BeanScope#all()
  */
-public class BeanEntry<T> {
+public interface BeanEntry {
 
   /**
-   * An explicitly supplied bean. See BeanScopeBuilder.
+   * Priority of externally supplied bean.
    */
-  public static final int SUPPLIED = 2;
+  int SUPPLIED = 2;
 
   /**
-   * An <code>@Primary</code> bean.
+   * Priority of <code>@Primary</code> bean.
    */
-  public static final int PRIMARY = 1;
+  int PRIMARY = 1;
 
   /**
-   * A normal priority bean.
+   * Priority of normal bean.
    */
-  public static final int NORMAL = 0;
+  int NORMAL = 0;
 
   /**
-   * A <code>@Secondary</code> bean.
+   * Priority of <code>@Secondary</code> bean.
    */
-  public static final int SECONDARY = -1;
-
-  private final int priority;
-
-  private final T bean;
-
-  private final String name;
+  int SECONDARY = -1;
 
   /**
-   * Construct with priority, name and the bean.
-   */
-  public BeanEntry(int priority, T bean, String name) {
-    this.priority = priority;
-    this.bean = bean;
-    this.name = name;
-  }
-
-  /**
-   * Return the priority (Primary, Normal and Secondary).
+   * Return the bean name.
    */
-  public int getPriority() {
-    return priority;
-  }
+  String qualifierName();
 
   /**
-   * Return the bean.
+   * Return the bean instance.
    */
-  public T getBean() {
-    return bean;
-  }
+  Object bean();
 
   /**
-   * Return the bean name.
+   * The bean instance type.
    */
-  public String getName() {
-    return name;
-  }
+  Class<?> type();
 
   /**
-   * Return true if this entry has Supplied priority.
+   * Return the priority indicating if the bean is Supplied Primary, Normal or Secondary.
    */
-  public boolean isSupplied() {
-    return priority == SUPPLIED;
-  }
+  int priority();
 
   /**
-   * Return true if this entry has Primary priority.
+   * Return the type keys for this bean.
+   * <p>
+   * This is the set of type, interface types and annotation types that the entry is registered for.
    */
-  public boolean isPrimary() {
-    return priority == PRIMARY;
-  }
+  Set<String> keys();
 
   /**
-   * Return true if this entry has Secondary priority.
+   * Return true if the entry has a key for this type.
+   * <p>
+   * This is true if the keys contains the canonical name of the given type.
+   *
+   * @param type The type to match. Can be any type including concrete, interface or annotation type.
    */
-  public boolean isSecondary() {
-    return priority == SECONDARY;
-  }
+  boolean hasKey(Class<?> type);
 
-  @Override
-  public String toString() {
-    StringBuilder sb = new StringBuilder();
-    sb.append("{bean:").append(bean);
-    if (name != null) {
-      sb.append(", name:").append(name);
-    }
-    switch (priority) {
-      case SUPPLIED:
-        sb.append(", Supplied");
-        break;
-      case PRIMARY:
-        sb.append(", @Primary");
-        break;
-      case SECONDARY:
-        sb.append(", @Secondary");
-        break;
-      default:
-        sb.append(", Normal");
-    }
-    return sb.append("}").toString();
-  }
 }
diff --git a/inject/src/main/java/io/avaje/inject/BeanScope.java b/inject/src/main/java/io/avaje/inject/BeanScope.java
index 7b9138bc..b5631c22 100644
--- a/inject/src/main/java/io/avaje/inject/BeanScope.java
+++ b/inject/src/main/java/io/avaje/inject/BeanScope.java
@@ -1,6 +1,5 @@
 package io.avaje.inject;
 
-import java.io.Closeable;
 import java.lang.annotation.Annotation;
 import java.util.List;
 
@@ -13,7 +12,7 @@ import java.util.List;
  *
  * <h3>Create a BeanScope</h3>
  * <p>
- * We can programmatically create a BeanScope via BeanScopeBuilder.
+ * We can programmatically create a BeanScope via {@code BeanScope.newBuilder()}.
  * </p>
  * <pre>{@code
  *
@@ -22,94 +21,86 @@ import java.util.List;
  *   try (BeanScope scope = BeanScope.newBuilder()
  *     .build()) {
  *
- *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
+ *     CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
  *     coffeeMaker.makeIt()
  *   }
  *
  * }</pre>
  *
- * <h3>Implicitly used</h3>
+ * <h3>External dependencies</h3>
  * <p>
- * The BeanScope is implicitly used by ApplicationScope.  It will be created as needed and
- * a shutdown hook will close the underlying BeanScope on JVM shutdown.
+ * We can supporting external dependencies when creating the BeanScope. We need to do 2 things.
+ * we need to specify these via
  * </p>
+ * <ul>
+ *   <li>
+ *       1. Specify the external dependency via {@code @InjectModule(requires=...)}.
+ *       Otherwise at compile time the annotation processor detects it as a missing dependency and we can't compile.
+ *   </li>
+ *   <li>
+ *       2. Provide the dependency when creating the BeanScope
+ *   </li>
+ * </ul>
+ * <p>
+ * For example, given we have Pump as an externally provided dependency.
+ *
  * <pre>{@code
  *
- *   // BeanScope created as needed under the hood
+ *   // tell the annotation processor Pump is provided externally
+ *   // otherwise it thinks we have a missing dependency
  *
- *   CoffeeMaker coffeeMaker = ApplicationScope.get(CoffeeMaker.class);
- *   coffeeMaker.brew();
+ *   @InjectModule(requires=Pump.class)
+ *
+ * }</pre>
+ * <p>
+ * When we build the BeanScope provide the dependency via {@link BeanScopeBuilder#withBean(Class, Object)}.
+ *
+ * <pre>{@code
+ *
+ *   // provide external dependencies ...
+ *   Pump pump = ...
+ *
+ *   try (BeanScope scope = BeanScope.newBuilder()
+ *     .withBean(Pump.class, pump)
+ *     .build()) {
+ *
+ *     CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
+ *     coffeeMaker.makeIt()
+ *   }
  *
  * }</pre>
  */
-public interface BeanScope extends Closeable {
+public interface BeanScope extends AutoCloseable {
 
   /**
-   * Build a bean scope with options for shutdown hook and supplying test doubles.
+   * Build a bean scope with options for shutdown hook and supplying external dependencies.
    * <p>
-   * We would choose to use BeanScopeBuilder in test code (for component testing)
-   * as it gives us the ability to inject test doubles, mocks, spy's etc.
-   * </p>
+   * We can optionally:
+   * <ul>
+   *   <li>Provide external dependencies</li>
+   *   <li>Specify a parent BeanScope</li>
+   *   <li>Specify specific modules to wire</li>
+   *   <li>Specify to include a shutdown hook (to fire preDestroy lifecycle methods)</li>
+   *   <li>Use {@code forTesting()} to specify mocks and spies to use when wiring tests</li>
+   * </ul>
    *
    * <pre>{@code
    *
-   *   @Test
-   *   public void someComponentTest() {
-   *
-   *     MyRedisApi mockRedis = mock(MyRedisApi.class);
-   *     MyDbApi mockDatabase = mock(MyDbApi.class);
+   *   // create a BeanScope ...
    *
-   *     try (BeanScope scope = BeanScope.newBuilder()
-   *       .withBeans(mockRedis, mockDatabase)
-   *       .build()) {
+   *   try (BeanScope scope = BeanScope.newBuilder()
+   *     .build()) {
    *
-   *       // built with test doubles injected ...
-   *       CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *       coffeeMaker.makeIt();
-   *
-   *       assertThat(...
-   *     }
+   *     CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
+   *     coffeeMaker.makeIt()
    *   }
+   *
    * }</pre>
    */
   static BeanScopeBuilder newBuilder() {
     return new DBeanScopeBuilder();
   }
 
-  /**
-   * Create a RequestScope via builder where we provide extra instances
-   * that can be used/included in wiring request scoped beans.
-   *
-   * <pre>{@code
-   *
-   *   try (RequestScope requestScope = beanScope.newRequestScope()
-   *       // supply some instances
-   *       .withBean(HttpRequest.class, request)
-   *       .withBean(HttpResponse.class, response)
-   *       .build()) {
-   *
-   *       MyController controller = requestScope.get(MyController.class);
-   *       controller.process();
-   *   }
-   *
-   *   ...
-   *
-   *   // define request scoped beans
-   *   @Request
-   *   MyController {
-   *
-   *     // can depend on supplied instances, singletons and other request scope beans
-   *     @Inject
-   *     MyController(HttpRequest request, HttpResponse response, MyService myService) {
-   *       ...
-   *     }
-   *
-   *   }
-   *
-   * }</pre>
-   */
-  RequestScopeBuilder newRequestScope();
-
   /**
    * Return a single bean given the type.
    *
@@ -124,14 +115,6 @@ public interface BeanScope extends Closeable {
    */
   <T> T get(Class<T> type);
 
-  /**
-   * Deprecated - migrate to get(type)
-   */
-  @Deprecated
-  default <T> T getBean(Class<T> type) {
-    return get(type);
-  }
-
   /**
    * Return a single bean given the type and name.
    *
@@ -147,14 +130,6 @@ public interface BeanScope extends Closeable {
    */
   <T> T get(Class<T> type, String name);
 
-  /**
-   * Deprecated - migrate to get(type, name).
-   */
-  @Deprecated
-  default <T> T getBean(Class<T> type, String name) {
-    return get(type, name);
-  }
-
   /**
    * Return the list of beans that have an annotation.
    *
@@ -163,26 +138,14 @@ public interface BeanScope extends Closeable {
    *   // e.g. register all controllers with web a framework
    *   // .. where Controller is an annotation on the beans
    *
-   *   List<Object> controllers = ApplicationScope.listByAnnotation(Controller.class);
+   *   List<Object> controllers = beanScope.listByAnnotation(Controller.class);
    *
    * }</pre>
    *
-   * <p>
-   * The classic use case for this is registering controllers or routes to
-   * web frameworks like Sparkjava, Javlin, Rapidoid etc.
-   *
    * @param annotation An annotation class.
    */
   List<Object> listByAnnotation(Class<?> annotation);
 
-  /**
-   * Deprecated - migrate to listByAnnotation()
-   */
-  @Deprecated
-  default List<Object> getBeansWithAnnotation(Class<?> annotation) {
-    return listByAnnotation(annotation);
-  }
-
   /**
    * Return the list of beans that implement the interface.
    *
@@ -190,7 +153,7 @@ public interface BeanScope extends Closeable {
    *
    *   // e.g. register all routes for a web framework
    *
-   *   List<WebRoute> routes = ApplicationScope.list(WebRoute.class);
+   *   List<WebRoute> routes = beanScope.list(WebRoute.class);
    *
    * }</pre>
    *
@@ -198,27 +161,11 @@ public interface BeanScope extends Closeable {
    */
   <T> List<T> list(Class<T> interfaceType);
 
-  /**
-   * Deprecated - migrate to list(interfaceType).
-   */
-  @Deprecated
-  default <T> List<T> getBeans(Class<T> interfaceType) {
-    return list(interfaceType);
-  }
-
   /**
    * Return the list of beans that implement the interface sorting by priority.
    */
   <T> List<T> listByPriority(Class<T> interfaceType);
 
-  /**
-   * Deprecated - migrate to listByPriority(interfaceType).
-   */
-  @Deprecated
-  default <T> List<T> getBeansByPriority(Class<T> interfaceType) {
-    return listByPriority(interfaceType);
-  }
-
   /**
    * Return the beans that implement the interface sorting by the priority annotation used.
    * <p>
@@ -231,24 +178,16 @@ public interface BeanScope extends Closeable {
   <T> List<T> listByPriority(Class<T> interfaceType, Class<? extends Annotation> priority);
 
   /**
-   * Deprecated - migrate to listByPriority().
-   */
-  @Deprecated
-  default <T> List<T> getBeansByPriority(Class<T> interfaceType, Class<? extends Annotation> priority) {
-    return listByPriority(interfaceType, priority);
-  }
-
-  /**
-   * Return a request scoped provided for the specific type and name.
+   * Return all the bean entries from the scope.
+   * <p>
+   * The bean entries include entries from the parent scope if it has one.
    *
-   * @param type The type of the request scoped bean
-   * @param name The optional qualifier name
-   * @return The request scope provider or null
+   * @return All bean entries from the scope.
    */
-  <T> RequestScopeMatch<T> requestProvider(Class<T> type, String name);
+  List<BeanEntry> all();
 
   /**
-   * Close the scope firing any <code>@PreDestroy</code> methods.
+   * Close the scope firing any <code>@PreDestroy</code> lifecycle methods.
    */
   void close();
 }
diff --git a/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java
index 063e4109..e1fc90df 100644
--- a/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java
@@ -1,60 +1,47 @@
 package io.avaje.inject;
 
+import io.avaje.inject.spi.Module;
+
 import java.util.function.Consumer;
 
 /**
- * Build a bean scope with options for shutdown hook and supplying test doubles.
+ * Build a bean scope with options for shutdown hook and supplying external dependencies.
  * <p>
- * We would choose to use BeanScopeBuilder in test code (for component testing) as it gives us
- * the ability to inject test doubles, mocks, spy's etc.
+ * We can provide external dependencies that are then used in wiring the components.
  * </p>
  *
  * <pre>{@code
  *
- *   @Test
- *   public void someComponentTest() {
- *
- *     MyRedisApi mockRedis = mock(MyRedisApi.class);
- *     MyDbApi mockDatabase = mock(MyDbApi.class);
+ *   // external dependencies
+ *   Pump pump = ...
  *
- *     try (BeanScope scope = BeanScope.newBuilder()
- *       .withBeans(mockRedis, mockDatabase)
- *       .build()) {
+ *   BeanScope scope = BeanScope.newBuilder()
+ *     .withBean(pump)
+ *     .build();
  *
- *       // built with test doubles injected ...
- *       CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
- *       coffeeMaker.makeIt();
- *
- *       assertThat(...
- *     }
- *   }
+ *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+ *   coffeeMaker.makeIt();
  *
  * }</pre>
  */
 public interface BeanScopeBuilder {
 
-  /**
-   * Deprecated - migrate to {@link #withShutdownHook(boolean)}
-   */
-  @Deprecated
-  BeanScopeBuilder withNoShutdownHook();
-
   /**
    * Create the bean scope registering a shutdown hook (defaults to false, no shutdown hook).
    * <p>
-   * The expectation is that the BeanScopeBuilder is closed via code or via using
-   * try with resources.
+   * With {@code withShutdownHook(true)} a shutdown hook will be registered with the Runtime
+   * and executed when the JVM initiates a shutdown. This then will run the {@code preDestroy}
+   * lifecycle methods.
    * </p>
    * <pre>{@code
    *
    *   // automatically closed via try with resources
    *
-   *   try (BeanScope scope = BeanScope.newBuilder()
+   *   BeanScope scope = BeanScope.newBuilder()
    *     .withShutdownHook(true)
-   *     .build()) {
+   *     .build());
    *
-   *     String makeIt = scope.get(CoffeeMaker.class).makeIt();
-   *   }
+   *   // on JVM shutdown the preDestroy lifecycle methods are executed
    *
    * }</pre>
    *
@@ -64,46 +51,27 @@ public interface BeanScopeBuilder {
 
   /**
    * Specify the modules to include in dependency injection.
-   * <p/>
-   * This is effectively a "whitelist" of modules names to include in the injection excluding
-   * any other modules that might otherwise exist in the classpath.
-   * <p/>
-   * We typically want to use this in component testing where we wish to exclude any other
-   * modules that exist on the classpath.
+   * <p>
+   * Only beans related to the module are included in the BeanScope that is built.
+   * <p>
+   * When we do not explicitly specify modules then all modules that are not "custom scoped"
+   * are found and used via service loading.
    *
    * <pre>{@code
    *
-   *   @Test
-   *   public void someComponentTest() {
-   *
-   *     EmailServiceApi mockEmailService = mock(EmailServiceApi.class);
+   *   BeanScope scope = BeanScope.newBuilder()
+   *     .withModules(new CustomModule())
+   *     .build());
    *
-   *     try (BeanScope scope = BeanScope.newBuilder()
-   *       .withBeans(mockEmailService)
-   *       .withModules("coffee")
-   *       .withIgnoreMissingModuleDependencies()
-   *       .build()) {
-   *
-   *       // built with test doubles injected ...
-   *       CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *       coffeeMaker.makeIt();
-   *
-   *       assertThat(...
-   *     }
-   *   }
+   *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+   *   coffeeMaker.makeIt();
    *
    * }</pre>
    *
-   * @param modules The names of modules that we want to include in dependency injection.
+   * @param modules The modules that we want to include in dependency injection.
    * @return This BeanScopeBuilder
    */
-  BeanScopeBuilder withModules(String... modules);
-
-  /**
-   * Set this when building a BeanScope (typically for testing) and supplied beans replace module dependencies.
-   * This means we don't need the usual module dependencies as supplied beans are used instead.
-   */
-  BeanScopeBuilder withIgnoreMissingModuleDependencies();
+  BeanScopeBuilder withModules(Module... modules);
 
   /**
    * Supply a bean to the scope that will be used instead of any
@@ -115,56 +83,40 @@ public interface BeanScopeBuilder {
    *
    * <pre>{@code
    *
-   *   Pump pump = mock(Pump.class);
-   *   Grinder grinder = mock(Grinder.class);
+   *   // external dependencies
+   *   Pump pump = ...
+   *   Grinder grinder = ...
    *
-   *   try (BeanScope scope = BeanScope.newBuilder()
+   *   BeanScope scope = BeanScope.newBuilder()
    *     .withBeans(pump, grinder)
-   *     .build()) {
-   *
-   *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *     coffeeMaker.makeIt();
+   *     .build();
    *
-   *     Pump pump1 = scope.get(Pump.class);
-   *     Grinder grinder1 = scope.get(Grinder.class);
-   *
-   *     assertThat(pump1).isSameAs(pump);
-   *     assertThat(grinder1).isSameAs(grinder);
-   *
-   *     verify(pump).pumpWater();
-   *     verify(grinder).grindBeans();
-   *   }
+   *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+   *   coffeeMaker.makeIt();
    *
    * }</pre>
    *
-   * @param beans The bean used when injecting a dependency for this bean or the interface(s) it implements
+   * @param beans Externally provided beans used when injecting a dependency for the bean or the interface(s) it implements
    * @return This BeanScopeBuilder
    */
   BeanScopeBuilder withBeans(Object... beans);
 
   /**
-   * Add a supplied bean instance with the given injection type.
-   * <p>
-   * This is typically a test double often created by Mockito or similar.
-   * </p>
+   * Add a supplied bean instance with the given injection type (typically an interface type).
    *
    * <pre>{@code
    *
-   *   Pump mockPump = ...
+   *   Pump externalDependency = ...
    *
    *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withBean(Pump.class, mockPump)
+   *     .withBean(Pump.class, externalDependency)
    *     .build()) {
    *
-   *     Pump pump = scope.get(Pump.class);
-   *     assertThat(pump).isSameAs(mock);
-   *
-   *     // act
    *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
    *     coffeeMaker.makeIt();
    *
-   *     verify(pump).pumpSteam();
-   *     verify(pump).pumpWater();
+   *     Pump pump = scope.get(Pump.class);
+   *     assertThat(pump).isSameAs(externalDependency);
    *   }
    *
    * }</pre>
@@ -184,151 +136,182 @@ public interface BeanScopeBuilder {
   <D> BeanScopeBuilder withBean(String name, Class<D> type, D bean);
 
   /**
-   * Use a mockito mock when injecting this bean type.
-   *
-   * <pre>{@code
-   *
-   *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withMock(Pump.class)
-   *     .withMock(Grinder.class, grinder -> {
-   *       // setup the mock
-   *       when(grinder.grindBeans()).thenReturn("stub response");
-   *     })
-   *     .build()) {
+   * Use the given BeanScope as the parent. This becomes an additional
+   * source of beans that can be wired and accessed in this scope.
    *
-   *
-   *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *     coffeeMaker.makeIt();
-   *
-   *     // this is a mockito mock
-   *     Grinder grinder = scope.get(Grinder.class);
-   *     verify(grinder).grindBeans();
-   *   }
-   *
-   * }</pre>
+   * @param parent The BeanScope that acts as the parent
    */
-  BeanScopeBuilder withMock(Class<?> type);
+  BeanScopeBuilder withParent(BeanScope parent);
 
   /**
-   * Register as a Mockito mock with a qualifier name.
-   *
-   * <pre>{@code
+   * Extend the builder to support testing using mockito with
+   * <code>withMock()</code> and <code>withSpy()</code> methods.
    *
-   *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withMock(Store.class, "red")
-   *     .withMock(Store.class, "blue")
-   *     .build()) {
-   *
-   *     ...
-   *   }
-   *
-   * }</pre>
+   * @return The builder with extra testing support for mockito mocks and spies
    */
-  BeanScopeBuilder withMock(Class<?> type, String name);
+  BeanScopeBuilder.ForTesting forTesting();
 
   /**
-   * Use a mockito mock when injecting this bean type additionally
-   * running setup on the mock instance.
-   *
-   * <pre>{@code
-   *
-   *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withMock(Pump.class)
-   *     .withMock(Grinder.class, grinder -> {
-   *
-   *       // setup the mock
-   *       when(grinder.grindBeans()).thenReturn("stub response");
-   *     })
-   *     .build()) {
-   *
-   *
-   *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *     coffeeMaker.makeIt();
-   *
-   *     // this is a mockito mock
-   *     Grinder grinder = scope.get(Grinder.class);
-   *     verify(grinder).grindBeans();
-   *   }
+   * Build and return the bean scope.
+   * <p>
+   * The BeanScope is effectively immutable in that all components are created
+   * and all PostConstruct lifecycle methods have been invoked.
+   * <p>
+   * The beanScope effectively contains eager singletons.
    *
-   * }</pre>
+   * @return The BeanScope
    */
-  <D> BeanScopeBuilder withMock(Class<D> type, Consumer<D> consumer);
+  BeanScope build();
 
   /**
-   * Use a mockito spy when injecting this bean type.
-   *
-   * <pre>{@code
-   *
-   *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withSpy(Pump.class)
-   *     .build()) {
-   *
-   *     // setup spy here ...
-   *     Pump pump = scope.get(Pump.class);
-   *     doNothing().when(pump).pumpSteam();
-   *
-   *     // act
-   *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *     coffeeMaker.makeIt();
-   *
-   *     verify(pump).pumpWater();
-   *     verify(pump).pumpSteam();
-   *   }
-   *
-   * }</pre>
+   * Extends the building with testing specific support for mocks and spies.
    */
-  BeanScopeBuilder withSpy(Class<?> type);
+  interface ForTesting extends BeanScopeBuilder {
 
-  /**
-   * Register a Mockito spy with a qualifier name.
-   *
-   * <pre>{@code
-   *
-   *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withSpy(Store.class, "red")
-   *     .withSpy(Store.class, "blue")
-   *     .build()) {
-   *
-   *     ...
-   *   }
-   *
-   * }</pre>
-   */
-  BeanScopeBuilder withSpy(Class<?> type, String name);
+    /**
+     * Use a mockito mock when injecting this bean type.
+     *
+     * <pre>{@code
+     *
+     *   try (BeanScope scope = BeanScope.newBuilder()
+     *     .forTesting()
+     *     .withMock(Pump.class)
+     *     .withMock(Grinder.class)
+     *     .build()) {
+     *
+     *
+     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+     *     coffeeMaker.makeIt();
+     *
+     *     // this is a mockito mock
+     *     Grinder grinder = scope.get(Grinder.class);
+     *     verify(grinder).grindBeans();
+     *   }
+     *
+     * }</pre>
+     */
+    BeanScopeBuilder.ForTesting withMock(Class<?> type);
 
-  /**
-   * Use a mockito spy when injecting this bean type additionally
-   * running setup on the spy instance.
-   *
-   * <pre>{@code
-   *
-   *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withSpy(Pump.class, pump -> {
-   *       // setup the spy
-   *       doNothing().when(pump).pumpWater();
-   *     })
-   *     .build()) {
-   *
-   *     // or setup here ...
-   *     Pump pump = scope.get(Pump.class);
-   *     doNothing().when(pump).pumpSteam();
-   *
-   *     // act
-   *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *     coffeeMaker.makeIt();
-   *
-   *     verify(pump).pumpWater();
-   *     verify(pump).pumpSteam();
-   *   }
-   *
-   * }</pre>
-   */
-  <D> BeanScopeBuilder withSpy(Class<D> type, Consumer<D> consumer);
+    /**
+     * Register as a Mockito mock with a qualifier name.
+     *
+     * <pre>{@code
+     *
+     *   try (BeanScope scope = BeanScope.newBuilder()
+     *     .forTesting()
+     *     .withMock(Store.class, "red")
+     *     .withMock(Store.class, "blue")
+     *     .build()) {
+     *
+     *     ...
+     *   }
+     *
+     * }</pre>
+     */
+    BeanScopeBuilder.ForTesting withMock(Class<?> type, String name);
 
-  /**
-   * Build and return the bean scope.
-   *
-   * @return The BeanScope
-   */
-  BeanScope build();
+    /**
+     * Use a mockito mock when injecting this bean type additionally
+     * running setup on the mock instance.
+     *
+     * <pre>{@code
+     *
+     *   try (BeanScope scope = BeanScope.newBuilder()
+     *     .forTesting()
+     *     .withMock(Pump.class)
+     *     .withMock(Grinder.class, grinder -> {
+     *
+     *       // setup the mock
+     *       when(grinder.grindBeans()).thenReturn("stub response");
+     *     })
+     *     .build()) {
+     *
+     *
+     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+     *     coffeeMaker.makeIt();
+     *
+     *     // this is a mockito mock
+     *     Grinder grinder = scope.get(Grinder.class);
+     *     verify(grinder).grindBeans();
+     *   }
+     *
+     * }</pre>
+     */
+    <D> BeanScopeBuilder.ForTesting withMock(Class<D> type, Consumer<D> consumer);
+
+    /**
+     * Use a mockito spy when injecting this bean type.
+     *
+     * <pre>{@code
+     *
+     *   try (BeanScope scope = BeanScope.newBuilder()
+     *     .forTesting()
+     *     .withSpy(Pump.class)
+     *     .build()) {
+     *
+     *     // setup spy here ...
+     *     Pump pump = scope.get(Pump.class);
+     *     doNothing().when(pump).pumpSteam();
+     *
+     *     // act
+     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+     *     coffeeMaker.makeIt();
+     *
+     *     verify(pump).pumpWater();
+     *     verify(pump).pumpSteam();
+     *   }
+     *
+     * }</pre>
+     */
+    BeanScopeBuilder.ForTesting withSpy(Class<?> type);
+
+    /**
+     * Register a Mockito spy with a qualifier name.
+     *
+     * <pre>{@code
+     *
+     *   try (BeanScope scope = BeanScope.newBuilder()
+     *     .forTesting()
+     *     .withSpy(Store.class, "red")
+     *     .withSpy(Store.class, "blue")
+     *     .build()) {
+     *
+     *     ...
+     *   }
+     *
+     * }</pre>
+     */
+    BeanScopeBuilder.ForTesting withSpy(Class<?> type, String name);
+
+    /**
+     * Use a mockito spy when injecting this bean type additionally
+     * running setup on the spy instance.
+     *
+     * <pre>{@code
+     *
+     *   try (BeanScope scope = BeanScope.newBuilder()
+     *     .forTesting()
+     *     .withSpy(Pump.class, pump -> {
+     *       // setup the spy
+     *       doNothing().when(pump).pumpWater();
+     *     })
+     *     .build()) {
+     *
+     *     // or setup here ...
+     *     Pump pump = scope.get(Pump.class);
+     *     doNothing().when(pump).pumpSteam();
+     *
+     *     // act
+     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+     *     coffeeMaker.makeIt();
+     *
+     *     verify(pump).pumpWater();
+     *     verify(pump).pumpSteam();
+     *   }
+     *
+     * }</pre>
+     */
+    <D> BeanScopeBuilder.ForTesting withSpy(Class<D> type, Consumer<D> consumer);
+
+  }
 }
diff --git a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
index 850106e2..ada4fe40 100644
--- a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
@@ -1,6 +1,6 @@
 package io.avaje.inject;
 
-import io.avaje.inject.spi.BeanScopeFactory;
+import io.avaje.inject.spi.Module;
 import io.avaje.inject.spi.Builder;
 import io.avaje.inject.spi.EnrichBean;
 import io.avaje.inject.spi.SuppliedBean;
@@ -13,21 +13,21 @@ import java.util.function.Consumer;
 /**
  * Build a bean scope with options for shutdown hook and supplying test doubles.
  */
-class DBeanScopeBuilder implements BeanScopeBuilder {
+class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
 
   private static final Logger log = LoggerFactory.getLogger(DBeanScopeBuilder.class);
 
-  private boolean shutdownHook = false;
-
   @SuppressWarnings("rawtypes")
   private final List<SuppliedBean> suppliedBeans = new ArrayList<>();
 
   @SuppressWarnings("rawtypes")
   private final List<EnrichBean> enrichBeans = new ArrayList<>();
 
-  private final Set<String> includeModules = new LinkedHashSet<>();
+  private final Set<Module> includeModules = new LinkedHashSet<>();
+
+  private BeanScope parent;
 
-  private boolean ignoreMissingModuleDependencies;
+  private boolean shutdownHook;
 
   /**
    * Create a BeanScopeBuilder to ultimately load and return a new BeanScope.
@@ -36,8 +36,7 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
   }
 
   @Override
-  public BeanScopeBuilder withNoShutdownHook() {
-    this.shutdownHook = false;
+  public ForTesting forTesting() {
     return this;
   }
 
@@ -48,17 +47,11 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
   }
 
   @Override
-  public BeanScopeBuilder withModules(String... modules) {
+  public BeanScopeBuilder withModules(Module... modules) {
     this.includeModules.addAll(Arrays.asList(modules));
     return this;
   }
 
-  @Override
-  public BeanScopeBuilder withIgnoreMissingModuleDependencies() {
-    this.ignoreMissingModuleDependencies = true;
-    return this;
-  }
-
   @Override
   @SuppressWarnings({"unchecked", "rawtypes"})
   public BeanScopeBuilder withBeans(Object... beans) {
@@ -80,39 +73,45 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
   }
 
   @Override
-  public BeanScopeBuilder withMock(Class<?> type) {
+  public BeanScopeBuilder withParent(BeanScope parent) {
+    this.parent = parent;
+    return this;
+  }
+
+  @Override
+  public BeanScopeBuilder.ForTesting withMock(Class<?> type) {
     return withMock(type, null, null);
   }
 
-  public BeanScopeBuilder withMock(Class<?> type, String name) {
+  public BeanScopeBuilder.ForTesting withMock(Class<?> type, String name) {
     return withMock(type, name, null);
   }
 
   @Override
-  public <D> BeanScopeBuilder withMock(Class<D> type, Consumer<D> consumer) {
+  public <D> BeanScopeBuilder.ForTesting withMock(Class<D> type, Consumer<D> consumer) {
     return withMock(type, null, consumer);
   }
 
-  private <D> BeanScopeBuilder withMock(Class<D> type, String name, Consumer<D> consumer) {
+  private <D> BeanScopeBuilder.ForTesting withMock(Class<D> type, String name, Consumer<D> consumer) {
     suppliedBeans.add(new SuppliedBean<>(name, type, null, consumer));
     return this;
   }
 
   @Override
-  public BeanScopeBuilder withSpy(Class<?> type) {
+  public BeanScopeBuilder.ForTesting withSpy(Class<?> type) {
     return spy(type, null, null);
   }
 
-  public BeanScopeBuilder withSpy(Class<?> type, String name) {
+  public BeanScopeBuilder.ForTesting withSpy(Class<?> type, String name) {
     return spy(type, name, null);
   }
 
   @Override
-  public <D> DBeanScopeBuilder withSpy(Class<D> type, Consumer<D> consumer) {
+  public <D> BeanScopeBuilder.ForTesting withSpy(Class<D> type, Consumer<D> consumer) {
     return spy(type, null, consumer);
   }
 
-  private <D> DBeanScopeBuilder spy(Class<D> type, String name, Consumer<D> consumer) {
+  private <D> BeanScopeBuilder.ForTesting spy(Class<D> type, String name, Consumer<D> consumer) {
     enrichBeans.add(new EnrichBean<>(type, name, consumer));
     return this;
   }
@@ -120,8 +119,10 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
   @Override
   public BeanScope build() {
     // sort factories by dependsOn
-    FactoryOrder factoryOrder = new FactoryOrder(includeModules, !suppliedBeans.isEmpty(), ignoreMissingModuleDependencies);
-    ServiceLoader.load(BeanScopeFactory.class).forEach(factoryOrder::add);
+    FactoryOrder factoryOrder = new FactoryOrder(includeModules, !suppliedBeans.isEmpty());
+    if (factoryOrder.isEmpty()) {
+      ServiceLoader.load(Module.class).forEach(factoryOrder::add);
+    }
 
     Set<String> moduleNames = factoryOrder.orderFactories();
     if (moduleNames.isEmpty()) {
@@ -132,8 +133,8 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
         " Refer to https://avaje.io/inject#gradle");
     }
     log.debug("building with modules {}", moduleNames);
-    Builder builder = Builder.newBuilder(suppliedBeans, enrichBeans);
-    for (BeanScopeFactory factory : factoryOrder.factories()) {
+    Builder builder = Builder.newBuilder(suppliedBeans, enrichBeans, parent);
+    for (Module factory : factoryOrder.factories()) {
       factory.build(builder);
     }
     return builder.build(shutdownHook);
@@ -157,66 +158,55 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
    */
   static class FactoryOrder {
 
-    private final Set<String> includeModules;
     private final boolean suppliedBeans;
-    private final boolean ignoreMissingModuleDependencies;
-
     private final Set<String> moduleNames = new LinkedHashSet<>();
-    private final List<BeanScopeFactory> factories = new ArrayList<>();
+    private final List<Module> factories = new ArrayList<>();
     private final List<FactoryState> queue = new ArrayList<>();
     private final List<FactoryState> queueNoDependencies = new ArrayList<>();
 
     private final Map<String, FactoryList> providesMap = new HashMap<>();
 
-    FactoryOrder(Set<String> includeModules, boolean suppliedBeans, boolean ignoreMissingModuleDependencies) {
-      this.includeModules = includeModules;
+    FactoryOrder(Set<Module> includeModules, boolean suppliedBeans) {
+      this.factories.addAll(includeModules);
       this.suppliedBeans = suppliedBeans;
-      this.ignoreMissingModuleDependencies = ignoreMissingModuleDependencies;
+      for (Module includeModule : includeModules) {
+        moduleNames.add(includeModule.getClass().getCanonicalName());
+      }
     }
 
-    void add(BeanScopeFactory factory) {
-
-      if (includeModule(factory)) {
-        FactoryState wrappedFactory = new FactoryState(factory);
-        providesMap.computeIfAbsent(factory.getName(), s -> new FactoryList()).add(wrappedFactory);
-        if (!isEmpty(factory.getProvides())) {
-          for (String feature : factory.getProvides()) {
-            providesMap.computeIfAbsent(feature, s -> new FactoryList()).add(wrappedFactory);
-          }
+    void add(Module factory) {
+      FactoryState wrappedFactory = new FactoryState(factory);
+      providesMap.computeIfAbsent(factory.getClass().getCanonicalName(), s -> new FactoryList()).add(wrappedFactory);
+      if (!isEmpty(factory.provides())) {
+        for (Class<?> feature : factory.provides()) {
+          providesMap.computeIfAbsent(feature.getCanonicalName(), s -> new FactoryList()).add(wrappedFactory);
         }
-        if (isEmpty(factory.getDependsOn())) {
-          if (!isEmpty(factory.getProvides())) {
-            // only has 'provides' so we can push this
-            push(wrappedFactory);
-          } else {
-            // hold until after all the 'provides only' modules are added
-            queueNoDependencies.add(wrappedFactory);
-          }
+      }
+      if (isEmpty(factory.requires())) {
+        if (!isEmpty(factory.provides())) {
+          // only has 'provides' so we can push this
+          push(wrappedFactory);
         } else {
-          // queue it to process by dependency ordering
-          queue.add(wrappedFactory);
+          // hold until after all the 'provides only' modules are added
+          queueNoDependencies.add(wrappedFactory);
         }
+      } else {
+        // queue it to process by dependency ordering
+        queue.add(wrappedFactory);
       }
     }
 
-    private boolean isEmpty(String[] values) {
+    private boolean isEmpty(Class<?>[] values) {
       return values == null || values.length == 0;
     }
 
-    /**
-     * Return true of the factory (for the module) should be included.
-     */
-    private boolean includeModule(BeanScopeFactory factory) {
-      return includeModules.isEmpty() || includeModules.contains(factory.getName());
-    }
-
     /**
      * Push the factory onto the build order (the wiring order for modules).
      */
     private void push(FactoryState factory) {
       factory.setPushed();
-      factories.add(factory.getFactory());
-      moduleNames.add(factory.getName());
+      factories.add(factory.factory());
+      moduleNames.add(factory.getClass().getCanonicalName());
     }
 
     /**
@@ -235,7 +225,7 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
     /**
      * Return the list of factories in the order they should be built.
      */
-    List<BeanScopeFactory> factories() {
+    List<Module> factories() {
       return factories;
     }
 
@@ -249,7 +239,7 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
         count = processQueuedFactories();
       } while (count > 0);
 
-      if (suppliedBeans || ignoreMissingModuleDependencies) {
+      if (suppliedBeans) {
         // just push everything left assuming supplied beans
         // will satisfy the required dependencies
         for (FactoryState factoryState : queue) {
@@ -259,8 +249,8 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
       } else if (!queue.isEmpty()) {
         StringBuilder sb = new StringBuilder();
         for (FactoryState factory : queue) {
-          sb.append("Module [").append(factory.getName()).append("] has unsatisfied dependencies on modules:");
-          for (String depModuleName : factory.getDependsOn()) {
+          sb.append("Module [").append(factory.getClass().getCanonicalName()).append("] has unsatisfied dependencies on modules:");
+          for (Class<?> depModuleName : factory.requires()) {
             if (!moduleNames.contains(depModuleName)) {
               sb.append(String.format(" [%s]", depModuleName));
             }
@@ -268,7 +258,7 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
         }
 
         sb.append(". Modules that were loaded ok are:").append(moduleNames);
-        sb.append(". Consider using BeanScopeBuilder.withIgnoreMissingModuleDependencies() or BeanScopeBuilder.withSuppliedBeans(...)");
+        sb.append(". Maybe need to add external dependencies via BeanScopeBuilder.withBean()?");
         throw new IllegalStateException(sb.toString());
       }
     }
@@ -280,7 +270,6 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
      * This returns the number of factories added so once this returns 0 it is done.
      */
     private int processQueuedFactories() {
-
       int count = 0;
       Iterator<FactoryState> it = queue.iterator();
       while (it.hasNext()) {
@@ -299,14 +288,18 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
      * Return true if the (module) dependencies are satisfied for this factory.
      */
     private boolean satisfiedDependencies(FactoryState factory) {
-      for (String moduleOrFeature : factory.getDependsOn()) {
-        FactoryList factories = providesMap.get(moduleOrFeature);
+      for (Class<?> moduleOrFeature : factory.requires()) {
+        FactoryList factories = providesMap.get(moduleOrFeature.getCanonicalName());
         if (factories == null || !factories.allPushed()) {
           return false;
         }
       }
       return true;
     }
+
+    boolean isEmpty() {
+      return factories.isEmpty();
+    }
   }
 
   /**
@@ -314,10 +307,10 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
    */
   private static class FactoryState {
 
-    private final BeanScopeFactory factory;
+    private final Module factory;
     private boolean pushed;
 
-    private FactoryState(BeanScopeFactory factory) {
+    private FactoryState(Module factory) {
       this.factory = factory;
     }
 
@@ -332,16 +325,12 @@ class DBeanScopeBuilder implements BeanScopeBuilder {
       return pushed;
     }
 
-    BeanScopeFactory getFactory() {
+    Module factory() {
       return factory;
     }
 
-    String getName() {
-      return factory.getName();
-    }
-
-    String[] getDependsOn() {
-      return factory.getDependsOn();
+    Class<?>[] requires() {
+      return factory.requires();
     }
   }
 
diff --git a/inject/src/main/java/io/avaje/inject/InjectModule.java b/inject/src/main/java/io/avaje/inject/InjectModule.java
index a61d83e2..c37d3668 100644
--- a/inject/src/main/java/io/avaje/inject/InjectModule.java
+++ b/inject/src/main/java/io/avaje/inject/InjectModule.java
@@ -1,111 +1,71 @@
 package io.avaje.inject;
 
 /**
- * Used to explicitly name a bean scope and optionally specify if it depends on other bean scopes.
- * <p>
- * If this annotation is not present then the name will be derived as the "top level package name"
- * e.g. "org.example.featuretoggle"
- * </p>
- *
- * <p>
- * Typically there is a single bean scope per Jar (module). In that sense the name is the "module name" and
- * the dependsOn specifies the names of modules that this depends on (provide beans that are used to wire this module).
- * </p>
+ * Used to explicitly specify if it depends on externally provided beans or provides.
  *
+ * <h3>External dependencies</h3>
  * <p>
- * This annotation is typically placed on a top level interface or package-info in the module.
- * </p>
+ * Use {@code requires} to specify dependencies that will be provided externally.
  *
  * <pre>{@code
  *
- * package org.example.featuretoggle;
+ *   // tell the annotation processor Pump and Grinder are provided externally
+ *   // otherwise it will think we have missing dependencies at compile time
  *
- * import io.avaje.inject.InjectModule;
- *
- * @InjectModule(name = "feature-toggle")
- * public interface FeatureToggle {
- *
- *   boolean isEnabled(String key);
- * }
+ *   @InjectModule(requires = {Pump.class, Grinder.class})
  *
  * }</pre>
  *
- * <h2>dependsOn</h2>
+ * <h3>Custom scope depending on another scope</h3>
  * <p>
- * We specify <code>dependsOn</code> when we have a module that depends on beans that
- * will be supplied by another module (jar).
- * </p>
+ * When using custom scopes we can have the case where we desire one scope to depend
+ * on another. In this case we put the custom scope annotation in requires.
  * <p>
- * In the example below we have the "Job System" which depends on the common "Feature Toggle" module.
- * When wiring the Job system module we expect some beans to be provided by the feature toggle module (jar).
- * </p>
+ * For example lets say we have a custom scope called {@code StoreComponent} and that
+ * depends on {@code QueueComponent} custom scope.
  *
  * <pre>{@code
  *
- * package org.example.jobsystem;
- *
- * import io.avaje.inject.InjectModule;
+ *   @Scope
+ *   @InjectModule(requires = {QueueComponent.class})
+ *   public @interface StoreComponent {
+ *   }
  *
- * @InjectModule(name = "job-system", dependsOn = {"feature-toggle"})
- * public interface JobSystem {
- *
- *   ...
- * }
  *
  * }</pre>
  */
 public @interface InjectModule {
 
   /**
-   * The name of this context/module.
-   * <p>
-   * Other modules can then depend on this name and when they do they should wire after than module.
-   * </p>
+   * Explicitly specify the name of the module.
    */
   String name() default "";
 
   /**
-   * Additional module features that is provided by this module.
+   * Explicitly define features that are provided by this module and required by other modules.
    * <p>
-   * These names are an addition to the module name and can be used in the <code>dependsOn</code> of other modules.
-   * </p>
-   *
-   * <pre>{@code
-   *
-   * // A module that provides 'email-service' and also 'health-check'.
-   * // ie. it has bean(s) that implement a health check interface
-   * @InjectModule(name="email-service", provides={"health-checks"})
-   *
-   * // provides beans that implement a health check interface
-   * // ... wires after 'email-service'
-   * @InjectModule(name="main", provides={"health-checks"}, dependsOn={"email-service"})
-   *
-   * // wire this after all modules that provide 'health-checks'
-   * @InjectModule(name="health-check-service", dependsOn={"health-checks"})
-   *
-   * }</pre>
+   * This is used to order wiring across multiple modules. Modules that provide dependencies
+   * should be wired before modules that require dependencies.
    */
-  String[] provides() default {};
+  Class<?>[] provides() default {};
 
   /**
-   * The list of modules this scope depends on.
+   * The dependencies that are provided externally or by other modules and that are required
+   * when wiring this module.
    * <p>
-   * Effectively dependsOn specifies the modules that must wire before this module.
-   * </p>
-   * <pre>{@code
-   *
-   * // wire after a module that is called 'email-service'
-   * // ... or any module that provides 'email-service'
-   *
-   * @InjectModule(name="...", dependsOn={"email-service"})
-   *
-   * }</pre>
+   * This effectively tells the annotation processor that these types are expected to be
+   * provided and to not treat them as missing dependencies. If we don't do this the annotation
+   * processor thinks the dependency is missing and will error the compilation saying there is
+   * a missing dependency.
    */
-  String[] dependsOn() default {};
+  Class<?>[] requires() default {};
 
   /**
-   * The dependencies that are provided externally and required when wiring this module.
+   * Internal use only - identifies the custom scope annotation associated to this module.
+   * <p>
+   * When a module is generated for a custom scope this is set to link the module back to the
+   * custom scope annotation and support partial compilation.
    */
-  Class<?>[] requires() default {};
+  String customScopeType() default "";
 
 }
diff --git a/inject/src/main/java/io/avaje/inject/Priority.java b/inject/src/main/java/io/avaje/inject/Priority.java
index 41de7583..937d9b14 100644
--- a/inject/src/main/java/io/avaje/inject/Priority.java
+++ b/inject/src/main/java/io/avaje/inject/Priority.java
@@ -9,7 +9,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 /**
  * The <code>Priority</code> annotation can be applied to classes to indicate
- * in what order they should be returned via @{@link ApplicationScope#listByPriority(Class)}.
+ * in what order they should be returned via @{@link BeanScope#listByPriority(Class)}.
  * <p>
  * Beans can be returned using other Priority annotation such as <code>javax.annotation.Priority</code>
  * or any custom priority annotation that has an <code>int value()</code> attribute.
diff --git a/inject/src/main/java/io/avaje/inject/Request.java b/inject/src/main/java/io/avaje/inject/Request.java
deleted file mode 100644
index e5835efa..00000000
--- a/inject/src/main/java/io/avaje/inject/Request.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package io.avaje.inject;
-
-import jakarta.inject.Scope;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Marks a request scoped bean.
- * <p>
- * Request scoped beans are only available via {@link RequestScope}.
- */
-@Scope
-@Documented
-@Retention(RetentionPolicy.RUNTIME)
-public @interface Request {
-
-}
diff --git a/inject/src/main/java/io/avaje/inject/RequestScope.java b/inject/src/main/java/io/avaje/inject/RequestScope.java
deleted file mode 100644
index 8758ae79..00000000
--- a/inject/src/main/java/io/avaje/inject/RequestScope.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package io.avaje.inject;
-
-import jakarta.inject.Provider;
-
-import java.io.Closeable;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * Provides request scoped beans in addition to beans from the underlying bean scope.
- */
-public interface RequestScope extends Closeable {
-
-  /**
-   * Get a dependency.
-   */
-  <T> T get(Class<T> type);
-
-  /**
-   * Get a named dependency.
-   */
-  <T> T get(Class<T> type, String name);
-
-  /**
-   * Get an optional dependency.
-   */
-  <T> Optional<T> getOptional(Class<T> type);
-
-  /**
-   * Get an optional named dependency.
-   */
-  <T> Optional<T> getOptional(Class<T> type, String name);
-
-  /**
-   * Get an optional dependency potentially returning null.
-   */
-  <T> T getNullable(Class<T> type);
-
-  /**
-   * Get an optional named dependency potentially returning null.
-   */
-  <T> T getNullable(Class<T> type, String name);
-
-  /**
-   * Return Provider of T given the type.
-   */
-  <T> Provider<T> getProvider(Class<T> type);
-
-  /**
-   * Return Provider of T given the type and name.
-   */
-  <T> Provider<T> getProvider(Class<T> type, String name);
-
-  /**
-   * Get a list of dependencies for the interface type .
-   */
-  <T> List<T> list(Class<T> interfaceType);
-
-  /**
-   * Get a set of dependencies for the interface type .
-   */
-  <T> Set<T> set(Class<T> interfaceType);
-
-  /**
-   * Register a closable with the request scope.
-   * <p>
-   * All closeables registered here are closed at the end of the request scope.
-   */
-  void addClosable(Closeable closeable);
-
-  /**
-   * Close the scope firing any <code>@PreDestroy</code> methods.
-   */
-  void close();
-
-}
diff --git a/inject/src/main/java/io/avaje/inject/RequestScopeBuilder.java b/inject/src/main/java/io/avaje/inject/RequestScopeBuilder.java
deleted file mode 100644
index ec783f83..00000000
--- a/inject/src/main/java/io/avaje/inject/RequestScopeBuilder.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package io.avaje.inject;
-
-/**
- * Builder for RequestScope.
- * <p>
- * When building a request scope we can provide additional instances that can then we wired as
- * dependencies to request scoped beans.
- */
-public interface RequestScopeBuilder {
-
-  /**
-   * Provide a bean that can be dependency of a request scoped bean.
-   *
-   * @param type The type of provided bean
-   * @param bean The instance being provided
-   * @return The builder
-   */
-  <D> RequestScopeBuilder withBean(Class<D> type, D bean);
-
-  /**
-   * Provide a bean that can be dependency of a request scoped bean.
-   *
-   * @param name The qualifier name of the provided bean
-   * @param type The type of provided bean
-   * @param bean The instance being provided
-   * @return The builder
-   */
-  <D> RequestScopeBuilder withBean(String name, Class<D> type, D bean);
-
-  /**
-   * Build and return the request scope.
-   */
-  RequestScope build();
-}
diff --git a/inject/src/main/java/io/avaje/inject/RequestScopeMatch.java b/inject/src/main/java/io/avaje/inject/RequestScopeMatch.java
deleted file mode 100644
index 09f42d3d..00000000
--- a/inject/src/main/java/io/avaje/inject/RequestScopeMatch.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package io.avaje.inject;
-
-import java.util.List;
-
-/**
- * Match for request scope provider.
- */
-public interface RequestScopeMatch<T> {
-
-  /**
-   * Return all the keys that match the provider.
-   */
-  List<String> keys();
-
-  /**
-   * Return the provider.
-   */
-  RequestScopeProvider<T> provider();
-}
diff --git a/inject/src/main/java/io/avaje/inject/RequestScopeProvider.java b/inject/src/main/java/io/avaje/inject/RequestScopeProvider.java
deleted file mode 100644
index 61ab6c3a..00000000
--- a/inject/src/main/java/io/avaje/inject/RequestScopeProvider.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package io.avaje.inject;
-
-/**
- * Provides request scoped beans.
- */
-public interface RequestScopeProvider<T> {
-
-  /**
-   * Create and return the request scope bean instance.
-   *
-   * @param scope The request scope when creating the bean
-   * @return The request scoped bean instance
-   */
-  <T> T provide(RequestScope scope);
-
-}
diff --git a/inject/src/main/java/io/avaje/inject/SystemContext.java b/inject/src/main/java/io/avaje/inject/SystemContext.java
deleted file mode 100644
index 84899e7a..00000000
--- a/inject/src/main/java/io/avaje/inject/SystemContext.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package io.avaje.inject;
-
-import java.util.List;
-
-/**
- * Deprecated - migrate to ApplicationScope.
- * <p>
- * Provides a global system wide BeanScope that contains all the beans.
- * <p>
- * This will automatically get all the beans and wire them all as necessary. It will use
- * a shutdown hook to fire any <code>@PreDestroy</code> methods on beans.
- * </p>
- *
- * <h3>Example: get a bean</h3>
- * <pre>{@code
- *
- *   CoffeeMaker coffeeMaker = ApplicationScope.get(CoffeeMaker.class);
- *   coffeeMaker.brew();
- *
- * }</pre>
- */
-@Deprecated
-public class SystemContext {
-
-  private SystemContext() {
-    // hide
-  }
-
-  /**
-   * Deprecated - migrate to ApplicationScope.scope().
-   */
-  @Deprecated
-  public static BeanScope context() {
-    return ApplicationScope.scope();
-  }
-
-  /**
-   * Deprecated - migrate to ApplicationScope.get(type).
-   */
-  @Deprecated
-  public static <T> T getBean(Class<T> type) {
-    return ApplicationScope.get(type);
-  }
-
-  /**
-   * Deprecated - migrate to ApplicationScope.get(type, name).
-   */
-  @Deprecated
-  public static <T> T getBean(Class<T> type, String name) {
-    return ApplicationScope.get(type, name);
-  }
-
-  /**
-   * Deprecated - migrate to ApplicationScope.listByAnnotation(annotation).
-   */
-  @Deprecated
-  public static List<Object> getBeansWithAnnotation(Class<?> annotation) {
-    return ApplicationScope.listByAnnotation(annotation);
-  }
-
-  /**
-   * Deprecated - migrate to ApplicationScope.list().
-   */
-  @Deprecated
-  public static <T> List<T> getBeans(Class<T> interfaceType) {
-    return ApplicationScope.list(interfaceType);
-  }
-
-  /**
-   * Deprecated - migrate to ApplicationScope.listByPriority().
-   */
-  @Deprecated
-  public static <T> List<T> getBeansByPriority(Class<T> interfaceType) {
-    return ApplicationScope.listByPriority(interfaceType);
-  }
-
-}
diff --git a/inject/src/main/java/io/avaje/inject/package-info.java b/inject/src/main/java/io/avaje/inject/package-info.java
index 102be215..9a4c405c 100644
--- a/inject/src/main/java/io/avaje/inject/package-info.java
+++ b/inject/src/main/java/io/avaje/inject/package-info.java
@@ -1,4 +1,159 @@
 /**
- * Avaje Inject API - see {@link io.avaje.inject.ApplicationScope} and {@link io.avaje.inject.BeanScope}.
+ * Avaje Inject API - see {@link io.avaje.inject.BeanScope}.
+ *
+ * <h2>Overview</h2>
+ *
+ * <h4>1. @Singleton / @Factory</h4>
+ * <p>
+ * Put <code>@Singleton</code> on beans that we want avaje-inject to wire.
+ * <p>
+ * We can use <code>@Factory/@Bean</code> to programmatically create dependencies
+ * when they have interesting construction logic (e.g. construction depends on
+ * system/environment properties etc - like Spring @Configuration).
+ *
+ * <pre>{@code
+ *
+ *   @Singleton
+ *   public class CoffeeMaker { ... }
+ *
+ *   @Singleton
+ *   public class Pump { ... }
+ *
+ *   // Use @Factory to programmatically build dependencies
+ *
+ *   @Factory
+ *   public class MyFactory {
+ *
+ *     @Bean
+ *     Grinder buildGrinder(Pump pump) { // interesting construction logic ... }
+ *
+ *   }
+ *
+ * }</pre>
+ *
+ * <h4>2. Create and use BeanScope</h4>
+ * <p>
+ * Create {@link io.avaje.inject.BeanScope} using a builder.
+ * Obtain beans from the scope and use them.
+ * <p>
+ * We should ensure the BeanScope is closed in order to fire
+ * preDestroy lifecycle methods. We can do this via a shutdown
+ * hook, or try with resource block or explicitly via application code.
+ *
+ * <pre>{@code
+ *
+ *   // create BeanScope
+ *   BeanScope scope = BeanScope.newBuilder()
+ *     .build();
+ *
+ *   // use it
+ *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+ *   coffeeMaker.makeIt();
+ *
+ *   // close it to fire preDestroy lifecycle methods
+ *   scope.close();
+ *
+ * }</pre>
+ *
+ * <h2>Default scope</h3>
+ * <p>
+ * All beans annotated with {@code @Singleton} and {@code @Factory} are by default included in
+ * the "default scope".
+ * <p>
+ * When we create a BeanScope and do not specify any modules then all the beans in the default
+ * scope are included. This is done via ServiceLoader and includes all 'default scope' modules
+ * that are in the classpath (other jars can have default scope modules and these are all included).
+ *
+ * <h4>Generated code</h4>
+ * <p>
+ * <em>avaje-inject</em> will generate a <code>$DI</code> class for each bean that it is going to
+ * wire - this has the code that instantiates the bean, performs field and method injection and
+ * lifecycle support (PostConstruct and PreDestroy).
+ * <p>
+ * <em>avaje-inject</em> will generate a Module class for the default scope. The main job of
+ * the module code is to specify the ordering of how all the beans are instantiated.
+ * <p>
+ * We typically find the generated source code in  <em>target/generate-sources/annotations</em>.
+ *
+ *
+ * <h2>Custom scopes</h3>
+ * <p>
+ * We can create custom scopes that only contain the beans/components that we want to include in
+ * that scope. These beans/components in the custom scope are not included in the default scope.
+ * <p>
+ * To do this we:
+ *
+ * <h4>1. Create the scope annotation</h4>
+ * <p>
+ * Create an annotation that has the {@code Scope} annotation. In the example below we create the
+ * <code>@StoreComponent</code> annotation. We will put this annotation on beans that we want
+ * included in the scope.
+ *
+ * <pre>{@code
+ *
+ *    @Scope
+ *    public @interface StoreComponent {
+ *    }
+ *
+ * }</pre>
+ *
+ * <p>
+ * Note that if this scope depends on external dependencies or another scope we specify that via
+ * {@code @InjectModule requires}. In the example below our StoreComponent depends on another
+ * scope called QueueComponent and an external dependency - SomeExternalDependency.
+ *
+ * <pre>{@code
+ *
+ *    @Scope
+ *    @InjectModule(requires = {QueueComponent.class, SomeExternalDependency.class})
+ *    public @interface StoreComponent {
+ *    }
+ *
+ * }</pre>
+ *
+ * <h4>2. Use the annotation</h4>
+ * <p>
+ * Put the <code>@StoreComponent</code> annotation on the beans that we want included in the
+ * custom scope.
+ *
+ * <pre>{@code
+ *
+ *   @StoreComponent
+ *   public class StoreLoader {
+ *     ...
+ *   }
+ *
+ * }</pre>
+ *
+ *
+ * <h4>3. Generated Module</h4>
+ * <p>
+ * <em>avaje-inject</em> will generate a <code>StoreComponentModule</code> with the appropriate
+ * code to create/wire all the beans in the custom scope.
+ * <p>
+ * StoreComponentModule is typically found in <em>target/generate-sources/annotations</em>.
+ * For each component we will also see a <code>$DI</code> class which is the generated code
+ * that creates the component.
+ *
+ * <h4>4. Use the custom scope</h4>
+ * <p>
+ * To use the custom scope we specify the <code>StoreComponentModule</code> when creating the
+ * BeanScope. Only the components in our module will be create/wired into the BeanScope.
+ * <p>
+ * If the scope depends on another scope then we specify that using {@code withParent()}.
+ *
+ * <pre>{@code
+ *
+ *   BeanScope parentScope = ...
+ *
+ *   BeanScope scope = BeanScope.newBuilder()
+ *     .withModules(new StoreComponentModule())
+ *     .withParent(parentScope)
+ *     .build());
+ *
+ *   StoreLoader storeLoader = scope.get(StoreLoader.class);
+ *   storeLoader.load();
+ *
+ * }</pre>
  */
 package io.avaje.inject;
diff --git a/inject/src/main/java/io/avaje/inject/spi/BeanScopeFactory.java b/inject/src/main/java/io/avaje/inject/spi/BeanScopeFactory.java
deleted file mode 100644
index 4d4a2f7a..00000000
--- a/inject/src/main/java/io/avaje/inject/spi/BeanScopeFactory.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package io.avaje.inject.spi;
-
-/**
- * This is the service loader interface defining the bean scope.
- */
-public interface BeanScopeFactory {
-
-  /**
-   * Return the name of the bean scope (module) this will create.
-   */
-  String getName();
-
-  /**
-   * Return the name of module features this module provides.
-   */
-  String[] getProvides();
-
-  /**
-   * Return the names of bean scopes (modules) that this is dependent on (they need to be built before this one).
-   */
-  String[] getDependsOn();
-
-  /**
-   * Build all the beans.
-   */
-  void build(Builder builder);
-}
diff --git a/inject/src/main/java/io/avaje/inject/spi/Builder.java b/inject/src/main/java/io/avaje/inject/spi/Builder.java
index fba3ff65..d6db7157 100644
--- a/inject/src/main/java/io/avaje/inject/spi/Builder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/Builder.java
@@ -1,7 +1,6 @@
 package io.avaje.inject.spi;
 
 import io.avaje.inject.BeanScope;
-import io.avaje.inject.RequestScopeProvider;
 import jakarta.inject.Provider;
 
 import java.util.List;
@@ -19,14 +18,15 @@ public interface Builder {
    *
    * @param suppliedBeans The list of beans (typically test doubles) supplied when building the context.
    * @param enrichBeans   The list of classes we want to have with mockito spy enhancement
+   * @param parent        The parent BeanScope
    */
   @SuppressWarnings("rawtypes")
-  static Builder newBuilder(List<SuppliedBean> suppliedBeans, List<EnrichBean> enrichBeans) {
+  static Builder newBuilder(List<SuppliedBean> suppliedBeans, List<EnrichBean> enrichBeans, BeanScope parent) {
     if (suppliedBeans.isEmpty() && enrichBeans.isEmpty()) {
       // simple case, no mocks or spies
-      return new DBuilder();
+      return new DBuilder(parent);
     }
-    return new DBuilderExtn(suppliedBeans, enrichBeans);
+    return new DBuilderExtn(parent, suppliedBeans, enrichBeans);
   }
 
   /**
@@ -50,23 +50,6 @@ public interface Builder {
    */
   boolean isAddBeanFor(Class<?>... types);
 
-  /**
-   * Register a request scoped bean provider.
-   *
-   * @param type     The type of the bean being provided
-   * @param provider The provider
-   */
-  <T> void requestScope(Class<T> type, RequestScopeProvider<T> provider);
-
-  /**
-   * Register a request scoped bean provider.
-   *
-   * @param name     The qualifier name of the bean being provided
-   * @param type     The type of the bean being provided
-   * @param provider The provider
-   */
-  <T> void requestScope(Class<T> type, RequestScopeProvider<T> provider, String name, Class<?>... interfaceTypes);
-
   /**
    * Register the bean instance into the context.
    *
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 8984abe3..b85a87a5 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
@@ -1,10 +1,11 @@
 package io.avaje.inject.spi;
 
-import io.avaje.inject.BeanEntry;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 
-import java.util.*;
-
-import static io.avaje.inject.BeanEntry.*;
+import static io.avaje.inject.BeanEntry.SUPPLIED;
 
 /**
  * Map of types (class types, interfaces and annotations) to a DContextEntry where the
@@ -19,6 +20,17 @@ class DBeanMap {
   DBeanMap() {
   }
 
+  /**
+   * Add to the map of entries.
+   */
+  void addAll(Map<DContextEntryBean,DEntry> map) {
+    for (Map.Entry<String, DContextEntry> entry : beans.entrySet()) {
+      for (DContextEntryBean contentEntry : entry.getValue().entries()) {
+        map.computeIfAbsent(contentEntry, dContextEntryBean -> contentEntry.entry()).addKey(entry.getKey());
+      }
+    }
+  }
+
   /**
    * Add test double supplied beans.
    */
diff --git a/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java b/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java
index ed619b20..8de6d0b1 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java
@@ -1,18 +1,15 @@
 package io.avaje.inject.spi;
 
-import io.avaje.inject.*;
+import io.avaje.inject.BeanEntry;
+import io.avaje.inject.BeanScope;
+import io.avaje.inject.Priority;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.lang.annotation.Annotation;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.locks.ReentrantLock;
 
-import static io.avaje.inject.spi.KeyUtil.key;
-
 class DBeanScope implements BeanScope {
 
   private static final Logger log = LoggerFactory.getLogger(DBeanScope.class);
@@ -21,16 +18,16 @@ class DBeanScope implements BeanScope {
   private final List<Runnable> postConstruct;
   private final List<AutoCloseable> preDestroy;
   private final DBeanMap beans;
-  private final Map<String, RequestScopeMatch<?>> reqScopeProviders;
   private final ShutdownHook shutdownHook;
+  private final BeanScope parent;
   private boolean shutdown;
   private boolean closed;
 
-  DBeanScope(boolean withShutdownHook, List<AutoCloseable> preDestroy, List<Runnable> postConstruct, DBeanMap beans, Map<String, RequestScopeMatch<?>> reqScopeProviders) {
+  DBeanScope(boolean withShutdownHook, List<AutoCloseable> preDestroy, List<Runnable> postConstruct, DBeanMap beans, BeanScope parent) {
     this.preDestroy = preDestroy;
     this.postConstruct = postConstruct;
     this.beans = beans;
-    this.reqScopeProviders = reqScopeProviders;
+    this.parent = parent;
     if (withShutdownHook) {
       this.shutdownHook = new ShutdownHook(this);
       Runtime.getRuntime().addShutdownHook(shutdownHook);
@@ -40,14 +37,17 @@ class DBeanScope implements BeanScope {
   }
 
   @Override
-  public RequestScopeBuilder newRequestScope() {
-    return new DRequestScopeBuilder(this);
+  public List<BeanEntry> all() {
+    IdentityHashMap<DContextEntryBean, DEntry> map = new IdentityHashMap<>();
+    if (parent != null) {
+      ((DBeanScope) parent).addAll(map);
+    }
+    addAll(map);
+    return new ArrayList<>(map.values());
   }
 
-  @SuppressWarnings("unchecked")
-  @Override
-  public <T> RequestScopeMatch<T> requestProvider(Class<T> type, String name) {
-    return (RequestScopeMatch<T>) reqScopeProviders.get(key(type, name));
+  void addAll(Map<DContextEntryBean, DEntry> map) {
+    beans.addAll(map);
   }
 
   @Override
@@ -57,13 +57,32 @@ class DBeanScope implements BeanScope {
 
   @Override
   public <T> T get(Class<T> beanClass, String name) {
-    return beans.get(beanClass, name);
+    final T bean = beans.get(beanClass, name);
+    if (bean != null) {
+      return bean;
+    }
+    return (parent == null) ? null : parent.get(beanClass, name);
   }
 
   @SuppressWarnings("unchecked")
   @Override
   public <T> List<T> list(Class<T> interfaceType) {
-    return (List<T>) beans.all(interfaceType);
+    List<T> values = (List<T>) beans.all(interfaceType);
+    if (parent == null) {
+      return values;
+    }
+    return combine(values, parent.list(interfaceType));
+  }
+
+  static <T> List<T> combine(List<T> values, List<T> parentValues) {
+    if (values.isEmpty()) {
+      return parentValues;
+    } else if (parentValues.isEmpty()) {
+      return values;
+    } else {
+      values.addAll(parentValues);
+      return values;
+    }
   }
 
   @Override
@@ -102,7 +121,11 @@ class DBeanScope implements BeanScope {
 
   @Override
   public List<Object> listByAnnotation(Class<?> annotation) {
-    return beans.all(annotation);
+    final List<Object> values = beans.all(annotation);
+    if (parent == null) {
+      return values;
+    }
+    return combine(values, parent.listByAnnotation(annotation));
   }
 
   DBeanScope start() {
@@ -165,7 +188,6 @@ class DBeanScope implements BeanScope {
     }
   }
 
-
   private static class SortBean<T> implements Comparable<SortBean<T>> {
 
     private final T bean;
diff --git a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
index 8ae4832b..c4dcd71e 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
@@ -2,13 +2,13 @@ package io.avaje.inject.spi;
 
 import io.avaje.inject.BeanEntry;
 import io.avaje.inject.BeanScope;
-import io.avaje.inject.RequestScopeMatch;
-import io.avaje.inject.RequestScopeProvider;
 import jakarta.inject.Provider;
 
 import java.util.*;
 import java.util.function.Consumer;
 
+import static io.avaje.inject.spi.DBeanScope.combine;
+
 class DBuilder implements Builder {
 
   /**
@@ -22,15 +22,12 @@ class DBuilder implements Builder {
    */
   private final List<Consumer<Builder>> injectors = new ArrayList<>();
 
-  /**
-   * Map of request scope providers and their associated keys.
-   */
-  private final Map<String, RequestScopeMatch<?>> reqScopeProviders = new HashMap<>();
-
   /**
    * The beans created and added to the scope during building.
    */
-  final DBeanMap beanMap = new DBeanMap();
+  protected final DBeanMap beanMap = new DBeanMap();
+
+  private final BeanScope parent;
 
   /**
    * Debug of the current bean being wired - used in injection errors.
@@ -42,6 +39,10 @@ class DBuilder implements Builder {
    */
   private boolean runningPostConstruct;
 
+  DBuilder(BeanScope parent) {
+    this.parent = parent;
+  }
+
   @Override
   public boolean isAddBeanFor(Class<?>... types) {
     return isAddBeanFor(null, types);
@@ -58,19 +59,6 @@ class DBuilder implements Builder {
     beanMap.nextBean(name, types);
   }
 
-  @Override
-  public <T> void requestScope(Class<T> type, RequestScopeProvider<T> provider) {
-    requestScope(type, provider, null, null);
-  }
-
-  @Override
-  public <T> void requestScope(Class<T> type, RequestScopeProvider<T> provider, String name, Class<?>... types) {
-    final DRequestScopeMatch<T> match = DRequestScopeMatch.of(provider, type, name, types);
-    for (String key : match.keys()) {
-      reqScopeProviders.put(key, match);
-    }
-  }
-
   private Class<?> firstOf(Class<?>[] types) {
     return types != null && types.length > 0 ? types[0] : null;
   }
@@ -83,11 +71,19 @@ class DBuilder implements Builder {
   @SuppressWarnings({"unchecked"})
   @Override
   public <T> List<T> list(Class<T> interfaceType) {
-    return (List<T>) beanMap.all(interfaceType);
+    List<T> values = (List<T>) beanMap.all(interfaceType);
+    if (parent == null) {
+      return values;
+    }
+    return combine(values, parent.list(interfaceType));
   }
 
   private <T> T getMaybe(Class<T> beanClass, String name) {
-    return beanMap.get(beanClass, name);
+    T bean = beanMap.get(beanClass, name);
+    if (bean != null) {
+      return bean;
+    }
+    return (parent == null) ? null : parent.get(beanClass, name);
   }
 
   /**
@@ -202,6 +198,6 @@ class DBuilder implements Builder {
 
   public BeanScope build(boolean withShutdownHook) {
     runInjectors();
-    return new DBeanScope(withShutdownHook, preDestroy, postConstruct, beanMap, reqScopeProviders).start();
+    return new DBeanScope(withShutdownHook, preDestroy, postConstruct, beanMap, parent).start();
   }
 }
diff --git a/inject/src/main/java/io/avaje/inject/spi/DBuilderExtn.java b/inject/src/main/java/io/avaje/inject/spi/DBuilderExtn.java
index 04aaa84a..ee7f9936 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBuilderExtn.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBuilderExtn.java
@@ -1,5 +1,7 @@
 package io.avaje.inject.spi;
 
+import io.avaje.inject.BeanScope;
+
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -15,8 +17,8 @@ class DBuilderExtn extends DBuilder {
   private final boolean hasSuppliedBeans;
 
   @SuppressWarnings("rawtypes")
-  DBuilderExtn(List<SuppliedBean> suppliedBeans, List<EnrichBean> enrichBeans) {
-    super();
+  DBuilderExtn(BeanScope parent, List<SuppliedBean> suppliedBeans, List<EnrichBean> enrichBeans) {
+    super(parent);
     this.hasSuppliedBeans = (suppliedBeans != null && !suppliedBeans.isEmpty());
     if (hasSuppliedBeans) {
       beanMap.add(suppliedBeans);
diff --git a/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java b/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java
index 23770dff..d89cf6bb 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java
@@ -12,6 +12,10 @@ class DContextEntry {
 
   private final List<DContextEntryBean> entries = new ArrayList<>(5);
 
+  List<DContextEntryBean> entries() {
+    return entries;
+  }
+
   void add(DContextEntryBean entryBean) {
     entries.add(entryBean);
   }
@@ -102,7 +106,8 @@ class DContextEntry {
         match = entry;
         return;
       }
-      throw new IllegalStateException("Expecting only 1 bean match but have multiple matching beans " + match.getBean() + " and " + entry.getBean());
+      throw new IllegalStateException("Expecting only 1 bean match but have multiple matching beans " + match.getBean()
+        + " and " + entry.getBean() + ". Maybe need a rebuild is required after adding a @Named qualifier?");
     }
 
     private Object candidate() {
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 75ed5db2..817c9634 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
@@ -25,7 +25,7 @@ class DContextEntryBean {
     }
   }
 
-  final Object source;
+  protected final Object source;
   private final String name;
   private final int flag;
 
@@ -35,6 +35,10 @@ class DContextEntryBean {
     this.flag = flag;
   }
 
+  DEntry entry() {
+    return new DEntry(name, flag, getBean());
+  }
+
   /**
    * Return true if qualifierName is null or matched.
    */
diff --git a/inject/src/main/java/io/avaje/inject/spi/DEntry.java b/inject/src/main/java/io/avaje/inject/spi/DEntry.java
new file mode 100644
index 00000000..de948601
--- /dev/null
+++ b/inject/src/main/java/io/avaje/inject/spi/DEntry.java
@@ -0,0 +1,54 @@
+package io.avaje.inject.spi;
+
+import io.avaje.inject.BeanEntry;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+class DEntry implements BeanEntry {
+
+  private final String qualifierName;
+  private final int priority;
+  private final Object bean;
+  private Set<String> keys = new LinkedHashSet<>();
+
+  DEntry(String qualifierName, int priority, Object bean) {
+    this.qualifierName = qualifierName;
+    this.priority = priority;
+    this.bean = bean;
+  }
+
+  void addKey(String key) {
+    keys.add(key);
+  }
+
+  @Override
+  public int priority() {
+    return priority;
+  }
+
+  @Override
+  public Object bean() {
+    return bean;
+  }
+
+  @Override
+  public Class<?> type() {
+    return bean.getClass();
+  }
+
+  @Override
+  public String qualifierName() {
+    return qualifierName;
+  }
+
+  @Override
+  public Set<String> keys() {
+    return keys;
+  }
+
+  @Override
+  public boolean hasKey(Class<?> type) {
+    return keys.contains(type.getCanonicalName());
+  }
+}
diff --git a/inject/src/main/java/io/avaje/inject/spi/DRequestScope.java b/inject/src/main/java/io/avaje/inject/spi/DRequestScope.java
deleted file mode 100644
index 740f3c99..00000000
--- a/inject/src/main/java/io/avaje/inject/spi/DRequestScope.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package io.avaje.inject.spi;
-
-import io.avaje.inject.BeanScope;
-import io.avaje.inject.RequestScope;
-import io.avaje.inject.RequestScopeMatch;
-import jakarta.inject.Provider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.Closeable;
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-
-import static io.avaje.inject.spi.KeyUtil.key;
-
-/**
- * Implementation of RequestScope.
- */
-class DRequestScope implements RequestScope {
-
-  private static final Logger log = LoggerFactory.getLogger(RequestScope.class);
-
-  private final BeanScope beanScope;
-  private final ConcurrentHashMap<String, Object> supplied;
-  private final List<Closeable> closeables = new ArrayList<>();
-
-  DRequestScope(BeanScope beanScope, ConcurrentHashMap<String, Object> supplied) {
-    this.beanScope = beanScope;
-    this.supplied = supplied;
-  }
-
-  @SuppressWarnings("unchecked")
-  @Override
-  public <T> T getNullable(Class<T> type, String name) {
-    Object suppliedBean = supplied.get(key(type, name));
-    if (suppliedBean != null) {
-      return (T) suppliedBean;
-    }
-    RequestScopeMatch<T> reqMatch = beanScope.requestProvider(type, name);
-    if (reqMatch != null) {
-      return createRequestScopeBean(reqMatch);
-    }
-    return beanScope.get(type, name);
-  }
-
-  @SuppressWarnings("unchecked")
-  private <T> T createRequestScopeBean(RequestScopeMatch<T> reqMatch) {
-    Object createdBean = reqMatch.provider().provide(this);
-    for (String matchKey : reqMatch.keys()) {
-      supplied.put(matchKey, createdBean);
-    }
-    return (T) createdBean;
-  }
-
-  @Override
-  public <T> T getNullable(Class<T> type) {
-    return getNullable(type, null);
-  }
-
-  @Override
-  public <T> Optional<T> getOptional(Class<T> type) {
-    return Optional.of(getNullable(type));
-  }
-
-  @Override
-  public <T> Optional<T> getOptional(Class<T> type, String name) {
-    return Optional.of(getNullable(type, name));
-  }
-
-  @Override
-  public <T> T get(Class<T> type) {
-    return get(type, null);
-  }
-
-  @Override
-  public <T> T get(Class<T> type, String name) {
-    T maybe = getNullable(type, name);
-    if (maybe == null) {
-      throw new IllegalStateException("Missing bean for type:" + type + " name:" + name);
-    }
-    return maybe;
-  }
-
-  @Override
-  public <T> Provider<T> getProvider(Class<T> type) {
-    return getProvider(type, null);
-  }
-
-  @Override
-  public <T> Provider<T> getProvider(Class<T> type, String name) {
-    // in request scope everything is already wired, there is no delay
-    return () -> get(type, name);
-  }
-
-  @Override
-  public <T> List<T> list(Class<T> interfaceType) {
-    return beanScope.list(interfaceType);
-  }
-
-  @Override
-  public <T> Set<T> set(Class<T> interfaceType) {
-    return new LinkedHashSet<>(beanScope.list(interfaceType));
-  }
-
-  @Override
-  public void addClosable(Closeable closeable) {
-    closeables.add(closeable);
-  }
-
-  @Override
-  public void close() {
-    for (Closeable closeable : closeables) {
-      try {
-        closeable.close();
-      } catch (Exception e) {
-        log.error("Error closing request scoped bean", e);
-      }
-    }
-  }
-}
diff --git a/inject/src/main/java/io/avaje/inject/spi/DRequestScopeBuilder.java b/inject/src/main/java/io/avaje/inject/spi/DRequestScopeBuilder.java
deleted file mode 100644
index 0250df71..00000000
--- a/inject/src/main/java/io/avaje/inject/spi/DRequestScopeBuilder.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package io.avaje.inject.spi;
-
-import io.avaje.inject.BeanScope;
-import io.avaje.inject.RequestScope;
-import io.avaje.inject.RequestScopeBuilder;
-
-import java.util.concurrent.ConcurrentHashMap;
-
-import static io.avaje.inject.spi.KeyUtil.key;
-
-/**
- * Builder for request scope.
- */
-class DRequestScopeBuilder implements RequestScopeBuilder {
-
-  private final ConcurrentHashMap<String, Object> supplied = new ConcurrentHashMap<>();
-
-  private final BeanScope beanScope;
-
-  DRequestScopeBuilder(BeanScope beanScope) {
-    this.beanScope = beanScope;
-  }
-
-  @Override
-  public <D> RequestScopeBuilder withBean(Class<D> type, D bean) {
-    return withBean(null, type, bean);
-  }
-
-  @Override
-  public <D> RequestScopeBuilder withBean(String name, Class<D> type, D bean) {
-    supplied.put(key(type, name), bean);
-    return this;
-  }
-
-  @Override
-  public RequestScope build() {
-    return new DRequestScope(beanScope, supplied);
-  }
-}
diff --git a/inject/src/main/java/io/avaje/inject/spi/DRequestScopeMatch.java b/inject/src/main/java/io/avaje/inject/spi/DRequestScopeMatch.java
deleted file mode 100644
index 813e8339..00000000
--- a/inject/src/main/java/io/avaje/inject/spi/DRequestScopeMatch.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package io.avaje.inject.spi;
-
-import io.avaje.inject.RequestScopeMatch;
-import io.avaje.inject.RequestScopeProvider;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import static io.avaje.inject.spi.KeyUtil.key;
-
-class DRequestScopeMatch<T> implements RequestScopeMatch<T> {
-
-  private final List<String> keys;
-  private final RequestScopeProvider<T> provider;
-
-  static <T> DRequestScopeMatch<T> of(RequestScopeProvider<T> provider, Class<T> type, String name, Class<?>[] types) {
-    if (name == null || types == null) {
-      return new DRequestScopeMatch<>(key(type, null), provider);
-    }
-    List<String> keys = new ArrayList<>(types.length + 1);
-    keys.add(key(type, null));
-    // add the alternate keys that the provider also matches to
-    for (Class<?> aClass : types) {
-      keys.add(key(aClass, name));
-    }
-    return new DRequestScopeMatch<>(keys, provider);
-  }
-
-  private DRequestScopeMatch(List<String> keys, RequestScopeProvider<T> provider) {
-    this.keys = keys;
-    this.provider = provider;
-  }
-
-  private DRequestScopeMatch(String key, RequestScopeProvider<T> provider) {
-    this.keys = Collections.singletonList(key);
-    this.provider = provider;
-  }
-
-  @Override
-  public List<String> keys() {
-    return keys;
-  }
-
-  @Override
-  public RequestScopeProvider<T> provider() {
-    return provider;
-  }
-}
diff --git a/inject/src/main/java/io/avaje/inject/spi/Module.java b/inject/src/main/java/io/avaje/inject/spi/Module.java
new file mode 100644
index 00000000..2cdf07c6
--- /dev/null
+++ b/inject/src/main/java/io/avaje/inject/spi/Module.java
@@ -0,0 +1,29 @@
+package io.avaje.inject.spi;
+
+/**
+ * A Module that can be included in BeanScope.
+ */
+public interface Module {
+
+  /**
+   * Return the types this module needs to be provided externally or via other modules.
+   */
+  Class<?>[] requires();
+
+  /**
+   * Return the set of types this module explicitly provides to other modules.
+   */
+  Class<?>[] provides();
+
+  /**
+   * Build all the beans.
+   */
+  void build(Builder builder);
+
+  /**
+   * Marker for custom scoped modules.
+   */
+  interface Custom extends Module {
+
+  }
+}
diff --git a/pom.xml b/pom.xml
index 51299265..2c371f15 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
 
   <groupId>io.avaje</groupId>
   <artifactId>avaje-inject-parent</artifactId>
-  <version>6.2</version>
+  <version>6.5-RC0</version>
   <packaging>pom</packaging>
   <name>avaje inject parent</name>
   <description>parent pom for avaje inject library</description>


Test patch:
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/ScopeUtilTest.java b/inject-generator/src/test/java/io/avaje/inject/generator/ScopeUtilTest.java
new file mode 100644
index 00000000..7d933108
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/ScopeUtilTest.java
@@ -0,0 +1,36 @@
+package io.avaje.inject.generator;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ScopeUtilTest {
+
+  @Test
+  void name() {
+    assertEquals("Example", ScopeUtil.name("org.example"));
+    assertEquals("Example", ScopeUtil.name("org.Example"));
+    assertEquals("Example", ScopeUtil.name("example"));
+    assertEquals("Example", ScopeUtil.name("Example"));
+  }
+
+  @Test
+  void name_withSpace() {
+    assertEquals("ExAmple", ScopeUtil.name("org.ex ample"));
+    assertEquals("ExAmple", ScopeUtil.name("org.ex_ample"));
+    assertEquals("ExAmple", ScopeUtil.name("ex-ample"));
+    assertEquals("Example", ScopeUtil.name("ex$ample"));
+  }
+
+  @Test
+  void name_withDigits() {
+    assertEquals("Example1", ScopeUtil.name("org.example1"));
+    assertEquals("Ex42ample", ScopeUtil.name("org.ex42ample"));
+  }
+
+  @Test
+  void name_withSuffix() {
+    assertEquals("MyCustom", ScopeUtil.name("MyCustomScope"));
+    assertEquals("MyCustom", ScopeUtil.name("MyCustomModule"));
+  }
+}
diff --git a/inject-test/pom.xml b/inject-test/pom.xml
index a3940e9b..f8dcf598 100644
--- a/inject-test/pom.xml
+++ b/inject-test/pom.xml
@@ -4,7 +4,7 @@
   <parent>
     <groupId>io.avaje</groupId>
     <artifactId>avaje-inject-parent</artifactId>
-    <version>6.2</version>
+    <version>6.5-RC0</version>
   </parent>
 
   <artifactId>avaje-inject-test</artifactId>
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 d0ac033a..4180fd50 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
@@ -108,11 +108,12 @@ class MetaReader {
   }
 
   void build(BeanScopeBuilder builder) {
+    final BeanScopeBuilder.ForTesting forTesting = builder.forTesting();
     for (FieldTarget target : mocks) {
-      builder.withMock(target.type(), target.name());
+      forTesting.withMock(target.type(), target.name());
     }
     for (FieldTarget target : spies) {
-      builder.withSpy(target.type(), target.name());
+      forTesting.withSpy(target.type(), target.name());
     }
   }
 
diff --git a/inject-test/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java b/inject-test/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
deleted file mode 100644
index be0bf952..00000000
--- a/inject-test/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-package io.avaje.inject;
-
-import io.avaje.inject.spi.BeanScopeFactory;
-import io.avaje.inject.spi.Builder;
-import org.junit.jupiter.api.Test;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class BeanScopeBuilderTest {
-
-  @Test
-  public void noDepends() {
-
-    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true);
-    factoryOrder.add(bc("1", null, null));
-    factoryOrder.add(bc("2", null, null));
-    factoryOrder.add(bc("3", null, null));
-    factoryOrder.orderFactories();
-
-    assertThat(names(factoryOrder.factories())).containsExactly("1", "2", "3");
-  }
-
-  @Test
-  public void name_depends() {
-
-    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true);
-    factoryOrder.add(bc("two", null, "one"));
-    factoryOrder.add(bc("one", null, null));
-    factoryOrder.orderFactories();
-
-    assertThat(names(factoryOrder.factories())).containsExactly("one", "two");
-  }
-
-  @Test
-  public void name_depends4() {
-
-    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true);
-    factoryOrder.add(bc("1", null, "3"));
-    factoryOrder.add(bc("2", null, "4"));
-    factoryOrder.add(bc("3", null, "4"));
-    factoryOrder.add(bc("4", null, null));
-
-    factoryOrder.orderFactories();
-
-    assertThat(names(factoryOrder.factories())).containsExactly("4", "2", "3", "1");
-  }
-
-  @Test
-  public void nameFeature_depends() {
-
-    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true);
-    factoryOrder.add(bc("1", "a", "3"));
-    factoryOrder.add(bc("2", null, "4,a"));
-    factoryOrder.add(bc("3", null, "4"));
-    factoryOrder.add(bc("4", null, null));
-
-    factoryOrder.orderFactories();
-
-    assertThat(names(factoryOrder.factories())).containsExactly("4", "3", "1", "2");
-  }
-
-  @Test
-  public void feature_depends() {
-
-    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true);
-    factoryOrder.add(bc("two", null, "myfeature"));
-    factoryOrder.add(bc("one", "myfeature", null));
-    factoryOrder.orderFactories();
-
-    assertThat(names(factoryOrder.factories())).containsExactly("one", "two");
-  }
-
-  @Test
-  public void feature_depends2() {
-
-    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true);
-    factoryOrder.add(bc("two", null, "myfeature"));
-    factoryOrder.add(bc("one", "myfeature", null));
-    factoryOrder.add(bc("three", "myfeature", null));
-    factoryOrder.orderFactories();
-
-    assertThat(names(factoryOrder.factories())).containsExactly("one", "three", "two");
-  }
-
-  private List<String> names(List<BeanScopeFactory> factories) {
-    return factories.stream()
-      .map(BeanScopeFactory::getName)
-      .collect(Collectors.toList());
-  }
-
-  private TDBeanScope bc(String name, String provides, String dependsOn) {
-    return new TDBeanScope(name, split(provides), split(dependsOn));
-  }
-
-  private String[] split(String val) {
-    return val == null ? null : val.split(",");
-  }
-
-  private static class TDBeanScope implements BeanScopeFactory {
-
-    final String name;
-    final String[] provides;
-    final String[] dependsOn;
-
-    private TDBeanScope(String name, String[] provides, String[] dependsOn) {
-      this.name = name;
-      this.provides = provides;
-      this.dependsOn = dependsOn;
-    }
-
-    @Override
-    public String getName() {
-      return name;
-    }
-
-    @Override
-    public String[] getProvides() {
-      return provides;
-    }
-
-    @Override
-    public String[] getDependsOn() {
-      return dependsOn;
-    }
-
-    @Override
-    public void build(Builder parent) {
-
-    }
-  }
-}
diff --git a/inject-test/src/test/java/io/avaje/inject/SystemContextTest.java b/inject-test/src/test/java/io/avaje/inject/SystemContextTest.java
index 912870c9..57b6cdad 100644
--- a/inject-test/src/test/java/io/avaje/inject/SystemContextTest.java
+++ b/inject-test/src/test/java/io/avaje/inject/SystemContextTest.java
@@ -20,38 +20,42 @@ public class SystemContextTest {
 
   @Test
   public void getBeansByPriority() {
-
-    final List<BaseIface> beans = SystemContext.getBeansByPriority(BaseIface.class);
-    assertThat(beans).hasSize(3);
-
-    assertThat(beans.get(0)).isInstanceOf(CBasei.class);
-    assertThat(beans.get(1)).isInstanceOf(BBasei.class);
-    assertThat(beans.get(2)).isInstanceOf(ABasei.class);
+    try (BeanScope context = BeanScope.newBuilder().build()) {
+      final List<BaseIface> beans = context.listByPriority(BaseIface.class);
+      assertThat(beans).hasSize(3);
+
+      assertThat(beans.get(0)).isInstanceOf(CBasei.class);
+      assertThat(beans.get(1)).isInstanceOf(BBasei.class);
+      assertThat(beans.get(2)).isInstanceOf(ABasei.class);
+    }
   }
 
   @Test
   public void getBeansByPriority_withAnnotation() {
-
-    final List<Somei> beans = ApplicationScope.scope().getBeansByPriority(Somei.class, Priority.class);
-    assertThat(beans).hasSize(3);
-
-    assertThat(beans.get(0)).isInstanceOf(BSomei.class);
-    assertThat(beans.get(1)).isInstanceOf(ASomei.class);
-    assertThat(beans.get(2)).isInstanceOf(A2Somei.class);
+    try (BeanScope context = BeanScope.newBuilder().build()) {
+      final List<Somei> beans = context.listByPriority(Somei.class, Priority.class);
+      assertThat(beans).hasSize(3);
+
+      assertThat(beans.get(0)).isInstanceOf(BSomei.class);
+      assertThat(beans.get(1)).isInstanceOf(ASomei.class);
+      assertThat(beans.get(2)).isInstanceOf(A2Somei.class);
+    }
   }
 
   @Test
   public void getBeansUnsorted_withPriority() {
-
-    final List<Somei> beans = ApplicationScope.list(Somei.class);
-    assertThat(beans).hasSize(3);
-    // can't assert bean order
+    try (BeanScope context = BeanScope.newBuilder().build()) {
+      final List<Somei> beans = context.list(Somei.class);
+      assertThat(beans).hasSize(3);
+      // can't assert bean order
+    }
   }
 
   @Test
   public void getBeansWithAnnotation() {
-
-    final List<Object> beans = SystemContext.getBeansWithAnnotation(Fruit.class);
-    assertThat(beans).hasSize(2);
+    try (BeanScope context = BeanScope.newBuilder().build()) {
+      final List<Object> beans = context.listByAnnotation(Fruit.class);
+      assertThat(beans).hasSize(2);
+    }
   }
 }
diff --git a/inject-test/src/test/java/org/example/MyCustomScope.java b/inject-test/src/test/java/org/example/MyCustomScope.java
new file mode 100644
index 00000000..3edfb9ef
--- /dev/null
+++ b/inject-test/src/test/java/org/example/MyCustomScope.java
@@ -0,0 +1,10 @@
+package org.example;
+
+import io.avaje.inject.InjectModule;
+import jakarta.inject.Scope;
+import org.example.custom.LocalExternal;
+
+@Scope
+@InjectModule(requires = {System.class, LocalExternal.class})
+public @interface MyCustomScope {
+}
diff --git a/inject-test/src/test/java/org/example/coffee/BeanScopeBuilderAddTest.java b/inject-test/src/test/java/org/example/coffee/BeanScopeBuilderAddTest.java
index 80e4ca50..0de15b39 100644
--- a/inject-test/src/test/java/org/example/coffee/BeanScopeBuilderAddTest.java
+++ b/inject-test/src/test/java/org/example/coffee/BeanScopeBuilderAddTest.java
@@ -1,34 +1,48 @@
 package org.example.coffee;
 
 import io.avaje.inject.BeanScope;
+import io.avaje.inject.spi.Builder;
+import io.avaje.inject.spi.Module;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.verify;
 
 public class BeanScopeBuilderAddTest {
 
   @Test
-  public void withModules_exludingThisOne() {
-    assertThrows(IllegalStateException.class, () -> {
-      TDPump testDoublePump = new TDPump();
-
-      try (BeanScope context = BeanScope.newBuilder()
-        .withBeans(testDoublePump)
-        // our module is "org.example.coffee"
-        // so this effectively includes no modules
-        .withModules("other")
-        .withIgnoreMissingModuleDependencies()
-        .build()) {
-
-        CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
-        assertThat(coffeeMaker).isNull();
-      }
-    });
+  public void withModules_excludingThisOne() {
+    TDPump testDoublePump = new TDPump();
+    try (BeanScope context = BeanScope.newBuilder()
+      .withBeans(testDoublePump)
+      // our module is "org.example.coffee"
+      // so this effectively includes no modules
+      .withModules(new SillyModule())
+      .build()) {
+
+      CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
+      assertThat(coffeeMaker).isNull();
+    }
   }
 
+  static class SillyModule implements Module {
+
+    @Override
+    public Class<?>[] requires() {
+      return new Class[0];
+    }
+
+    @Override
+    public Class<?>[] provides() {
+      return new Class[0];
+    }
+
+    @Override
+    public void build(Builder builder) {
+      // do nothing
+    }
+  }
 
   @Test
   public void withModules_includeThisOne() {
@@ -37,10 +51,10 @@ public class BeanScopeBuilderAddTest {
 
     try (BeanScope context = BeanScope.newBuilder()
       .withBeans(testDoublePump)
-      .withModules("org.example")
+      .withModules(new org.example.ExampleModule())
       .build()) {
 
-      String makeIt = context.getBean(CoffeeMaker.class).makeIt();
+      String makeIt = context.get(CoffeeMaker.class).makeIt();
       assertThat(makeIt).isEqualTo("done");
 
       assertThat(testDoublePump.steam).isEqualTo(1);
@@ -57,7 +71,7 @@ public class BeanScopeBuilderAddTest {
       .withBeans(testDoublePump)
       .build()) {
 
-      String makeIt = context.getBean(CoffeeMaker.class).makeIt();
+      String makeIt = context.get(CoffeeMaker.class).makeIt();
       assertThat(makeIt).isEqualTo("done");
 
       assertThat(testDoublePump.steam).isEqualTo(1);
@@ -74,10 +88,10 @@ public class BeanScopeBuilderAddTest {
       .withBean(Pump.class, mock)
       .build()) {
 
-      Pump pump = context.getBean(Pump.class);
+      Pump pump = context.get(Pump.class);
       assertThat(pump).isSameAs(mock);
 
-      CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
+      CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
       assertThat(coffeeMaker).isNotNull();
       coffeeMaker.makeIt();
 
diff --git a/inject-test/src/test/java/org/example/coffee/BeanScope_Builder_mockitoSpyTest.java b/inject-test/src/test/java/org/example/coffee/BeanScope_Builder_mockitoSpyTest.java
index fcdd5bfc..8f45dd54 100644
--- a/inject-test/src/test/java/org/example/coffee/BeanScope_Builder_mockitoSpyTest.java
+++ b/inject-test/src/test/java/org/example/coffee/BeanScope_Builder_mockitoSpyTest.java
@@ -30,11 +30,11 @@ public class BeanScope_Builder_mockitoSpyTest {
       .withBeans(pump, grinder)
       .build()) {
 
-      CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
+      CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
       coffeeMaker.makeIt();
 
-      Pump pump1 = context.getBean(Pump.class);
-      Grinder grinder1 = context.getBean(Grinder.class);
+      Pump pump1 = context.get(Pump.class);
+      Grinder grinder1 = context.get(Grinder.class);
 
       assertThat(pump1).isSameAs(pump);
       assertThat(grinder1).isSameAs(grinder);
@@ -48,14 +48,15 @@ public class BeanScope_Builder_mockitoSpyTest {
   public void withMockitoSpy_noSetup_expect_spyUsed() {
 
     try (BeanScope context = BeanScope.newBuilder()
+      .forTesting()
       .withSpy(Pump.class)
       .build()) {
 
-      CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
+      CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
       assertThat(coffeeMaker).isNotNull();
       coffeeMaker.makeIt();
 
-      Pump pump = context.getBean(Pump.class);
+      Pump pump = context.get(Pump.class);
       verify(pump).pumpWater();
     }
   }
@@ -64,21 +65,22 @@ public class BeanScope_Builder_mockitoSpyTest {
   public void withMockitoSpy_postLoadSetup_expect_spyUsed() {
 
     try (BeanScope context = BeanScope.newBuilder()
+      .forTesting()
       .withSpy(Pump.class)
       .withSpy(Grinder.class)
       .build()) {
 
       // setup after load()
-      Pump pump = context.getBean(Pump.class);
+      Pump pump = context.get(Pump.class);
       doNothing().when(pump).pumpWater();
 
-      CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
+      CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
       assertThat(coffeeMaker).isNotNull();
       coffeeMaker.makeIt();
 
       verify(pump).pumpWater();
 
-      Grinder grinder = context.getBean(Grinder.class);
+      Grinder grinder = context.get(Grinder.class);
       verify(grinder).grindBeans();
     }
   }
@@ -87,6 +89,7 @@ public class BeanScope_Builder_mockitoSpyTest {
   public void withMockitoSpy_expect_spyUsed() {
 
     try (BeanScope context = BeanScope.newBuilder()
+      .forTesting()
       .withSpy(Pump.class, pump -> {
         // setup the spy
         doNothing().when(pump).pumpWater();
@@ -94,11 +97,11 @@ public class BeanScope_Builder_mockitoSpyTest {
       .build()) {
 
       // or setup here ...
-      Pump pump = context.getBean(Pump.class);
+      Pump pump = context.get(Pump.class);
       doNothing().when(pump).pumpSteam();
 
       // act
-      CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
+      CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
       coffeeMaker.makeIt();
 
       verify(pump).pumpWater();
@@ -110,11 +113,12 @@ public class BeanScope_Builder_mockitoSpyTest {
   public void withMockitoSpy_whenPrimary_expect_spyUsed() {
 
     try (BeanScope context = BeanScope.newBuilder()
+      .forTesting()
       .withSpy(PEmailer.class) // has a primary
       .build()) {
 
-      UserOfPEmailer user = context.getBean(UserOfPEmailer.class);
-      PEmailer emailer = context.getBean(PEmailer.class);
+      UserOfPEmailer user = context.get(UserOfPEmailer.class);
+      PEmailer emailer = context.get(PEmailer.class);
 
       user.email();
       verify(emailer).email();
@@ -125,18 +129,19 @@ public class BeanScope_Builder_mockitoSpyTest {
   public void withMockitoSpy_whenOnlySecondary_expect_spyUsed() {
 
     try (BeanScope context = BeanScope.newBuilder()
+      .forTesting()
       .withSpy(Widget.class) // only secondary
       .build()) {
 
-      WidgetUser widgetUser = context.getBean(WidgetUser.class);
+      WidgetUser widgetUser = context.get(WidgetUser.class);
 
       String val = widgetUser.wid();
       assertThat(val).isEqualTo("second");
-      Widget widget = context.getBean(Widget.class);
+      Widget widget = context.get(Widget.class);
       verify(widget).wid();
 
       // these are the same (secondary only)
-      WidgetSecondary widgetSecondary = context.getBean(WidgetSecondary.class);
+      WidgetSecondary widgetSecondary = context.get(WidgetSecondary.class);
       assertThat(widget).isSameAs(widgetSecondary);
     }
   }
@@ -145,20 +150,21 @@ public class BeanScope_Builder_mockitoSpyTest {
   public void withMockitoSpy_whenSecondary_expect_spyUsed() {
 
     try (BeanScope context = BeanScope.newBuilder()
+      .forTesting()
       .withSpy(Something.class) // has a secondary and a normal
       .build()) {
 
-      Unused unused = context.getBean(Unused.class);
-      Something something = context.getBean(Something.class);
+      Unused unused = context.get(Unused.class);
+      Something something = context.get(Something.class);
       String result = unused.doSomething();
       verify(something).doStuff();
 
       // someImpl has higher precedence than the Secondary
       assertThat(result).isEqualTo("SomeImpl");
 
-      SomeImpl someImpl = context.getBean(SomeImpl.class);
+      SomeImpl someImpl = context.get(SomeImpl.class);
       assertThat(someImpl).isNull();
-      SomeImplBean someImplBean = context.getBean(SomeImplBean.class);
+      SomeImplBean someImplBean = context.get(SomeImplBean.class);
       assertThat(something).isNotSameAs(someImplBean);
     }
   }
@@ -169,6 +175,7 @@ public class BeanScope_Builder_mockitoSpyTest {
     AtomicReference<Grinder> mock = new AtomicReference<>();
 
     try (BeanScope context = BeanScope.newBuilder()
+      .forTesting()
       .withMock(Pump.class)
       .withMock(Grinder.class, grinder -> {
         // setup the mock
@@ -177,10 +184,10 @@ public class BeanScope_Builder_mockitoSpyTest {
       })
       .build()) {
 
-      Grinder grinder = context.getBean(Grinder.class);
+      Grinder grinder = context.get(Grinder.class);
       assertThat(grinder).isSameAs(mock.get());
 
-      CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
+      CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
       assertThat(coffeeMaker).isNotNull();
       coffeeMaker.makeIt();
 
diff --git a/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java b/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
index 329520d8..86006527 100644
--- a/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
+++ b/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
@@ -1,46 +1,67 @@
 package org.example.coffee;
 
-import io.avaje.inject.ApplicationScope;
+import io.avaje.inject.BeanEntry;
 import io.avaje.inject.BeanScope;
-import io.avaje.inject.SystemContext;
 import org.example.coffee.core.DuperPump;
+import org.example.coffee.list.BSomei;
+import org.example.coffee.list.Somei;
 import org.junit.jupiter.api.Test;
 
+import java.util.List;
+import java.util.Optional;
+
+import static java.util.stream.Collectors.toList;
 import static org.assertj.core.api.Assertions.assertThat;
 
 public class CoffeeMakerTest {
 
   @Test
   public void makeIt_via_SystemContext() {
+    try (BeanScope context = BeanScope.newBuilder().build()) {
+      String makeIt = context.get(CoffeeMaker.class).makeIt();
+      assertThat(makeIt).isEqualTo("done");
 
-    String makeIt = ApplicationScope.get(CoffeeMaker.class).makeIt();
-    assertThat(makeIt).isEqualTo("done");
-
-    Pump pump = ApplicationScope.get(Pump.class);
-    assertThat(pump).isInstanceOf(DuperPump.class);
-
-    Pump pump2 = SystemContext.context().getBean(Pump.class);
-    assertThat(pump2).isSameAs(pump);
+      Pump pump = context.get(Pump.class);
+      assertThat(pump).isInstanceOf(DuperPump.class);
+    }
   }
 
   @Test
   public void makeIt_via_BootContext_withNoShutdownHook() {
-
     try (BeanScope context = BeanScope.newBuilder()
-      .withNoShutdownHook()
+      .withShutdownHook(false)
       .build()) {
 
-      String makeIt = context.getBean(CoffeeMaker.class).makeIt();
+      String makeIt = context.get(CoffeeMaker.class).makeIt();
       assertThat(makeIt).isEqualTo("done");
     }
   }
 
   @Test
   public void makeIt_via_BootContext() {
-
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      String makeIt = context.getBean(CoffeeMaker.class).makeIt();
+      String makeIt = context.get(CoffeeMaker.class).makeIt();
       assertThat(makeIt).isEqualTo("done");
+
+      final List<BeanEntry> beanEntries = context.all();
+      assertThat(beanEntries).hasSizeGreaterThan(10);
+
+      // all entries for an interface
+      final List<BeanEntry> someiEntries = beanEntries.stream()
+        .filter(beanEntry -> beanEntry.hasKey(Somei.class))
+        .collect(toList());
+      assertThat(someiEntries).hasSize(3);
+
+      final Optional<BeanEntry> bsomeEntry = someiEntries.stream().filter(entry -> entry.type().equals(BSomei.class)).findFirst();
+      assertThat(bsomeEntry).isPresent();
+
+      final BeanEntry entry = bsomeEntry.get();
+      assertThat(entry.qualifierName()).isEqualTo("b");
+      assertThat(entry.keys()).containsExactly(BSomei.class.getCanonicalName(), Somei.class.getCanonicalName());
+      assertThat(entry.type()).isEqualTo(BSomei.class);
+      assertThat(entry.priority()).isEqualTo(0);
+      assertThat(entry.bean()).isEqualTo(context.get(Somei.class, "b"));
+      assertThat(entry.bean()).isEqualTo(context.get(BSomei.class));
     }
   }
 
diff --git a/inject-test/src/test/java/org/example/coffee/ExtensionExample.java b/inject-test/src/test/java/org/example/coffee/ExtensionExample.java
index 76f80415..15177a91 100644
--- a/inject-test/src/test/java/org/example/coffee/ExtensionExample.java
+++ b/inject-test/src/test/java/org/example/coffee/ExtensionExample.java
@@ -16,7 +16,7 @@ class ExtensionExample {
   }
 
   BeanScope build() {
-    BeanScopeBuilder bootContext = BeanScope.newBuilder();
+    BeanScopeBuilder.ForTesting bootContext = BeanScope.newBuilder().forTesting();
     withMocks.forEach(bootContext::withMock);
     withSpies.forEach(bootContext::withSpy);
     return bootContext.build();
diff --git a/inject-test/src/test/java/org/example/coffee/ExtensionExampleTest.java b/inject-test/src/test/java/org/example/coffee/ExtensionExampleTest.java
index c79a0670..8c7a30e2 100644
--- a/inject-test/src/test/java/org/example/coffee/ExtensionExampleTest.java
+++ b/inject-test/src/test/java/org/example/coffee/ExtensionExampleTest.java
@@ -22,6 +22,7 @@ public class ExtensionExampleTest {
     Class<?> cls1 = SEmailer.class;
 
     BeanScopeBuilder bootContext = BeanScope.newBuilder()
+      .forTesting()
       .withSpy(cls0)
       .withSpy(cls1)
       .withMock(cls0)
diff --git a/inject-test/src/test/java/org/example/coffee/FactoryTest.java b/inject-test/src/test/java/org/example/coffee/FactoryTest.java
index 556108e3..59cb0d39 100644
--- a/inject-test/src/test/java/org/example/coffee/FactoryTest.java
+++ b/inject-test/src/test/java/org/example/coffee/FactoryTest.java
@@ -12,7 +12,7 @@ public class FactoryTest {
   public void test() {
 
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      BFact bean = context.getBean(BFact.class);
+      BFact bean = context.get(BFact.class);
       String b = bean.b();
       assertThat(b).isNotNull();
     }
diff --git a/inject-test/src/test/java/org/example/coffee/InjectListTest.java b/inject-test/src/test/java/org/example/coffee/InjectListTest.java
index 27107a93..b6050d48 100644
--- a/inject-test/src/test/java/org/example/coffee/InjectListTest.java
+++ b/inject-test/src/test/java/org/example/coffee/InjectListTest.java
@@ -14,7 +14,7 @@ public class InjectListTest {
   @Test
   public void test() {
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      CombinedSomei bean = context.getBean(CombinedSomei.class);
+      CombinedSomei bean = context.get(CombinedSomei.class);
       List<String> somes = bean.lotsOfSomes();
       assertThat(somes).containsOnly("a", "b", "a2");
     }
@@ -23,7 +23,7 @@ public class InjectListTest {
   @Test
   public void test_set() {
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      CombinedSetSomei bean = context.getBean(CombinedSetSomei.class);
+      CombinedSetSomei bean = context.get(CombinedSetSomei.class);
       List<String> somes = bean.lotsOfSomes();
       assertThat(somes).containsOnly("a", "b", "a2");
     }
diff --git a/inject-test/src/test/java/org/example/coffee/ProviderTest.java b/inject-test/src/test/java/org/example/coffee/ProviderTest.java
index e50a6cbb..911fc3e9 100644
--- a/inject-test/src/test/java/org/example/coffee/ProviderTest.java
+++ b/inject-test/src/test/java/org/example/coffee/ProviderTest.java
@@ -14,11 +14,11 @@ public class ProviderTest {
 
     try (BeanScope context = BeanScope.newBuilder().build()) {
 
-      ProvOther bean = context.getBean(ProvOther.class);
+      ProvOther bean = context.get(ProvOther.class);
       String other = bean.other();
       assertThat(other).isEqualTo("mush mush beans");
 
-      ProvOther2 bean2 = context.getBean(ProvOther2.class);
+      ProvOther2 bean2 = context.get(ProvOther2.class);
       assertThat(bean2.getaProv()).isSameAs(bean.getaProv());
     }
 
diff --git a/inject-test/src/test/java/org/example/coffee/circular/CircularDependencyTest.java b/inject-test/src/test/java/org/example/coffee/circular/CircularDependencyTest.java
index 913cb19b..93f83231 100644
--- a/inject-test/src/test/java/org/example/coffee/circular/CircularDependencyTest.java
+++ b/inject-test/src/test/java/org/example/coffee/circular/CircularDependencyTest.java
@@ -10,13 +10,13 @@ class CircularDependencyTest {
   @Test
   void wire() {
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      assertThat(context.getBean(CircA.class)).isNotNull();
-      assertThat(context.getBean(CircB.class)).isNotNull();
-      assertThat(context.getBean(CircC.class)).isNotNull();
+      assertThat(context.get(CircA.class)).isNotNull();
+      assertThat(context.get(CircB.class)).isNotNull();
+      assertThat(context.get(CircC.class)).isNotNull();
 
-      final CircA circA = context.getBean(CircA.class);
-      final CircB circB = context.getBean(CircB.class);
-      final CircC circC = context.getBean(CircC.class);
+      final CircA circA = context.get(CircA.class);
+      final CircB circB = context.get(CircB.class);
+      final CircC circC = context.get(CircC.class);
       assertThat(circC.gen()).isEqualTo("C+CircA-CircB-CircC-Stop");
 
       assertThat(circC.circA).isSameAs(circA);
diff --git a/inject-test/src/test/java/org/example/coffee/factory/BFactTest.java b/inject-test/src/test/java/org/example/coffee/factory/BFactTest.java
index 9a4d5071..558dc138 100644
--- a/inject-test/src/test/java/org/example/coffee/factory/BFactTest.java
+++ b/inject-test/src/test/java/org/example/coffee/factory/BFactTest.java
@@ -12,7 +12,7 @@ public class BFactTest {
 
     BFact bFact;
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      bFact = context.getBean(BFact.class);
+      bFact = context.get(BFact.class);
       assertThat(bFact.getCountInit()).isEqualTo(1);
       assertThat(bFact.getCountClose()).isEqualTo(0);
     }
diff --git a/inject-test/src/test/java/org/example/coffee/factory/ConfigurationTest.java b/inject-test/src/test/java/org/example/coffee/factory/ConfigurationTest.java
index d1e5d809..ba8792f0 100644
--- a/inject-test/src/test/java/org/example/coffee/factory/ConfigurationTest.java
+++ b/inject-test/src/test/java/org/example/coffee/factory/ConfigurationTest.java
@@ -12,7 +12,7 @@ public class ConfigurationTest {
 
     Configuration configuration;
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      configuration = context.getBean(Configuration.class);
+      configuration = context.get(Configuration.class);
       assertEquals(configuration.getCountInit(), 1);
       assertEquals(configuration.getCountClose(), 0);
     }
diff --git a/inject-test/src/test/java/org/example/coffee/factory/MultipleOtherThingsTest.java b/inject-test/src/test/java/org/example/coffee/factory/MultipleOtherThingsTest.java
index f39a8274..e1b8fd4d 100644
--- a/inject-test/src/test/java/org/example/coffee/factory/MultipleOtherThingsTest.java
+++ b/inject-test/src/test/java/org/example/coffee/factory/MultipleOtherThingsTest.java
@@ -12,7 +12,7 @@ class MultipleOtherThingsTest {
   @Test
   void test() {
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      final MultipleOtherThings combined = context.getBean(MultipleOtherThings.class);
+      final MultipleOtherThings combined = context.get(MultipleOtherThings.class);
       assertEquals("blue", combined.blue());
       assertEquals("red", combined.red());
       assertEquals("green", combined.green());
diff --git a/inject-test/src/test/java/org/example/coffee/factory/MyFactoryTest.java b/inject-test/src/test/java/org/example/coffee/factory/MyFactoryTest.java
index 7b0a7991..7353efb8 100644
--- a/inject-test/src/test/java/org/example/coffee/factory/MyFactoryTest.java
+++ b/inject-test/src/test/java/org/example/coffee/factory/MyFactoryTest.java
@@ -12,7 +12,7 @@ public class MyFactoryTest {
   @Test
   public void methodsCalled() {
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      final MyFactory myFactory = context.getBean(MyFactory.class);
+      final MyFactory myFactory = context.get(MyFactory.class);
       assertThat(myFactory.methodsCalled()).contains("|useCFact", "|anotherCFact", "|buildEngi");
     }
   }
diff --git a/inject-test/src/test/java/org/example/coffee/fruit/AppleServiceTest.java b/inject-test/src/test/java/org/example/coffee/fruit/AppleServiceTest.java
index b461ee79..1b646ea9 100644
--- a/inject-test/src/test/java/org/example/coffee/fruit/AppleServiceTest.java
+++ b/inject-test/src/test/java/org/example/coffee/fruit/AppleServiceTest.java
@@ -14,11 +14,11 @@ public class AppleServiceTest {
   public void test_spyWithFieldInjection() {
 
     BeanScopeBuilder contextBuilder = BeanScope.newBuilder();
-    contextBuilder.withSpy(AppleService.class);
+    contextBuilder.forTesting().withSpy(AppleService.class);
 
     try (BeanScope beanScope = contextBuilder.build()) {
 
-      AppleService appleService = beanScope.getBean(AppleService.class);
+      AppleService appleService = beanScope.get(AppleService.class);
 
       doNothing()
         .when(appleService)
@@ -39,7 +39,7 @@ public class AppleServiceTest {
 
     try (BeanScope beanScope = BeanScope.newBuilder().build()) {
 
-      AppleService appleService = beanScope.getBean(AppleService.class);
+      AppleService appleService = beanScope.get(AppleService.class);
 
       assertThat(appleService.bananaService).isNotNull();
       assertThat(appleService.peachService).isNotNull();
diff --git a/inject-test/src/test/java/org/example/coffee/generic/HazManagerTest.java b/inject-test/src/test/java/org/example/coffee/generic/HazManagerTest.java
index 508245e3..911169cf 100644
--- a/inject-test/src/test/java/org/example/coffee/generic/HazManagerTest.java
+++ b/inject-test/src/test/java/org/example/coffee/generic/HazManagerTest.java
@@ -23,10 +23,11 @@ public class HazManagerTest {
   public void fin_with_mockHaz() {
 
     try (BeanScope context = BeanScope.newBuilder()
+      .forTesting()
       .withMock(HazRepo.class)
       .build()) {
 
-      HazManager hazManager = context.getBean(HazManager.class);
+      HazManager hazManager = context.get(HazManager.class);
       Haz haz = hazManager.find(42L);
 
       assertThat(haz).isNull();
@@ -37,12 +38,13 @@ public class HazManagerTest {
   public void find_with_stubHazUsingMockito() {
 
     try (BeanScope context = BeanScope.newBuilder()
+      .forTesting()
       .withMock(HazRepo.class, hazRepo -> {
         when(hazRepo.findById(anyLong())).thenReturn(new Haz(-23L));
       })
       .build()) {
 
-      HazManager hazManager = context.getBean(HazManager.class);
+      HazManager hazManager = context.get(HazManager.class);
       Haz haz = hazManager.find(42L);
 
       assertThat(haz.id).isEqualTo(-23L);
@@ -58,7 +60,7 @@ public class HazManagerTest {
       .withBeans(testDouble)
       .build()) {
 
-      HazManager hazManager = context.getBean(HazManager.class);
+      HazManager hazManager = context.get(HazManager.class);
 
       Haz haz = hazManager.find(42L);
       assertThat(haz.id).isEqualTo(64L);
diff --git a/inject-test/src/test/java/org/example/coffee/grind/AMusherTest.java b/inject-test/src/test/java/org/example/coffee/grind/AMusherTest.java
index 9295d5c8..9e334207 100644
--- a/inject-test/src/test/java/org/example/coffee/grind/AMusherTest.java
+++ b/inject-test/src/test/java/org/example/coffee/grind/AMusherTest.java
@@ -12,7 +12,7 @@ public class AMusherTest {
 
     AMusher aMusher;
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      aMusher = context.getBean(AMusher.class);
+      aMusher = context.get(AMusher.class);
       assertEquals(aMusher.getCountInit(), 1);
       assertEquals(aMusher.getCountClose(), 0);
     }
diff --git a/inject-test/src/test/java/org/example/coffee/grind/BMusherTest.java b/inject-test/src/test/java/org/example/coffee/grind/BMusherTest.java
index 077c843e..9690b88e 100644
--- a/inject-test/src/test/java/org/example/coffee/grind/BMusherTest.java
+++ b/inject-test/src/test/java/org/example/coffee/grind/BMusherTest.java
@@ -12,7 +12,7 @@ public class BMusherTest {
 
     BMusher bMusher;
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      bMusher = context.getBean(BMusher.class);
+      bMusher = context.get(BMusher.class);
       assertEquals(bMusher.getCountInit(), 1);
       assertEquals(bMusher.getCountClose(), 0);
     }
diff --git a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithFieldQualifierTest.java b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithFieldQualifierTest.java
index b87fb8a6..7fe5b7a5 100644
--- a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithFieldQualifierTest.java
+++ b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithFieldQualifierTest.java
@@ -11,7 +11,7 @@ public class StoreManagerWithFieldQualifierTest {
   public void test() {
 
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      StoreManagerWithFieldQualifier manager = context.getBean(StoreManagerWithFieldQualifier.class);
+      StoreManagerWithFieldQualifier manager = context.get(StoreManagerWithFieldQualifier.class);
       String store = manager.store();
       assertThat(store).isEqualTo("blue");
     }
diff --git a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithNamedTest.java b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithNamedTest.java
index 704bc7fa..1b7c3261 100644
--- a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithNamedTest.java
+++ b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithNamedTest.java
@@ -11,7 +11,7 @@ public class StoreManagerWithNamedTest {
   public void test() {
 
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      StoreManagerWithNamed manager = context.getBean(StoreManagerWithNamed.class);
+      StoreManagerWithNamed manager = context.get(StoreManagerWithNamed.class);
       String store = manager.store();
       assertThat(store).isEqualTo("blue");
     }
diff --git a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithQualifierTest.java b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithQualifierTest.java
index d5fed776..8f18827a 100644
--- a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithQualifierTest.java
+++ b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithQualifierTest.java
@@ -11,7 +11,7 @@ public class StoreManagerWithQualifierTest {
   public void test() {
 
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      StoreManagerWithQualifier manager = context.getBean(StoreManagerWithQualifier.class);
+      StoreManagerWithQualifier manager = context.get(StoreManagerWithQualifier.class);
       String store = manager.store();
       assertThat(store).isEqualTo("blue");
     }
diff --git a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithSetterQualifierTest.java b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithSetterQualifierTest.java
index c5ce60b7..92653847 100644
--- a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithSetterQualifierTest.java
+++ b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithSetterQualifierTest.java
@@ -10,7 +10,7 @@ class StoreManagerWithSetterQualifierTest {
   @Test
   void redStore() {
     try (BeanScope context = BeanScope.newBuilder().build()) {
-      StoreManagerWithSetterQualifier manager = context.getBean(StoreManagerWithSetterQualifier.class);
+      StoreManagerWithSetterQualifier manager = context.get(StoreManagerWithSetterQualifier.class);
       assertThat(manager.blueStore()).isEqualTo("blue");
       assertThat(manager.greenStore()).isEqualTo("green");
     }
@@ -23,7 +23,7 @@ class StoreManagerWithSetterQualifierTest {
       .withBean("Green", SomeStore.class, () -> "TD Green")
       .build()) {
 
-      StoreManagerWithSetterQualifier manager = context.getBean(StoreManagerWithSetterQualifier.class);
+      StoreManagerWithSetterQualifier manager = context.get(StoreManagerWithSetterQualifier.class);
       assertThat(manager.blueStore()).isEqualTo("TD Blue");
       assertThat(manager.greenStore()).isEqualTo("TD Green");
     }
@@ -36,7 +36,7 @@ class StoreManagerWithSetterQualifierTest {
       // with GreenStore still wired
       .build()) {
 
-      StoreManagerWithSetterQualifier manager = context.getBean(StoreManagerWithSetterQualifier.class);
+      StoreManagerWithSetterQualifier manager = context.get(StoreManagerWithSetterQualifier.class);
       assertThat(manager.blueStore()).isEqualTo("TD Blue Only");
       assertThat(manager.greenStore()).isEqualTo("green");
     }
diff --git a/inject-test/src/test/java/org/example/custom/CustomBean.java b/inject-test/src/test/java/org/example/custom/CustomBean.java
new file mode 100644
index 00000000..f02c7da4
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom/CustomBean.java
@@ -0,0 +1,10 @@
+package org.example.custom;
+
+import jakarta.inject.Named;
+import org.example.MyCustomScope;
+
+@Named("Hello")
+@MyCustomScope
+public class CustomBean {
+
+}
diff --git a/inject-test/src/test/java/org/example/custom/CustomScopeTest.java b/inject-test/src/test/java/org/example/custom/CustomScopeTest.java
new file mode 100644
index 00000000..664906e5
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom/CustomScopeTest.java
@@ -0,0 +1,128 @@
+package org.example.custom;
+
+import io.avaje.inject.BeanEntry;
+import io.avaje.inject.BeanScope;
+import org.example.MyCustomScope;
+import org.example.coffee.CoffeeMaker;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.List;
+import java.util.Optional;
+
+import static java.util.stream.Collectors.toList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CustomScopeTest {
+
+  @Test
+  void customScopeWithParent() {
+    try (final BeanScope parentScope = BeanScope.newBuilder().build()) {
+      final CoffeeMaker parentMaker = parentScope.get(CoffeeMaker.class);
+      assertThat(parentMaker).isNotNull();
+
+      LocalExt ext = new LocalExt();
+
+      try (BeanScope beanScope = BeanScope.newBuilder()
+        .withParent(parentScope)
+        .withModules(new MyCustomModule())
+        .withBeans(LocalExternal.class, ext)
+        .build()) {
+
+        final CoffeeMaker coffeeMaker = beanScope.get(CoffeeMaker.class);
+        assertThat(coffeeMaker).isNotNull();
+
+        final CustomBean bean = beanScope.get(CustomBean.class);
+        final OtherCBean cBean = beanScope.get(OtherCBean.class);
+        assertThat(bean).isNotNull();
+        assertThat(cBean.dependency()).isSameAs(bean);
+
+        final LocalExternal externallyProvided = beanScope.get(LocalExternal.class);
+        assertThat(externallyProvided).isSameAs(ext);
+      }
+    }
+  }
+
+  @Test
+  void buildSimpleCustomScope() {
+
+    LocalExt ext = new LocalExt();
+    CoffeeMaker suppliedCoffeeMaker = Mockito.mock(CoffeeMaker.class);
+
+    try (BeanScope beanScope = BeanScope.newBuilder()
+      .withModules(new MyCustomModule())
+      .withBean(LocalExternal.class, ext)
+      .withBean(CoffeeMaker.class, suppliedCoffeeMaker)
+      .build()) {
+
+      final CustomBean bean = beanScope.get(CustomBean.class);
+      final OtherCBean cBean = beanScope.get(OtherCBean.class);
+      assertThat(bean).isNotNull();
+      assertThat(cBean.dependency()).isSameAs(bean);
+
+      final LocalExternal externallyProvided = beanScope.get(LocalExternal.class);
+      assertThat(externallyProvided).isSameAs(ext);
+
+      final CoffeeMaker coffeeMaker = beanScope.get(CoffeeMaker.class);
+      assertThat(coffeeMaker).isSameAs(suppliedCoffeeMaker);
+    }
+  }
+
+  @Test
+  void customScopeAll() {
+
+    LocalExt ext = new LocalExt();
+    CoffeeMaker suppliedCoffeeMaker = Mockito.mock(CoffeeMaker.class);
+
+    try (BeanScope beanScope = BeanScope.newBuilder()
+      .withModules(new MyCustomModule())
+      .withBean(LocalExternal.class, ext)
+      .withBean(CoffeeMaker.class, suppliedCoffeeMaker)
+      .build()) {
+
+      // includes the 2 supplied beans
+      final List<BeanEntry> all = beanScope.all();
+      assertThat(all).hasSize(5);
+
+      final CustomBean customBean = beanScope.get(CustomBean.class);
+
+      final Optional<BeanEntry> customBeanEntry = all.stream()
+        .filter(beanEntry -> beanEntry.hasKey(CustomBean.class))
+        .findFirst();
+
+      assertThat(customBeanEntry).isPresent();
+      assertThat(customBeanEntry.get().bean()).isSameAs(customBean);
+      assertThat(customBeanEntry.get().qualifierName()).isEqualTo("hello");
+      assertThat(customBeanEntry.get().priority()).isEqualTo(0);
+      assertThat(customBeanEntry.get().keys()).containsExactly(CustomBean.class.getCanonicalName(), MyCustomScope.class.getCanonicalName());
+
+
+      final Optional<BeanEntry> fooCustomEntry = all.stream()
+        .filter(beanEntry -> beanEntry.hasKey(FooCustom.class))
+        .findFirst();
+
+      assertThat(fooCustomEntry).isPresent();
+      assertThat(fooCustomEntry.get().qualifierName()).isNull();
+
+      // only the beans with MyCustomScope annotation
+      final List<BeanEntry> myCustomScopeBeans = all.stream()
+        .filter(beanEntry -> beanEntry.hasKey(MyCustomScope.class))
+        .collect(toList());
+      assertThat(myCustomScopeBeans).hasSize(3);
+
+      final OtherCBean cBean = beanScope.get(OtherCBean.class);
+      assertThat(customBean).isNotNull();
+      assertThat(cBean.dependency()).isSameAs(customBean);
+
+      final LocalExternal externallyProvided = beanScope.get(LocalExternal.class);
+      assertThat(externallyProvided).isSameAs(ext);
+
+      final CoffeeMaker coffeeMaker = beanScope.get(CoffeeMaker.class);
+      assertThat(coffeeMaker).isSameAs(suppliedCoffeeMaker);
+    }
+  }
+
+  static class LocalExt implements LocalExternal {
+
+  }
+}
diff --git a/inject-test/src/test/java/org/example/custom/FooCustom.java b/inject-test/src/test/java/org/example/custom/FooCustom.java
new file mode 100644
index 00000000..d8178718
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom/FooCustom.java
@@ -0,0 +1,19 @@
+package org.example.custom;
+
+import org.example.MyCustomScope;
+import org.example.coffee.CoffeeMaker;
+
+@MyCustomScope
+public class FooCustom {
+
+  /**
+   * CoffeeMaker is provided by the "default" scope and that is "good enough" for now (could be better/tighter here).
+   */
+  final CoffeeMaker coffeeMaker;
+  final LocalExternal external;
+
+  public FooCustom(CoffeeMaker coffeeMaker, LocalExternal external) {
+    this.coffeeMaker = coffeeMaker;
+    this.external = external;
+  }
+}
diff --git a/inject-test/src/test/java/org/example/custom/LocalExternal.java b/inject-test/src/test/java/org/example/custom/LocalExternal.java
new file mode 100644
index 00000000..4147b770
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom/LocalExternal.java
@@ -0,0 +1,4 @@
+package org.example.custom;
+
+public interface LocalExternal {
+}
diff --git a/inject-test/src/test/java/org/example/custom/OtherCBean.java b/inject-test/src/test/java/org/example/custom/OtherCBean.java
new file mode 100644
index 00000000..f413313e
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom/OtherCBean.java
@@ -0,0 +1,17 @@
+package org.example.custom;
+
+import org.example.MyCustomScope;
+
+@MyCustomScope
+public class OtherCBean {
+
+  final CustomBean dependency;
+
+  public OtherCBean(CustomBean dependency) {
+    this.dependency = dependency;
+  }
+
+  public CustomBean dependency() {
+    return dependency;
+  }
+}
diff --git a/inject-test/src/test/java/org/example/custom2/OciMarker.java b/inject-test/src/test/java/org/example/custom2/OciMarker.java
new file mode 100644
index 00000000..f2fd8cd0
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom2/OciMarker.java
@@ -0,0 +1,4 @@
+package org.example.custom2;
+
+public @interface OciMarker {
+}
diff --git a/inject-test/src/test/java/org/example/custom2/OciPlant.java b/inject-test/src/test/java/org/example/custom2/OciPlant.java
new file mode 100644
index 00000000..a58d4f80
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom2/OciPlant.java
@@ -0,0 +1,4 @@
+package org.example.custom2;
+
+public interface OciPlant {
+}
diff --git a/inject-test/src/test/java/org/example/custom2/OciRock.java b/inject-test/src/test/java/org/example/custom2/OciRock.java
new file mode 100644
index 00000000..4922d324
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom2/OciRock.java
@@ -0,0 +1,4 @@
+package org.example.custom2;
+
+public interface OciRock {
+}
diff --git a/inject-test/src/test/java/org/example/custom2/OcsOne.java b/inject-test/src/test/java/org/example/custom2/OcsOne.java
new file mode 100644
index 00000000..d8b8ed51
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom2/OcsOne.java
@@ -0,0 +1,6 @@
+package org.example.custom2;
+
+@OtherScope
+public class OcsOne implements OciPlant, OciRock {
+
+}
diff --git a/inject-test/src/test/java/org/example/custom2/OcsThree.java b/inject-test/src/test/java/org/example/custom2/OcsThree.java
new file mode 100644
index 00000000..0f78c1c3
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom2/OcsThree.java
@@ -0,0 +1,5 @@
+package org.example.custom2;
+
+@OtherScope
+public class OcsThree {
+}
diff --git a/inject-test/src/test/java/org/example/custom2/OcsTwo.java b/inject-test/src/test/java/org/example/custom2/OcsTwo.java
new file mode 100644
index 00000000..2b13c647
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom2/OcsTwo.java
@@ -0,0 +1,12 @@
+package org.example.custom2;
+
+@OciMarker
+@OtherScope
+public class OcsTwo implements OciRock {
+
+  final OcsOne one;
+
+  OcsTwo(OcsOne one) {
+    this.one = one;
+  }
+}
diff --git a/inject-test/src/test/java/org/example/custom2/OtherScope.java b/inject-test/src/test/java/org/example/custom2/OtherScope.java
new file mode 100644
index 00000000..23bbde40
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom2/OtherScope.java
@@ -0,0 +1,7 @@
+package org.example.custom2;
+
+import jakarta.inject.Scope;
+
+@Scope
+public @interface OtherScope {
+}
diff --git a/inject-test/src/test/java/org/example/custom3/MyThreeScope.java b/inject-test/src/test/java/org/example/custom3/MyThreeScope.java
new file mode 100644
index 00000000..68be8a21
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom3/MyThreeScope.java
@@ -0,0 +1,13 @@
+package org.example.custom3;
+
+import io.avaje.inject.InjectModule;
+import jakarta.inject.Scope;
+import org.example.custom2.OtherScope;
+
+/**
+ * This scope has a requires on another scope - requires = OtherScope
+ */
+@Scope
+@InjectModule(requires = OtherScope.class)
+public @interface MyThreeScope {
+}
diff --git a/inject-test/src/test/java/org/example/custom3/ParentScopeTest.java b/inject-test/src/test/java/org/example/custom3/ParentScopeTest.java
new file mode 100644
index 00000000..95c6d604
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom3/ParentScopeTest.java
@@ -0,0 +1,47 @@
+package org.example.custom3;
+
+import io.avaje.inject.BeanScope;
+import org.example.custom2.*;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ParentScopeTest {
+
+  @Test
+  void parentScope() {
+
+    final BeanScope parent = BeanScope.newBuilder()
+      .withModules(new OtherModule())
+      .build();
+
+    final BeanScope scope = BeanScope.newBuilder()
+      .withModules(new MyThreeModule())
+      .withParent(parent)
+      .build();
+
+    // factory for custom scope
+    final TcsGreen green = scope.get(TcsGreen.class);
+    assertThat(green).isNotNull();
+
+    final TcsRed red = scope.get(TcsRed.class);
+    final TcsBlue blue = scope.get(TcsBlue.class);
+    final OcsOne ocsOne = scope.get(OcsOne.class);
+    final OcsTwo ocsTwo = scope.get(OcsTwo.class);
+    final OcsThree ocsThree = scope.get(OcsThree.class);
+
+    // combined from both scopes
+    final List<OciRock> allRocks = scope.list(OciRock.class);
+    assertThat(allRocks).containsOnly(red, ocsOne, ocsTwo);
+
+    // combined from both scopes
+    final List<Object> marked = scope.listByAnnotation(OciMarker.class);
+    assertThat(marked).containsOnly(red, ocsTwo);
+
+    // dependency wired from parent
+    assertThat(blue).isNotNull();
+    assertThat(blue.getDependency()).isSameAs(ocsThree);
+  }
+}
diff --git a/inject-test/src/test/java/org/example/custom3/TcsBlue.java b/inject-test/src/test/java/org/example/custom3/TcsBlue.java
new file mode 100644
index 00000000..d7ac0da0
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom3/TcsBlue.java
@@ -0,0 +1,20 @@
+package org.example.custom3;
+
+import org.example.custom2.OcsThree;
+
+@MyThreeScope
+public class TcsBlue {
+
+  /**
+   * Wire this dependency from the parent BeanScope.
+   */
+  final OcsThree ocs;
+
+  public TcsBlue(OcsThree ocs) {
+    this.ocs = ocs;
+  }
+
+  public OcsThree getDependency() {
+    return ocs;
+  }
+}
diff --git a/inject-test/src/test/java/org/example/custom3/TcsFactory.java b/inject-test/src/test/java/org/example/custom3/TcsFactory.java
new file mode 100644
index 00000000..9dcb930a
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom3/TcsFactory.java
@@ -0,0 +1,14 @@
+package org.example.custom3;
+
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+
+@MyThreeScope
+@Factory
+public class TcsFactory {
+
+  @Bean
+  TcsGreen green() {
+    return new TcsGreen();
+  }
+}
diff --git a/inject-test/src/test/java/org/example/custom3/TcsGreen.java b/inject-test/src/test/java/org/example/custom3/TcsGreen.java
new file mode 100644
index 00000000..804f2be5
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom3/TcsGreen.java
@@ -0,0 +1,7 @@
+package org.example.custom3;
+
+/**
+ * Created by TcsFactory
+ */
+public class TcsGreen {
+}
diff --git a/inject-test/src/test/java/org/example/custom3/TcsRed.java b/inject-test/src/test/java/org/example/custom3/TcsRed.java
new file mode 100644
index 00000000..1822828d
--- /dev/null
+++ b/inject-test/src/test/java/org/example/custom3/TcsRed.java
@@ -0,0 +1,9 @@
+package org.example.custom3;
+
+import org.example.custom2.OciMarker;
+import org.example.custom2.OciRock;
+
+@OciMarker
+@MyThreeScope
+public class TcsRed implements OciRock {
+}
diff --git a/inject-test/src/test/java/org/example/iface/NestedInterfaceTest.java b/inject-test/src/test/java/org/example/iface/NestedInterfaceTest.java
index 7ae99dda..68864d70 100644
--- a/inject-test/src/test/java/org/example/iface/NestedInterfaceTest.java
+++ b/inject-test/src/test/java/org/example/iface/NestedInterfaceTest.java
@@ -22,10 +22,11 @@ public class NestedInterfaceTest {
   void test_provided() {
 
     try (BeanScope context = BeanScope.newBuilder()
+      .forTesting()
       .withMock(Some.Nested.class, nested -> when(nested.doNested()).thenReturn("myMock"))
       .build()) {
 
-      final Some.Nested nestedInterface = context.getBean(Some.Nested.class);
+      final Some.Nested nestedInterface = context.get(Some.Nested.class);
       assertNotNull(nestedInterface);
       assertEquals("myMock", nestedInterface.doNested());
     }
diff --git a/inject-test/src/test/java/org/example/injectextension/WithExtnNamedMocksTest.java b/inject-test/src/test/java/org/example/injectextension/WithExtnNamedMocksTest.java
index a79bd538..e2fc09b8 100644
--- a/inject-test/src/test/java/org/example/injectextension/WithExtnNamedMocksTest.java
+++ b/inject-test/src/test/java/org/example/injectextension/WithExtnNamedMocksTest.java
@@ -38,6 +38,7 @@ class WithExtnNamedMocksTest {
     void test() {
 
       try (BeanScope beanScope = BeanScope.newBuilder()
+        .forTesting()
         .withMock(SomeStore.class, "Blue")
         .withMock(SomeStore.class, "green")
         .build()) {
diff --git a/inject-test/src/test/java/org/example/injectextension/WithExtnNamedSpyTest.java b/inject-test/src/test/java/org/example/injectextension/WithExtnNamedSpyTest.java
index 0873ee2c..f7be3071 100644
--- a/inject-test/src/test/java/org/example/injectextension/WithExtnNamedSpyTest.java
+++ b/inject-test/src/test/java/org/example/injectextension/WithExtnNamedSpyTest.java
@@ -42,6 +42,7 @@ class WithExtnNamedSpyTest {
     void test() {
 
       try (BeanScope beanScope = BeanScope.newBuilder()
+        .forTesting()
         .withSpy(SomeStore.class, "blue")
         .withSpy(SomeStore.class, "Green")
         .build()) {
diff --git a/inject-test/src/test/java/org/example/optional/AllQue.java b/inject-test/src/test/java/org/example/optional/AllQue.java
index 59de651d..02000dac 100644
--- a/inject-test/src/test/java/org/example/optional/AllQue.java
+++ b/inject-test/src/test/java/org/example/optional/AllQue.java
@@ -19,6 +19,7 @@ public class AllQue {
     this.frodo = frodo;
     this.sam = sam;
     this.bilbo = bilbo;
+    System.out.println("bazz");
   }
 
   String whichSet() {
diff --git a/inject-test/src/test/java/org/example/requestscope/BluesRStuff.java b/inject-test/src/test/java/org/example/requestscope/BluesRStuff.java
deleted file mode 100644
index 416d192b..00000000
--- a/inject-test/src/test/java/org/example/requestscope/BluesRStuff.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.example.requestscope;
-
-import io.avaje.inject.Request;
-import jakarta.inject.Named;
-
-//@Named("blues")
-// "blues" is derived qualifier name based on short name relative to interface short name
-@Request
-public class BluesRStuff implements RStuff {
-
-  private final RPump pump;
-
-  public BluesRStuff(@Named("blue") RPump pump) {
-    this.pump = pump;
-  }
-
-  String pump() {
-    return pump.pump();
-  }
-
-  @Override
-  public String stuff() {
-    return "stuff_"+pump();
-  }
-}
diff --git a/inject-test/src/test/java/org/example/requestscope/ComboRStuff.java b/inject-test/src/test/java/org/example/requestscope/ComboRStuff.java
deleted file mode 100644
index 64029080..00000000
--- a/inject-test/src/test/java/org/example/requestscope/ComboRStuff.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.example.requestscope;
-
-import io.avaje.inject.Request;
-import jakarta.inject.Named;
-
-@Request
-public class ComboRStuff {
-
-  private final RStuff red;
-  private final RStuff blue;
-
-  public ComboRStuff(@Named("reds") RStuff red, @Named("blues") RStuff blue) {
-    this.red = red;
-    this.blue = blue;
-  }
-
-
-  String stuff() {
-    return red.stuff() + " " + blue.stuff();
-  }
-}
diff --git a/inject-test/src/test/java/org/example/requestscope/MyReqThingWithContext.java b/inject-test/src/test/java/org/example/requestscope/MyReqThingWithContext.java
deleted file mode 100644
index d8767e63..00000000
--- a/inject-test/src/test/java/org/example/requestscope/MyReqThingWithContext.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.example.requestscope;
-
-import io.avaje.inject.Request;
-import io.javalin.http.Context;
-
-@Request
-public class MyReqThingWithContext {
-
-  final Context javalinContext;
-
-  public MyReqThingWithContext(Context javalinContext) {
-    this.javalinContext = javalinContext;
-  }
-
-}
diff --git a/inject-test/src/test/java/org/example/requestscope/MyRequestOne.java b/inject-test/src/test/java/org/example/requestscope/MyRequestOne.java
deleted file mode 100644
index 8404c2d0..00000000
--- a/inject-test/src/test/java/org/example/requestscope/MyRequestOne.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package org.example.requestscope;
-
-import io.avaje.inject.Request;
-import jakarta.inject.Inject;
-import org.example.request.AService;
-
-import javax.annotation.PostConstruct;
-import java.io.Closeable;
-import java.util.Optional;
-
-@Request
-class MyRequestOne implements Closeable {
-
-  /**
-   * Wiring a bean scope singleton.
-   */
-  private final AService service;
-  /**
-   * Wiring an bean provided to the request scope.
-   */
-  private final ReqThing reqThing;
-
-  private AService otherService;
-  private ReqThing otherReqThing;
-
-  /**
-   * Field injection on request scoped bean.
-   */
-  @Inject
-  Optional<AService> myopt;
-
-  private boolean firedPostConstruct;
-  private boolean firedMethodInjection;
-  private boolean firedClose;
-
-  MyRequestOne(AService service, ReqThing reqThing) {
-    this.service = service;
-    this.reqThing = reqThing;
-  }
-
-  /**
-   * Method injection on request scoped bean.
-   */
-  @Inject
-  void foo(AService other, ReqThing otherThing) {
-    firedMethodInjection = true;
-    otherService = other;
-    otherReqThing = otherThing;
-  }
-
-  /**
-   * Run immediately after field/method injection of request scoped bean.
-   */
-  @PostConstruct
-  void init() {
-    firedPostConstruct = true;
-  }
-
-  /**
-   * Closable automatically treated as PreDestroy for request scoped beans.
-   */
-  @Override
-  public void close() {
-    firedClose = true;
-  }
-
-  AService getService() {
-    return service;
-  }
-
-  ReqThing getReqThing() {
-    return reqThing;
-  }
-
-  AService getOtherService() {
-    return otherService;
-  }
-
-  ReqThing getOtherReqThing() {
-    return otherReqThing;
-  }
-
-  boolean isFiredPostConstruct() {
-    return firedPostConstruct;
-  }
-
-  boolean isFiredMethodInjection() {
-    return firedMethodInjection;
-  }
-
-  boolean isFiredClose() {
-    return firedClose;
-  }
-}
diff --git a/inject-test/src/test/java/org/example/requestscope/MyRequestPreDestroy.java b/inject-test/src/test/java/org/example/requestscope/MyRequestPreDestroy.java
deleted file mode 100644
index 193acfb3..00000000
--- a/inject-test/src/test/java/org/example/requestscope/MyRequestPreDestroy.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.example.requestscope;
-
-import io.avaje.inject.PreDestroy;
-import io.avaje.inject.Request;
-
-@Request
-public class MyRequestPreDestroy {
-
-  private boolean firedPreDestroy;
-
-  /**
-   * Using PreDestroy rather than implementing Closable.
-   */
-  @PreDestroy
-  void destroy() {
-    firedPreDestroy = true;
-  }
-
-  boolean isFiredPreDestroy() {
-    return firedPreDestroy;
-  }
-}
diff --git a/inject-test/src/test/java/org/example/requestscope/NotReqBean.java b/inject-test/src/test/java/org/example/requestscope/NotReqBean.java
deleted file mode 100644
index 8d46b86f..00000000
--- a/inject-test/src/test/java/org/example/requestscope/NotReqBean.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.example.requestscope;
-
-import jakarta.inject.Singleton;
-
-@Singleton
-public class NotReqBean {
-
-  //final MyReqThingWithContext myReq;
-
-  public NotReqBean() {//MyReqThingWithContext myReq) {
-    //this.myReq = myReq;
-  }
-
-}
diff --git a/inject-test/src/test/java/org/example/requestscope/RPump.java b/inject-test/src/test/java/org/example/requestscope/RPump.java
deleted file mode 100644
index c7a8f392..00000000
--- a/inject-test/src/test/java/org/example/requestscope/RPump.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.example.requestscope;
-
-public interface RPump {
-
-  String pump();
-}
diff --git a/inject-test/src/test/java/org/example/requestscope/RPumpF.java b/inject-test/src/test/java/org/example/requestscope/RPumpF.java
deleted file mode 100644
index 9a5511f0..00000000
--- a/inject-test/src/test/java/org/example/requestscope/RPumpF.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.example.requestscope;
-
-class RPumpF implements RPump {
-
-  final String data;
-
-  RPumpF(String data) {
-    this.data = data;
-  }
-
-  @Override
-  public String pump() {
-    return data;
-  }
-}
diff --git a/inject-test/src/test/java/org/example/requestscope/RStuff.java b/inject-test/src/test/java/org/example/requestscope/RStuff.java
deleted file mode 100644
index d0e2e1f3..00000000
--- a/inject-test/src/test/java/org/example/requestscope/RStuff.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.example.requestscope;
-
-public interface RStuff {
-
-  String stuff();
-}
diff --git a/inject-test/src/test/java/org/example/requestscope/RedsRStuff.java b/inject-test/src/test/java/org/example/requestscope/RedsRStuff.java
deleted file mode 100644
index 507204f0..00000000
--- a/inject-test/src/test/java/org/example/requestscope/RedsRStuff.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.example.requestscope;
-
-import io.avaje.inject.Request;
-import jakarta.inject.Named;
-
-@Named("reds")
-@Request
-public class RedsRStuff implements RStuff {
-
-  private final RPump pump;
-
-  public RedsRStuff(@Named("red") RPump pump) {
-    this.pump = pump;
-  }
-
-  String pump() {
-    return pump.pump();
-  }
-
-  @Override
-  public String stuff() {
-    return "stuff_"+pump();
-  }
-}
diff --git a/inject-test/src/test/java/org/example/requestscope/ReqThing.java b/inject-test/src/test/java/org/example/requestscope/ReqThing.java
deleted file mode 100644
index 26455434..00000000
--- a/inject-test/src/test/java/org/example/requestscope/ReqThing.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package org.example.requestscope;
-
-public interface ReqThing {
-  String hello();
-}
diff --git a/inject-test/src/test/java/org/example/requestscope/RequestScopedBeanTest.java b/inject-test/src/test/java/org/example/requestscope/RequestScopedBeanTest.java
deleted file mode 100644
index e66ba52c..00000000
--- a/inject-test/src/test/java/org/example/requestscope/RequestScopedBeanTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package org.example.requestscope;
-
-import io.avaje.inject.ApplicationScope;
-import io.avaje.inject.RequestScope;
-import org.example.request.AService;
-import org.junit.jupiter.api.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-class RequestScopedBeanTest {
-
-  @Test
-  void preDestroy() {
-
-    MyRequestPreDestroy bean;
-    try (RequestScope requestScope = ApplicationScope.newRequestScope()
-      .build()) {
-      bean = requestScope.get(MyRequestPreDestroy.class);
-      assertThat(bean.isFiredPreDestroy()).isFalse();
-    }
-    assertThat(bean.isFiredPreDestroy()).isTrue();
-  }
-
-  @Test
-  void newRequestScope() {
-
-    MyReqThing myReqThing = new MyReqThing();
-
-    MyRequestOne myRequestOne;
-    try (RequestScope requestScope = ApplicationScope.newRequestScope()
-      .withBean(ReqThing.class, myReqThing)
-      .build()) {
-
-      myRequestOne = requestScope.get(MyRequestOne.class);
-      assertThat(myRequestOne.getReqThing().hello()).isEqualTo("MyReqThing");
-
-      assertThat(myRequestOne.isFiredPostConstruct()).isTrue();
-      assertThat(myRequestOne.isFiredMethodInjection()).isTrue();
-
-      assertThat(myRequestOne).isNotNull();
-      assertThat(myRequestOne.getReqThing()).isSameAs(myReqThing);
-
-      AService service = requestScope.get(AService.class);
-      assertThat(myRequestOne.getService()).isSameAs(service);
-      assertThat(myRequestOne.getOtherService()).isSameAs(service);
-      assertThat(myRequestOne.getOtherReqThing()).isSameAs(myReqThing);
-
-      // assert we get back the same instance
-      MyRequestOne myRequestOneAgain = requestScope.get(MyRequestOne.class);
-      assertThat(myRequestOneAgain).isSameAs(myRequestOne);
-    }
-
-    // assert request scoped closable was closed
-    assertThat(myRequestOne.isFiredClose()).isTrue();
-  }
-
-  @Test
-  void provideNamedForRequestBeans() {
-
-    // providing named implementations of RPump
-    RPump redPump = new RPumpF("red");
-    RPump bluePump = new RPumpF("blue");
-
-    try (RequestScope requestScope = ApplicationScope.newRequestScope()
-      .withBean("red", RPump.class, redPump)
-      .withBean("blue", RPump.class, bluePump)
-      .build()) {
-
-      final RedsRStuff redsRStuff = requestScope.get(RedsRStuff.class);
-      assertThat(redsRStuff.pump()).isEqualTo("red");
-
-      final RStuff redsRStuff2 = requestScope.get(RStuff.class, "reds");
-      assertThat(redsRStuff2.stuff()).isEqualTo("stuff_red");
-      assertThat(redsRStuff2).isSameAs(redsRStuff);
-
-
-      final BluesRStuff bluesRStuff = requestScope.get(BluesRStuff.class);
-      assertThat(bluesRStuff.pump()).isEqualTo("blue");
-
-      final RStuff blues2 = requestScope.get(RStuff.class, "blues");
-      assertThat(blues2.stuff()).isEqualTo("stuff_blue");
-      assertThat(blues2).isSameAs(bluesRStuff);
-
-      final ComboRStuff comboRStuff = requestScope.get(ComboRStuff.class);
-      assertThat(comboRStuff.stuff()).isEqualTo("stuff_red stuff_blue");
-    }
-  }
-
-  static class MyReqThing implements ReqThing {
-    @Override
-    public String hello() {
-      return "MyReqThing";
-    }
-  }
-}
diff --git a/inject-test/src/test/java/org/example/requestscope/RequestScopedWithControllerDependencyTest.java b/inject-test/src/test/java/org/example/requestscope/RequestScopedWithControllerDependencyTest.java
deleted file mode 100644
index 6c84913f..00000000
--- a/inject-test/src/test/java/org/example/requestscope/RequestScopedWithControllerDependencyTest.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.example.requestscope;
-
-import io.avaje.inject.ApplicationScope;
-import io.avaje.inject.RequestScope;
-import io.javalin.http.Context;
-import org.junit.jupiter.api.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-
-/**
- * Test that the factory generation is suppressed for the case where
- * the request scoped bean has a dependency on one of the special
- * 'request scope controller dependencies' (like Javalin Context).
- */
-class RequestScopedWithControllerDependencyTest {
-
-  @Test
-  void test() {
-    try (RequestScope scope = ApplicationScope.newRequestScope()
-      .withBean(Context.class, mock(Context.class))
-      .build()) {
-
-      final MyReqThingWithContext reqThing = scope.get(MyReqThingWithContext.class);
-      assertThat(reqThing.javalinContext).isNotNull();
-    }
-  }
-}
diff --git a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
index be0bf952..326bab20 100644
--- a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
+++ b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
@@ -1,11 +1,10 @@
 package io.avaje.inject;
 
-import io.avaje.inject.spi.BeanScopeFactory;
+import io.avaje.inject.spi.Module;
 import io.avaje.inject.spi.Builder;
 import org.junit.jupiter.api.Test;
 
-import java.util.Collections;
-import java.util.List;
+import java.util.*;
 import java.util.stream.Collectors;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -15,7 +14,7 @@ public class BeanScopeBuilderTest {
   @Test
   public void noDepends() {
 
-    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true);
+    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true);
     factoryOrder.add(bc("1", null, null));
     factoryOrder.add(bc("2", null, null));
     factoryOrder.add(bc("3", null, null));
@@ -27,8 +26,8 @@ public class BeanScopeBuilderTest {
   @Test
   public void name_depends() {
 
-    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true);
-    factoryOrder.add(bc("two", null, "one"));
+    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true);
+    factoryOrder.add(bc("two", null, of(Mod3.class)));
     factoryOrder.add(bc("one", null, null));
     factoryOrder.orderFactories();
 
@@ -38,11 +37,11 @@ public class BeanScopeBuilderTest {
   @Test
   public void name_depends4() {
 
-    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true);
-    factoryOrder.add(bc("1", null, "3"));
-    factoryOrder.add(bc("2", null, "4"));
-    factoryOrder.add(bc("3", null, "4"));
-    factoryOrder.add(bc("4", null, null));
+    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true);
+    factoryOrder.add(bc("1", null, of(Mod3.class)));
+    factoryOrder.add(bc("2", null, of(Mod4.class)));
+    factoryOrder.add(bc("3", of(Mod3.class), of(Mod4.class)));
+    factoryOrder.add(bc("4", of(Mod4.class), null));
 
     factoryOrder.orderFactories();
 
@@ -52,11 +51,11 @@ public class BeanScopeBuilderTest {
   @Test
   public void nameFeature_depends() {
 
-    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true);
-    factoryOrder.add(bc("1", "a", "3"));
-    factoryOrder.add(bc("2", null, "4,a"));
-    factoryOrder.add(bc("3", null, "4"));
-    factoryOrder.add(bc("4", null, null));
+    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(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("3", of(Mod3.class), of(Mod4.class)));
+    factoryOrder.add(bc("4", of(Mod4.class), null));
 
     factoryOrder.orderFactories();
 
@@ -66,9 +65,9 @@ public class BeanScopeBuilderTest {
   @Test
   public void feature_depends() {
 
-    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true);
-    factoryOrder.add(bc("two", null, "myfeature"));
-    factoryOrder.add(bc("one", "myfeature", null));
+    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true);
+    factoryOrder.add(bc("two", null, of(MyFeature.class)));
+    factoryOrder.add(bc("one", of(MyFeature.class), null));
     factoryOrder.orderFactories();
 
     assertThat(names(factoryOrder.factories())).containsExactly("one", "two");
@@ -77,54 +76,50 @@ public class BeanScopeBuilderTest {
   @Test
   public void feature_depends2() {
 
-    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true);
-    factoryOrder.add(bc("two", null, "myfeature"));
-    factoryOrder.add(bc("one", "myfeature", null));
-    factoryOrder.add(bc("three", "myfeature", null));
+    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(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.orderFactories();
 
     assertThat(names(factoryOrder.factories())).containsExactly("one", "three", "two");
   }
 
-  private List<String> names(List<BeanScopeFactory> factories) {
+  private List<String> names(List<Module> factories) {
     return factories.stream()
-      .map(BeanScopeFactory::getName)
+      .map(Module::toString)
       .collect(Collectors.toList());
   }
 
-  private TDBeanScope bc(String name, String provides, String dependsOn) {
-    return new TDBeanScope(name, split(provides), split(dependsOn));
+  private TDBeanScope bc(String name, Class<?>[] provides, Class<?>[] dependsOn) {
+    return new TDBeanScope(name, provides, dependsOn);
   }
 
-  private String[] split(String val) {
-    return val == null ? null : val.split(",");
-  }
-
-  private static class TDBeanScope implements BeanScopeFactory {
+  private static class TDBeanScope implements Module {
 
     final String name;
-    final String[] provides;
-    final String[] dependsOn;
+    final Class<?>[] provides;
+    final Class<?>[] requires;
 
-    private TDBeanScope(String name, String[] provides, String[] dependsOn) {
+    private TDBeanScope(String name, Class<?>[] provides, Class<?>[] requires) {
       this.name = name;
       this.provides = provides;
-      this.dependsOn = dependsOn;
+      this.requires = requires;
     }
 
     @Override
-    public String getName() {
+    public String toString() {
       return name;
     }
 
     @Override
-    public String[] getProvides() {
+    public Class<?>[] provides() {
       return provides;
     }
 
     @Override
-    public String[] getDependsOn() {
-      return dependsOn;
+    public Class<?>[] requires() {
+      return requires;
     }
 
     @Override
@@ -132,4 +127,17 @@ public class BeanScopeBuilderTest {
 
     }
   }
+
+  Class<?>[] of(Class<?>... cls) {
+    return cls != null ? cls : new Class<?>[0];
+  }
+
+  class MyFeature {
+  }
+  class FeatureA {
+  }
+  class Mod3 {
+  }
+  class Mod4 {
+  }
 }
diff --git a/inject/src/test/java/io/avaje/inject/spi/DBeanScopeTest.java b/inject/src/test/java/io/avaje/inject/spi/DBeanScopeTest.java
new file mode 100644
index 00000000..244c9445
--- /dev/null
+++ b/inject/src/test/java/io/avaje/inject/spi/DBeanScopeTest.java
@@ -0,0 +1,34 @@
+package io.avaje.inject.spi;
+
+import org.assertj.core.util.Arrays;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static java.util.Collections.emptyList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DBeanScopeTest {
+
+  @Test
+  void combine_asFirst() {
+    final List<Object> result = DBeanScope.combine(list("A", "B"), emptyList());
+    assertThat(result).containsExactly("A", "B");
+  }
+
+  @Test
+  void combine_asLast() {
+    final List<Object> result = DBeanScope.combine(emptyList(), list("A", "B"));
+    assertThat(result).containsExactly("A", "B");
+  }
+  @Test
+  void combine_both() {
+    final List<Object> result = DBeanScope.combine(list("A", "B"),list("C"));
+    assertThat(result).containsExactly("A", "B", "C");
+  }
+
+
+  List<Object> list(String... vals) {
+    return Arrays.asList(vals);
+  }
+}