Repository: avaje/avaje-inject
Pull request #81 (2020-11-04 22:19:08)

Refactor BeanContextBuilder extract interface, use static newBuilder()
Refactor to make the api more abstract by changing BeanContextBuilder to be an interface.

Breaking change, we need to change `new BeanContextBuilder()` to be `BeanContext.newBuilder()`


Patch:
diff --git a/inject/src/main/java/io/avaje/inject/BeanContext.java b/inject/src/main/java/io/avaje/inject/BeanContext.java
index 857a2825..eabf8c87 100644
--- a/inject/src/main/java/io/avaje/inject/BeanContext.java
+++ b/inject/src/main/java/io/avaje/inject/BeanContext.java
@@ -44,6 +44,39 @@ import java.util.List;
  */
 public interface BeanContext extends Closeable {
 
+  /**
+   * Build a bean context with options for shutdown hook and supplying test doubles.
+   * <p>
+   * We would choose to use BeanContextBuilder in test code (for component testing)
+   * as it gives us the ability to inject test doubles, mocks, spy's etc.
+   * </p>
+   *
+   * <pre>{@code
+   *
+   *   ＠Test
+   *   public void someComponentTest() {
+   *
+   *     MyRedisApi mockRedis = mock(MyRedisApi.class);
+   *     MyDbApi mockDatabase = mock(MyDbApi.class);
+   *
+   *     try (BeanContext context = BeanContext.newBuilder()
+   *       .withBeans(mockRedis, mockDatabase)
+   *       .build()) {
+   *
+   *       // built with test doubles injected ...
+   *       CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
+   *       coffeeMaker.makeIt();
+   *
+   *       assertThat(...
+   *     }
+   *   }
+   *
+   * }</pre>
+   */
+  static BeanContextBuilder newBuilder() {
+    return new DBeanContextBuilder();
+  }
+
   /**
    * Return the module name of the bean context.
    *
diff --git a/inject/src/main/java/io/avaje/inject/BeanContextBuilder.java b/inject/src/main/java/io/avaje/inject/BeanContextBuilder.java
index 24600b06..26dcfe7c 100644
--- a/inject/src/main/java/io/avaje/inject/BeanContextBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/BeanContextBuilder.java
@@ -1,22 +1,5 @@
 package io.avaje.inject;
 
-import io.avaje.inject.spi.BeanContextFactory;
-import io.avaje.inject.spi.Builder;
-import io.avaje.inject.spi.EnrichBean;
-import io.avaje.inject.spi.SuppliedBean;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.lang.annotation.Annotation;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.ServiceLoader;
-import java.util.Set;
 import java.util.function.Consumer;
 
 /**
@@ -34,7 +17,7 @@ import java.util.function.Consumer;
  *     MyRedisApi mockRedis = mock(MyRedisApi.class);
  *     MyDbApi mockDatabase = mock(MyDbApi.class);
  *
- *     try (BeanContext context = new BeanContextBuilder()
+ *     try (BeanContext context = BeanContext.newBuilder()
  *       .withBeans(mockRedis, mockDatabase)
  *       .build()) {
  *
@@ -48,39 +31,10 @@ import java.util.function.Consumer;
  *
  * }</pre>
  */
-public class BeanContextBuilder {
-
-  private static final Logger log = LoggerFactory.getLogger(BeanContextBuilder.class);
-
-  private boolean shutdownHook = true;
-
-  @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 boolean ignoreMissingModuleDependencies;
+public interface BeanContextBuilder {
 
   /**
-   * Create a BeanContextBuilder to ultimately load and return a new BeanContext.
-   *
-   * <pre>{@code
-   *
-   *   try (BeanContext context = new BeanContextBuilder()
-   *     .build()) {
-   *
-   *     String makeIt = context.getBean(CoffeeMaker.class).makeIt();
-   *   }
-   * }</pre>
-   */
-  public BeanContextBuilder() {
-  }
-
-  /**
-   * Boot the bean context without registering a shutdown hook.
+   * Create the bean context without registering a shutdown hook.
    * <p>
    * The expectation is that the BeanContextBuilder is closed via code or via using
    * try with resources.
@@ -89,7 +43,7 @@ public class BeanContextBuilder {
    *
    *   // automatically closed via try with resources
    *
-   *   try (BeanContext context = new BeanContextBuilder()
+   *   try (BeanContext context = BeanContext.newBuilder()
    *     .withNoShutdownHook()
    *     .build()) {
    *
@@ -100,10 +54,7 @@ public class BeanContextBuilder {
    *
    * @return This BeanContextBuilder
    */
-  public BeanContextBuilder withNoShutdownHook() {
-    this.shutdownHook = false;
-    return this;
-  }
+  BeanContextBuilder withNoShutdownHook();
 
   /**
    * Specify the modules to include in dependency injection.
@@ -121,7 +72,7 @@ public class BeanContextBuilder {
    *
    *     EmailServiceApi mockEmailService = mock(EmailServiceApi.class);
    *
-   *     try (BeanContext context = new BeanContextBuilder()
+   *     try (BeanContext context = BeanContext.newBuilder()
    *       .withBeans(mockEmailService)
    *       .withModules("coffee")
    *       .withIgnoreMissingModuleDependencies()
@@ -140,19 +91,13 @@ public class BeanContextBuilder {
    * @param modules The names of modules that we want to include in dependency injection.
    * @return This BeanContextBuilder
    */
-  public BeanContextBuilder withModules(String... modules) {
-    this.includeModules.addAll(Arrays.asList(modules));
-    return this;
-  }
+  BeanContextBuilder withModules(String... modules);
 
   /**
    * Set this when building a BeanContext (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.
    */
-  public BeanContextBuilder withIgnoreMissingModuleDependencies() {
-    this.ignoreMissingModuleDependencies = true;
-    return this;
-  }
+  BeanContextBuilder withIgnoreMissingModuleDependencies();
 
   /**
    * Supply a bean to the context that will be used instead of any
@@ -167,7 +112,7 @@ public class BeanContextBuilder {
    *   Pump pump = mock(Pump.class);
    *   Grinder grinder = mock(Grinder.class);
    *
-   *   try (BeanContext context = new BeanContextBuilder()
+   *   try (BeanContext context = BeanContext.newBuilder()
    *     .withBeans(pump, grinder)
    *     .build()) {
    *
@@ -190,12 +135,7 @@ public class BeanContextBuilder {
    * @return This BeanContextBuilder
    */
   @SuppressWarnings({"unchecked", "rawtypes"})
-  public BeanContextBuilder withBeans(Object... beans) {
-    for (Object bean : beans) {
-      suppliedBeans.add(new SuppliedBean(suppliedType(bean.getClass()), bean));
-    }
-    return this;
-  }
+  BeanContextBuilder withBeans(Object... beans);
 
   /**
    * Add a supplied bean instance with the given injection type.
@@ -205,8 +145,10 @@ public class BeanContextBuilder {
    *
    * <pre>{@code
    *
-   *   try (BeanContext context = new BeanContextBuilder()
-   *     .withBean(Pump.class, mock)
+   *   Pump mockPump = ...
+   *
+   *   try (BeanContext context = BeanContext.newBuilder()
+   *     .withBean(Pump.class, mockPump)
    *     .build()) {
    *
    *     Pump pump = context.getBean(Pump.class);
@@ -225,17 +167,14 @@ public class BeanContextBuilder {
    * @param type The dependency injection type this bean is target for
    * @param bean The supplied bean instance to use (typically a test mock)
    */
-  public <D> BeanContextBuilder withBean(Class<D> type, D bean) {
-    suppliedBeans.add(new SuppliedBean<>(type, bean));
-    return this;
-  }
+  <D> BeanContextBuilder withBean(Class<D> type, D bean);
 
   /**
    * Use a mockito mock when injecting this bean type.
    *
    * <pre>{@code
    *
-   *   try (BeanContext context = new BeanContextBuilder()
+   *   try (BeanContext context = BeanContext.newBuilder()
    *     .withMock(Pump.class)
    *     .withMock(Grinder.class, grinder -> {
    *       // setup the mock
@@ -254,9 +193,7 @@ public class BeanContextBuilder {
    *
    * }</pre>
    */
-  public BeanContextBuilder withMock(Class<?> type) {
-    return withMock(type, null);
-  }
+  BeanContextBuilder withMock(Class<?> type);
 
   /**
    * Use a mockito mock when injecting this bean type additionally
@@ -264,7 +201,7 @@ public class BeanContextBuilder {
    *
    * <pre>{@code
    *
-   *   try (BeanContext context = new BeanContextBuilder()
+   *   try (BeanContext context = BeanContext.newBuilder()
    *     .withMock(Pump.class)
    *     .withMock(Grinder.class, grinder -> {
    *
@@ -284,17 +221,14 @@ public class BeanContextBuilder {
    *
    * }</pre>
    */
-  public <D> BeanContextBuilder withMock(Class<D> type, Consumer<D> consumer) {
-    suppliedBeans.add(new SuppliedBean<>(type, null, consumer));
-    return this;
-  }
+  <D> BeanContextBuilder withMock(Class<D> type, Consumer<D> consumer);
 
   /**
    * Use a mockito spy when injecting this bean type.
    *
    * <pre>{@code
    *
-   *   try (BeanContext context = new BeanContextBuilder()
+   *   try (BeanContext context = BeanContext.newBuilder()
    *     .withSpy(Pump.class)
    *     .build()) {
    *
@@ -312,9 +246,7 @@ public class BeanContextBuilder {
    *
    * }</pre>
    */
-  public BeanContextBuilder withSpy(Class<?> type) {
-    return withSpy(type, null);
-  }
+  BeanContextBuilder withSpy(Class<?> type);
 
   /**
    * Use a mockito spy when injecting this bean type additionally
@@ -322,7 +254,7 @@ public class BeanContextBuilder {
    *
    * <pre>{@code
    *
-   *   try (BeanContext context = new BeanContextBuilder()
+   *   try (BeanContext context = BeanContext.newBuilder()
    *     .withSpy(Pump.class, pump -> {
    *       // setup the spy
    *       doNothing().when(pump).pumpWater();
@@ -343,390 +275,12 @@ public class BeanContextBuilder {
    *
    * }</pre>
    */
-  public <D> BeanContextBuilder withSpy(Class<D> type, Consumer<D> consumer) {
-    enrichBeans.add(new EnrichBean<>(type, consumer));
-    return this;
-  }
+  <D> BeanContextBuilder withSpy(Class<D> type, Consumer<D> consumer);
 
   /**
    * Build and return the bean context.
    *
    * @return The BeanContext
    */
-  public BeanContext build() {
-    // sort factories by dependsOn
-    FactoryOrder factoryOrder = new FactoryOrder(includeModules, !suppliedBeans.isEmpty(), ignoreMissingModuleDependencies);
-    ServiceLoader.load(BeanContextFactory.class).forEach(factoryOrder::add);
-
-    Set<String> moduleNames = factoryOrder.orderFactories();
-    if (moduleNames.isEmpty()) {
-      throw new IllegalStateException("No modules found. When using java module system we need an explicit provides clause in module-info like:\n\n" +
-        " provides io.avaje.inject.spi.BeanContextFactory with org.example._di$BeanContextFactory;\n\n" +
-        " Otherwise perhaps using Gradle and IDEA but with a setup issue?" +
-        " Review IntelliJ Settings / Build / Build tools / Gradle - 'Build and run using' value and set that to 'Gradle'. " +
-        " Refer to https://avaje.io/inject#gradle");
-    }
-    log.debug("building context with modules {}", moduleNames);
-    Builder rootBuilder = Builder.newRootBuilder(suppliedBeans, enrichBeans);
-    for (BeanContextFactory factory : factoryOrder.factories()) {
-      rootBuilder.addChild(factory);
-    }
-
-    BeanContext beanContext = rootBuilder.build();
-    // entire graph built, fire postConstruct
-    beanContext.start();
-    if (shutdownHook) {
-      return new ShutdownAwareBeanContext(beanContext);
-    }
-    return beanContext;
-  }
-
-  /**
-   * Return the type that we map the supplied bean to.
-   */
-  private Class<?> suppliedType(Class<?> suppliedClass) {
-    Class<?> suppliedSuper = suppliedClass.getSuperclass();
-    if (Object.class.equals(suppliedSuper)) {
-      return suppliedClass;
-    } else {
-      // prefer to use the super type of the supplied bean (test double)
-      return suppliedSuper;
-    }
-  }
-
-  /**
-   * Internal shutdown hook.
-   */
-  private static class Hook extends Thread {
-
-    private final ShutdownAwareBeanContext context;
-
-    Hook(ShutdownAwareBeanContext context) {
-      this.context = context;
-    }
-
-    @Override
-    public void run() {
-      context.shutdown();
-    }
-  }
-
-  /**
-   * Proxy that handles shutdown hook registration and de-registration.
-   */
-  private static class ShutdownAwareBeanContext implements BeanContext {
-
-    private final BeanContext context;
-    private final Hook hook;
-    private boolean shutdown;
-
-    ShutdownAwareBeanContext(BeanContext context) {
-      this.context = context;
-      this.hook = new Hook(this);
-      Runtime.getRuntime().addShutdownHook(hook);
-    }
-
-    @Override
-    public String getName() {
-      return context.getName();
-    }
-
-    @Override
-    public String[] getProvides() {
-      return context.getProvides();
-    }
-
-    @Override
-    public String[] getDependsOn() {
-      return context.getDependsOn();
-    }
-
-    @Override
-    public <T> T getBean(Class<T> beanClass) {
-      return context.getBean(beanClass);
-    }
-
-    @Override
-    public <T> T getBean(Class<T> beanClass, String name) {
-      return context.getBean(beanClass, name);
-    }
-
-    @Override
-    public <T> BeanEntry<T> candidate(Class<T> type, String name) {
-      return context.candidate(type, name);
-    }
-
-    @Override
-    public List<Object> getBeansWithAnnotation(Class<?> annotation) {
-      return context.getBeansWithAnnotation(annotation);
-    }
-
-    @Override
-    public <T> List<T> getBeans(Class<T> interfaceType) {
-      return context.getBeans(interfaceType);
-    }
-
-    @Override
-    public <T> List<T> getBeansByPriority(Class<T> interfaceType) {
-      return context.getBeansByPriority(interfaceType);
-    }
-
-    @Override
-    public <T> List<T> getBeansByPriority(Class<T> interfaceType, Class<? extends Annotation> priority) {
-      return context.getBeansByPriority(interfaceType, priority);
-    }
-
-    @Override
-    public <T> List<T> sortByPriority(List<T> list) {
-      return context.sortByPriority(list);
-    }
-
-    @Override
-    public <T> List<T> sortByPriority(List<T> list, Class<? extends Annotation> priority) {
-      return context.sortByPriority(list, priority);
-    }
-
-    @Override
-    public void start() {
-      context.start();
-    }
-
-    @Override
-    public void close() {
-      synchronized (this) {
-        if (!shutdown) {
-          Runtime.getRuntime().removeShutdownHook(hook);
-        }
-        context.close();
-      }
-    }
-
-    /**
-     * Close via shutdown hook.
-     */
-    void shutdown() {
-      synchronized (this) {
-        shutdown = true;
-        close();
-      }
-    }
-  }
-
-  /**
-   * Helper to order the BeanContextFactory based on dependsOn.
-   */
-  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<BeanContextFactory> 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;
-      this.suppliedBeans = suppliedBeans;
-      this.ignoreMissingModuleDependencies = ignoreMissingModuleDependencies;
-    }
-
-    void add(BeanContextFactory 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);
-          }
-        }
-        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);
-          }
-        } else {
-          // queue it to process by dependency ordering
-          queue.add(wrappedFactory);
-        }
-      }
-    }
-
-    private boolean isEmpty(String[] values) {
-      return values == null || values.length == 0;
-    }
-
-    /**
-     * Return true of the factory (for the module) should be included.
-     */
-    private boolean includeModule(BeanContextFactory 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());
-    }
-
-    /**
-     * Order the factories returning the ordered list of module names.
-     */
-    Set<String> orderFactories() {
-      // push the 'no dependency' modules after the 'provides only' ones
-      // as this is more intuitive for the simple (only provides modules case)
-      for (FactoryState factoryState : queueNoDependencies) {
-        push(factoryState);
-      }
-      processQueue();
-      return moduleNames;
-    }
-
-    /**
-     * Return the list of factories in the order they should be built.
-     */
-    List<BeanContextFactory> factories() {
-      return factories;
-    }
-
-    /**
-     * Process the queue pushing the factories in order to satisfy dependencies.
-     */
-    private void processQueue() {
-
-      int count;
-      do {
-        count = processQueuedFactories();
-      } while (count > 0);
-
-      if (suppliedBeans || ignoreMissingModuleDependencies) {
-        // just push everything left assuming supplied beans
-        // will satisfy the required dependencies
-        for (FactoryState factoryState : queue) {
-          push(factoryState);
-        }
-
-      } 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()) {
-            if (!moduleNames.contains(depModuleName)) {
-              sb.append(String.format(" [%s]", depModuleName));
-            }
-          }
-        }
-
-        sb.append(". Modules that were loaded ok are:").append(moduleNames);
-        sb.append(". Consider using BeanContextBuilder.withIgnoreMissingModuleDependencies() or BeanContextBuilder.withSuppliedBeans(...)");
-        throw new IllegalStateException(sb.toString());
-      }
-    }
-
-    /**
-     * Process the queued factories pushing them when all their (module) dependencies
-     * are satisfied.
-     * <p>
-     * 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()) {
-        FactoryState factory = it.next();
-        if (satisfiedDependencies(factory)) {
-          // push the factory onto the build order
-          it.remove();
-          push(factory);
-          count++;
-        }
-      }
-      return count;
-    }
-
-    /**
-     * 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);
-        if (factories == null || !factories.allPushed()) {
-          return false;
-        }
-      }
-      return true;
-    }
-  }
-
-  /**
-   * Wrapper on Factory holding the pushed state.
-   */
-  private static class FactoryState {
-
-    private final BeanContextFactory factory;
-    private boolean pushed;
-
-    private FactoryState(BeanContextFactory factory) {
-      this.factory = factory;
-    }
-
-    /**
-     * Set when factory is pushed onto the build/wiring order.
-     */
-    void setPushed() {
-      this.pushed = true;
-    }
-
-    boolean isPushed() {
-      return pushed;
-    }
-
-    BeanContextFactory getFactory() {
-      return factory;
-    }
-
-    String getName() {
-      return factory.getName();
-    }
-
-    String[] getDependsOn() {
-      return factory.getDependsOn();
-    }
-  }
-
-  /**
-   * List of factories for a given name or feature.
-   */
-  private static class FactoryList {
-
-    private final List<FactoryState> factories = new ArrayList<>();
-
-    void add(FactoryState factory) {
-      factories.add(factory);
-    }
-
-    /**
-     * Return true if all factories here have been pushed onto the build order.
-     */
-    boolean allPushed() {
-      for (FactoryState factory : factories) {
-        if (!factory.isPushed()) {
-          return false;
-        }
-      }
-      return true;
-    }
-  }
-
+  BeanContext build();
 }
diff --git a/inject/src/main/java/io/avaje/inject/DBeanContextBuilder.java b/inject/src/main/java/io/avaje/inject/DBeanContextBuilder.java
new file mode 100644
index 00000000..d4b493a6
--- /dev/null
+++ b/inject/src/main/java/io/avaje/inject/DBeanContextBuilder.java
@@ -0,0 +1,479 @@
+package io.avaje.inject;
+
+import io.avaje.inject.spi.BeanContextFactory;
+import io.avaje.inject.spi.Builder;
+import io.avaje.inject.spi.EnrichBean;
+import io.avaje.inject.spi.SuppliedBean;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.function.Consumer;
+
+/**
+ * Build a bean context with options for shutdown hook and supplying test doubles.
+ */
+class DBeanContextBuilder implements BeanContextBuilder {
+
+  private static final Logger log = LoggerFactory.getLogger(DBeanContextBuilder.class);
+
+  private boolean shutdownHook = true;
+
+  @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 boolean ignoreMissingModuleDependencies;
+
+  /**
+   * Create a BeanContextBuilder to ultimately load and return a new BeanContext.
+   */
+  DBeanContextBuilder() {
+  }
+
+  @Override
+  public BeanContextBuilder withNoShutdownHook() {
+    this.shutdownHook = false;
+    return this;
+  }
+
+  @Override
+  public BeanContextBuilder withModules(String... modules) {
+    this.includeModules.addAll(Arrays.asList(modules));
+    return this;
+  }
+
+  @Override
+  public BeanContextBuilder withIgnoreMissingModuleDependencies() {
+    this.ignoreMissingModuleDependencies = true;
+    return this;
+  }
+
+  @Override
+  @SuppressWarnings({"unchecked", "rawtypes"})
+  public BeanContextBuilder withBeans(Object... beans) {
+    for (Object bean : beans) {
+      suppliedBeans.add(new SuppliedBean(suppliedType(bean.getClass()), bean));
+    }
+    return this;
+  }
+
+  @Override
+  public <D> DBeanContextBuilder withBean(Class<D> type, D bean) {
+    suppliedBeans.add(new SuppliedBean<>(type, bean));
+    return this;
+  }
+
+  @Override
+  public BeanContextBuilder withMock(Class<?> type) {
+    return withMock(type, null);
+  }
+
+  @Override
+  public <D> DBeanContextBuilder withMock(Class<D> type, Consumer<D> consumer) {
+    suppliedBeans.add(new SuppliedBean<>(type, null, consumer));
+    return this;
+  }
+
+  @Override
+  public BeanContextBuilder withSpy(Class<?> type) {
+    return withSpy(type, null);
+  }
+
+  @Override
+  public <D> DBeanContextBuilder withSpy(Class<D> type, Consumer<D> consumer) {
+    enrichBeans.add(new EnrichBean<>(type, consumer));
+    return this;
+  }
+
+  @Override
+  public BeanContext build() {
+    // sort factories by dependsOn
+    FactoryOrder factoryOrder = new FactoryOrder(includeModules, !suppliedBeans.isEmpty(), ignoreMissingModuleDependencies);
+    ServiceLoader.load(BeanContextFactory.class).forEach(factoryOrder::add);
+
+    Set<String> moduleNames = factoryOrder.orderFactories();
+    if (moduleNames.isEmpty()) {
+      throw new IllegalStateException("No modules found. When using java module system we need an explicit provides clause in module-info like:\n\n" +
+        " provides io.avaje.inject.spi.BeanContextFactory with org.example._di$BeanContextFactory;\n\n" +
+        " Otherwise perhaps using Gradle and IDEA but with a setup issue?" +
+        " Review IntelliJ Settings / Build / Build tools / Gradle - 'Build and run using' value and set that to 'Gradle'. " +
+        " Refer to https://avaje.io/inject#gradle");
+    }
+    log.debug("building context with modules {}", moduleNames);
+    Builder rootBuilder = Builder.newRootBuilder(suppliedBeans, enrichBeans);
+    for (BeanContextFactory factory : factoryOrder.factories()) {
+      rootBuilder.addChild(factory);
+    }
+
+    BeanContext beanContext = rootBuilder.build();
+    // entire graph built, fire postConstruct
+    beanContext.start();
+    if (shutdownHook) {
+      return new ShutdownAwareBeanContext(beanContext);
+    }
+    return beanContext;
+  }
+
+  /**
+   * Return the type that we map the supplied bean to.
+   */
+  private Class<?> suppliedType(Class<?> suppliedClass) {
+    Class<?> suppliedSuper = suppliedClass.getSuperclass();
+    if (Object.class.equals(suppliedSuper)) {
+      return suppliedClass;
+    } else {
+      // prefer to use the super type of the supplied bean (test double)
+      return suppliedSuper;
+    }
+  }
+
+  /**
+   * Internal shutdown hook.
+   */
+  private static class Hook extends Thread {
+
+    private final ShutdownAwareBeanContext context;
+
+    Hook(ShutdownAwareBeanContext context) {
+      this.context = context;
+    }
+
+    @Override
+    public void run() {
+      context.shutdown();
+    }
+  }
+
+  /**
+   * Proxy that handles shutdown hook registration and de-registration.
+   */
+  private static class ShutdownAwareBeanContext implements BeanContext {
+
+    private final BeanContext context;
+    private final Hook hook;
+    private boolean shutdown;
+
+    ShutdownAwareBeanContext(BeanContext context) {
+      this.context = context;
+      this.hook = new Hook(this);
+      Runtime.getRuntime().addShutdownHook(hook);
+    }
+
+    @Override
+    public String getName() {
+      return context.getName();
+    }
+
+    @Override
+    public String[] getProvides() {
+      return context.getProvides();
+    }
+
+    @Override
+    public String[] getDependsOn() {
+      return context.getDependsOn();
+    }
+
+    @Override
+    public <T> T getBean(Class<T> beanClass) {
+      return context.getBean(beanClass);
+    }
+
+    @Override
+    public <T> T getBean(Class<T> beanClass, String name) {
+      return context.getBean(beanClass, name);
+    }
+
+    @Override
+    public <T> BeanEntry<T> candidate(Class<T> type, String name) {
+      return context.candidate(type, name);
+    }
+
+    @Override
+    public List<Object> getBeansWithAnnotation(Class<?> annotation) {
+      return context.getBeansWithAnnotation(annotation);
+    }
+
+    @Override
+    public <T> List<T> getBeans(Class<T> interfaceType) {
+      return context.getBeans(interfaceType);
+    }
+
+    @Override
+    public <T> List<T> getBeansByPriority(Class<T> interfaceType) {
+      return context.getBeansByPriority(interfaceType);
+    }
+
+    @Override
+    public <T> List<T> getBeansByPriority(Class<T> interfaceType, Class<? extends Annotation> priority) {
+      return context.getBeansByPriority(interfaceType, priority);
+    }
+
+    @Override
+    public <T> List<T> sortByPriority(List<T> list) {
+      return context.sortByPriority(list);
+    }
+
+    @Override
+    public <T> List<T> sortByPriority(List<T> list, Class<? extends Annotation> priority) {
+      return context.sortByPriority(list, priority);
+    }
+
+    @Override
+    public void start() {
+      context.start();
+    }
+
+    @Override
+    public void close() {
+      synchronized (this) {
+        if (!shutdown) {
+          Runtime.getRuntime().removeShutdownHook(hook);
+        }
+        context.close();
+      }
+    }
+
+    /**
+     * Close via shutdown hook.
+     */
+    void shutdown() {
+      synchronized (this) {
+        shutdown = true;
+        close();
+      }
+    }
+  }
+
+  /**
+   * Helper to order the BeanContextFactory based on dependsOn.
+   */
+  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<BeanContextFactory> 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;
+      this.suppliedBeans = suppliedBeans;
+      this.ignoreMissingModuleDependencies = ignoreMissingModuleDependencies;
+    }
+
+    void add(BeanContextFactory 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);
+          }
+        }
+        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);
+          }
+        } else {
+          // queue it to process by dependency ordering
+          queue.add(wrappedFactory);
+        }
+      }
+    }
+
+    private boolean isEmpty(String[] values) {
+      return values == null || values.length == 0;
+    }
+
+    /**
+     * Return true of the factory (for the module) should be included.
+     */
+    private boolean includeModule(BeanContextFactory 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());
+    }
+
+    /**
+     * Order the factories returning the ordered list of module names.
+     */
+    Set<String> orderFactories() {
+      // push the 'no dependency' modules after the 'provides only' ones
+      // as this is more intuitive for the simple (only provides modules case)
+      for (FactoryState factoryState : queueNoDependencies) {
+        push(factoryState);
+      }
+      processQueue();
+      return moduleNames;
+    }
+
+    /**
+     * Return the list of factories in the order they should be built.
+     */
+    List<BeanContextFactory> factories() {
+      return factories;
+    }
+
+    /**
+     * Process the queue pushing the factories in order to satisfy dependencies.
+     */
+    private void processQueue() {
+
+      int count;
+      do {
+        count = processQueuedFactories();
+      } while (count > 0);
+
+      if (suppliedBeans || ignoreMissingModuleDependencies) {
+        // just push everything left assuming supplied beans
+        // will satisfy the required dependencies
+        for (FactoryState factoryState : queue) {
+          push(factoryState);
+        }
+
+      } 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()) {
+            if (!moduleNames.contains(depModuleName)) {
+              sb.append(String.format(" [%s]", depModuleName));
+            }
+          }
+        }
+
+        sb.append(". Modules that were loaded ok are:").append(moduleNames);
+        sb.append(". Consider using BeanContextBuilder.withIgnoreMissingModuleDependencies() or BeanContextBuilder.withSuppliedBeans(...)");
+        throw new IllegalStateException(sb.toString());
+      }
+    }
+
+    /**
+     * Process the queued factories pushing them when all their (module) dependencies
+     * are satisfied.
+     * <p>
+     * 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()) {
+        FactoryState factory = it.next();
+        if (satisfiedDependencies(factory)) {
+          // push the factory onto the build order
+          it.remove();
+          push(factory);
+          count++;
+        }
+      }
+      return count;
+    }
+
+    /**
+     * 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);
+        if (factories == null || !factories.allPushed()) {
+          return false;
+        }
+      }
+      return true;
+    }
+  }
+
+  /**
+   * Wrapper on Factory holding the pushed state.
+   */
+  private static class FactoryState {
+
+    private final BeanContextFactory factory;
+    private boolean pushed;
+
+    private FactoryState(BeanContextFactory factory) {
+      this.factory = factory;
+    }
+
+    /**
+     * Set when factory is pushed onto the build/wiring order.
+     */
+    void setPushed() {
+      this.pushed = true;
+    }
+
+    boolean isPushed() {
+      return pushed;
+    }
+
+    BeanContextFactory getFactory() {
+      return factory;
+    }
+
+    String getName() {
+      return factory.getName();
+    }
+
+    String[] getDependsOn() {
+      return factory.getDependsOn();
+    }
+  }
+
+  /**
+   * List of factories for a given name or feature.
+   */
+  private static class FactoryList {
+
+    private final List<FactoryState> factories = new ArrayList<>();
+
+    void add(FactoryState factory) {
+      factories.add(factory);
+    }
+
+    /**
+     * Return true if all factories here have been pushed onto the build order.
+     */
+    boolean allPushed() {
+      for (FactoryState factory : factories) {
+        if (!factory.isPushed()) {
+          return false;
+        }
+      }
+      return true;
+    }
+  }
+
+}
diff --git a/inject/src/main/java/io/avaje/inject/SystemContext.java b/inject/src/main/java/io/avaje/inject/SystemContext.java
index 1afb11ca..05364234 100644
--- a/inject/src/main/java/io/avaje/inject/SystemContext.java
+++ b/inject/src/main/java/io/avaje/inject/SystemContext.java
@@ -45,7 +45,7 @@ public class SystemContext {
   private static final BeanContext rootContext = init();
 
   private static BeanContext init() {
-    return new BeanContextBuilder().build();
+    return BeanContext.newBuilder().build();
   }
 
   private SystemContext() {


Test patch:
diff --git a/inject-test/src/test/java/io/avaje/inject/BeanContextBuilderTest.java b/inject-test/src/test/java/io/avaje/inject/BeanContextBuilderTest.java
index 23159fcf..8fc7e752 100644
--- a/inject-test/src/test/java/io/avaje/inject/BeanContextBuilderTest.java
+++ b/inject-test/src/test/java/io/avaje/inject/BeanContextBuilderTest.java
@@ -15,7 +15,7 @@ public class BeanContextBuilderTest {
   @Test
   public void noDepends() {
 
-    BeanContextBuilder.FactoryOrder factoryOrder = new BeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
+    DBeanContextBuilder.FactoryOrder factoryOrder = new DBeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
     factoryOrder.add(bc("1", null, null));
     factoryOrder.add(bc("2", null, null));
     factoryOrder.add(bc("3", null, null));
@@ -27,7 +27,7 @@ public class BeanContextBuilderTest {
   @Test
   public void name_depends() {
 
-    BeanContextBuilder.FactoryOrder factoryOrder = new BeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
+    DBeanContextBuilder.FactoryOrder factoryOrder = new DBeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
     factoryOrder.add(bc("two", null, "one"));
     factoryOrder.add(bc("one", null, null));
     factoryOrder.orderFactories();
@@ -38,7 +38,7 @@ public class BeanContextBuilderTest {
   @Test
   public void name_depends4() {
 
-    BeanContextBuilder.FactoryOrder factoryOrder = new BeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
+    DBeanContextBuilder.FactoryOrder factoryOrder = new DBeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
     factoryOrder.add(bc("1", null, "3"));
     factoryOrder.add(bc("2", null, "4"));
     factoryOrder.add(bc("3", null, "4"));
@@ -52,7 +52,7 @@ public class BeanContextBuilderTest {
   @Test
   public void nameFeature_depends() {
 
-    BeanContextBuilder.FactoryOrder factoryOrder = new BeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
+    DBeanContextBuilder.FactoryOrder factoryOrder = new DBeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
     factoryOrder.add(bc("1", "a", "3"));
     factoryOrder.add(bc("2", null, "4,a"));
     factoryOrder.add(bc("3", null, "4"));
@@ -66,7 +66,7 @@ public class BeanContextBuilderTest {
   @Test
   public void feature_depends() {
 
-    BeanContextBuilder.FactoryOrder factoryOrder = new BeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
+    DBeanContextBuilder.FactoryOrder factoryOrder = new DBeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
     factoryOrder.add(bc("two", null, "myfeature"));
     factoryOrder.add(bc("one", "myfeature", null));
     factoryOrder.orderFactories();
@@ -77,7 +77,7 @@ public class BeanContextBuilderTest {
   @Test
   public void feature_depends2() {
 
-    BeanContextBuilder.FactoryOrder factoryOrder = new BeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
+    DBeanContextBuilder.FactoryOrder factoryOrder = new DBeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
     factoryOrder.add(bc("two", null, "myfeature"));
     factoryOrder.add(bc("one", "myfeature", null));
     factoryOrder.add(bc("three", "myfeature", null));
diff --git a/inject-test/src/test/java/org/example/coffee/BeanContextBuilderAddTest.java b/inject-test/src/test/java/org/example/coffee/BeanContextBuilderAddTest.java
index 8ba49e5f..45341b51 100644
--- a/inject-test/src/test/java/org/example/coffee/BeanContextBuilderAddTest.java
+++ b/inject-test/src/test/java/org/example/coffee/BeanContextBuilderAddTest.java
@@ -1,7 +1,6 @@
 package org.example.coffee;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
@@ -16,7 +15,7 @@ public class BeanContextBuilderAddTest {
     assertThrows(IllegalStateException.class, () -> {
       TDPump testDoublePump = new TDPump();
 
-      try (BeanContext context = new BeanContextBuilder()
+      try (BeanContext context = BeanContext.newBuilder()
         .withBeans(testDoublePump)
         // our module is "org.example.coffee"
         // so this effectively includes no modules
@@ -35,7 +34,7 @@ public class BeanContextBuilderAddTest {
 
     TDPump testDoublePump = new TDPump();
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withBeans(testDoublePump)
       .withModules("org.example")
       .build()) {
@@ -53,7 +52,7 @@ public class BeanContextBuilderAddTest {
 
     TDPump testDoublePump = new TDPump();
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withBeans(testDoublePump)
       .build()) {
 
@@ -70,7 +69,7 @@ public class BeanContextBuilderAddTest {
 
     Pump mock = Mockito.mock(Pump.class);
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withBean(Pump.class, mock)
       .build()) {
 
diff --git a/inject-test/src/test/java/org/example/coffee/BeanContext_Builder_mockitoSpyTest.java b/inject-test/src/test/java/org/example/coffee/BeanContext_Builder_mockitoSpyTest.java
index 135edc71..f3b29a05 100644
--- a/inject-test/src/test/java/org/example/coffee/BeanContext_Builder_mockitoSpyTest.java
+++ b/inject-test/src/test/java/org/example/coffee/BeanContext_Builder_mockitoSpyTest.java
@@ -1,7 +1,6 @@
 package org.example.coffee;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.example.coffee.factory.SomeImpl;
 import org.example.coffee.factory.SomeImplBean;
 import org.example.coffee.factory.Unused;
@@ -30,7 +29,7 @@ public class BeanContext_Builder_mockitoSpyTest {
     Pump pump = mock(Pump.class);
     Grinder grinder = mock(Grinder.class);
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withBeans(pump, grinder)
       .build()) {
 
@@ -51,7 +50,7 @@ public class BeanContext_Builder_mockitoSpyTest {
   @Test
   public void withMockitoSpy_noSetup_expect_spyUsed() {
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withSpy(Pump.class)
       .build()) {
 
@@ -67,7 +66,7 @@ public class BeanContext_Builder_mockitoSpyTest {
   @Test
   public void withMockitoSpy_postLoadSetup_expect_spyUsed() {
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withSpy(Pump.class)
       .withSpy(Grinder.class)
       .build()) {
@@ -90,7 +89,7 @@ public class BeanContext_Builder_mockitoSpyTest {
   @Test
   public void withMockitoSpy_expect_spyUsed() {
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withSpy(Pump.class, pump -> {
         // setup the spy
         doNothing().when(pump).pumpWater();
@@ -113,7 +112,7 @@ public class BeanContext_Builder_mockitoSpyTest {
   @Test
   public void withMockitoSpy_whenPrimary_expect_spyUsed() {
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withSpy(PEmailer.class) // has a primary
       .build()) {
 
@@ -128,7 +127,7 @@ public class BeanContext_Builder_mockitoSpyTest {
   @Test
   public void withMockitoSpy_whenOnlySecondary_expect_spyUsed() {
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withSpy(Widget.class) // only secondary
       .build()) {
 
@@ -148,7 +147,7 @@ public class BeanContext_Builder_mockitoSpyTest {
   @Test
   public void withMockitoSpy_whenSecondary_expect_spyUsed() {
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withSpy(Something.class) // has a secondary and a normal
       .build()) {
 
@@ -172,7 +171,7 @@ public class BeanContext_Builder_mockitoSpyTest {
 
     AtomicReference<Grinder> mock = new AtomicReference<>();
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withMock(Pump.class)
       .withMock(Grinder.class, grinder -> {
         // setup the mock
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 b6a0a6bd..49a1b255 100644
--- a/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
+++ b/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
@@ -1,7 +1,6 @@
 package org.example.coffee;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import io.avaje.inject.SystemContext;
 import org.example.coffee.core.DuperPump;
 import org.junit.jupiter.api.Test;
@@ -26,7 +25,7 @@ public class CoffeeMakerTest {
   @Test
   public void makeIt_via_BootContext_withNoShutdownHook() {
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withNoShutdownHook()
       .build()) {
 
@@ -38,8 +37,7 @@ public class CoffeeMakerTest {
   @Test
   public void makeIt_via_BootContext() {
 
-    try (BeanContext context = new BeanContextBuilder().build()) {
-
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       String makeIt = context.getBean(CoffeeMaker.class).makeIt();
       assertThat(makeIt).isEqualTo("done");
     }
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 97b03e1d..6e16f548 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 {
   }
 
   BeanContext build() {
-    BeanContextBuilder bootContext = new BeanContextBuilder();
+    BeanContextBuilder bootContext = BeanContext.newBuilder();
     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 2d20fb9f..295b5b23 100644
--- a/inject-test/src/test/java/org/example/coffee/ExtensionExampleTest.java
+++ b/inject-test/src/test/java/org/example/coffee/ExtensionExampleTest.java
@@ -21,7 +21,7 @@ public class ExtensionExampleTest {
     Class cls0 = Widget.class;
     Class<?> cls1 = SEmailer.class;
 
-    BeanContextBuilder bootContext = new BeanContextBuilder()
+    BeanContextBuilder bootContext = BeanContext.newBuilder()
       .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 e30f1306..cd5521b4 100644
--- a/inject-test/src/test/java/org/example/coffee/FactoryTest.java
+++ b/inject-test/src/test/java/org/example/coffee/FactoryTest.java
@@ -1,7 +1,6 @@
 package org.example.coffee;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.example.coffee.factory.BFact;
 import org.junit.jupiter.api.Test;
 
@@ -12,9 +11,7 @@ public class FactoryTest {
   @Test
   public void test() {
 
-    try (BeanContext context = new BeanContextBuilder()
-      .build()) {
-
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       BFact bean = context.getBean(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 765a6fcb..db5263b0 100644
--- a/inject-test/src/test/java/org/example/coffee/InjectListTest.java
+++ b/inject-test/src/test/java/org/example/coffee/InjectListTest.java
@@ -1,7 +1,6 @@
 package org.example.coffee;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.example.coffee.list.CombinedSetSomei;
 import org.example.coffee.list.CombinedSomei;
 import org.junit.jupiter.api.Test;
@@ -14,7 +13,7 @@ public class InjectListTest {
 
   @Test
   public void test() {
-    try (BeanContext context = new BeanContextBuilder().build()) {
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       CombinedSomei bean = context.getBean(CombinedSomei.class);
       List<String> somes = bean.lotsOfSomes();
       assertThat(somes).containsOnly("a", "b", "a2");
@@ -23,7 +22,7 @@ public class InjectListTest {
 
   @Test
   public void test_set() {
-    try (BeanContext context = new BeanContextBuilder().build()) {
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       CombinedSetSomei bean = context.getBean(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 e3887cc6..d4a502e5 100644
--- a/inject-test/src/test/java/org/example/coffee/ProviderTest.java
+++ b/inject-test/src/test/java/org/example/coffee/ProviderTest.java
@@ -1,7 +1,6 @@
 package org.example.coffee;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.example.coffee.provider.ProvOther;
 import org.example.coffee.provider.ProvOther2;
 import org.junit.jupiter.api.Test;
@@ -13,7 +12,7 @@ public class ProviderTest {
   @Test
   public void test() {
 
-    try (BeanContext context = new BeanContextBuilder().build()) {
+    try (BeanContext context = BeanContext.newBuilder().build()) {
 
       ProvOther bean = context.getBean(ProvOther.class);
       String other = bean.other();
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 49fc32b0..926a45b7 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
@@ -1,7 +1,6 @@
 package org.example.coffee.circular;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -10,8 +9,7 @@ class CircularDependencyTest {
 
   @Test
   void wire() {
-    try (BeanContext context = new BeanContextBuilder()
-      .build()) {
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       assertThat(context.getBean(CircA.class)).isNotNull();
       assertThat(context.getBean(CircB.class)).isNotNull();
       assertThat(context.getBean(CircC.class)).isNotNull();
diff --git a/inject-test/src/test/java/org/example/coffee/factory/AFactTest.java b/inject-test/src/test/java/org/example/coffee/factory/AFactTest.java
index 6d9a1ec6..2a08a033 100644
--- a/inject-test/src/test/java/org/example/coffee/factory/AFactTest.java
+++ b/inject-test/src/test/java/org/example/coffee/factory/AFactTest.java
@@ -1,7 +1,6 @@
 package org.example.coffee.factory;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -12,9 +11,7 @@ public class AFactTest {
   public void postConstruct() {
 
     AFact bean;
-    try (BeanContext context = new BeanContextBuilder()
-      .build()) {
-
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       bean = context.getBean(AFact.class);
 
       assertThat(bean.getCountConstruct()).isEqualTo(1);
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 7a64b8c9..b9c5b092 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
@@ -1,7 +1,6 @@
 package org.example.coffee.factory;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -12,8 +11,7 @@ public class BFactTest {
   public void getCountInit() {
 
     BFact bFact;
-    try (BeanContext context = new BeanContextBuilder().build()) {
-
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       bFact = context.getBean(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 6c654163..df9fa1a8 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
@@ -1,7 +1,6 @@
 package org.example.coffee.factory;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -12,7 +11,7 @@ public class ConfigurationTest {
   public void getCountInit() {
 
     Configuration configuration;
-    try (BeanContext context = new BeanContextBuilder().build()) {
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       configuration = context.getBean(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 e8694b6e..53db6b04 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
@@ -1,7 +1,6 @@
 package org.example.coffee.factory;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -11,7 +10,7 @@ public class MultipleOtherThingsTest {
   @Test
   public void test() {
 
-    try (BeanContext context = new BeanContextBuilder().build()) {
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       final MultipleOtherThings combined = context.getBean(MultipleOtherThings.class);
       assertEquals("blue", combined.blue());
       assertEquals("red", combined.red());
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 99c44ce8..8f57798d 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
@@ -1,7 +1,6 @@
 package org.example.coffee.factory;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -10,7 +9,7 @@ public class MyFactoryTest {
 
   @Test
   public void methodsCalled() {
-    try (BeanContext context = new BeanContextBuilder().build()) {
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       final MyFactory myFactory = context.getBean(MyFactory.class);
       assertThat(myFactory.methodsCalled()).contains("|useCFact", "|anotherCFact");
     }
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 efe257f9..ad94e5d0 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
@@ -13,10 +13,10 @@ public class AppleServiceTest {
   @Test
   public void test_spyWithFieldInjection() {
 
-    BeanContextBuilder bootContext = new BeanContextBuilder();
-    bootContext.withSpy(AppleService.class);
+    BeanContextBuilder contextBuilder = BeanContext.newBuilder();
+    contextBuilder.withSpy(AppleService.class);
 
-    try (BeanContext beanContext = bootContext.build()) {
+    try (BeanContext beanContext = contextBuilder.build()) {
 
       AppleService appleService = beanContext.getBean(AppleService.class);
 
@@ -37,9 +37,7 @@ public class AppleServiceTest {
   @Test
   public void test_whenNoMockOrSpy() {
 
-    BeanContextBuilder bootContext = new BeanContextBuilder();
-
-    try (BeanContext beanContext = bootContext.build()) {
+    try (BeanContext beanContext = BeanContext.newBuilder().build()) {
 
       AppleService appleService = beanContext.getBean(AppleService.class);
 
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 0471613b..3ba15a9f 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
@@ -1,7 +1,6 @@
 package org.example.coffee.generic;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import io.avaje.inject.SystemContext;
 import org.junit.jupiter.api.Test;
 
@@ -23,7 +22,7 @@ public class HazManagerTest {
   @Test
   public void fin_with_mockHaz() {
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withMock(HazRepo.class)
       .build()) {
 
@@ -37,7 +36,7 @@ public class HazManagerTest {
   @Test
   public void find_with_stubHazUsingMockito() {
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withMock(HazRepo.class, hazRepo -> {
         when(hazRepo.findById(anyLong())).thenReturn(new Haz(-23L));
       })
@@ -55,7 +54,7 @@ public class HazManagerTest {
 
     TDHazRepo testDouble = new TDHazRepo();
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withBeans(testDouble)
       .build()) {
 
@@ -78,7 +77,7 @@ public class HazManagerTest {
   /**
    * Test double for HazRepo - nice when we want interesting test behaviour.
    */
-  class TDHazRepo extends HazRepo {
+  static class TDHazRepo extends HazRepo {
 
     long id = 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 4f1fa3aa..77e29038 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
@@ -1,7 +1,6 @@
 package org.example.coffee.grind;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -12,7 +11,7 @@ public class AMusherTest {
   public void getCountInit() {
 
     AMusher aMusher;
-    try (BeanContext context = new BeanContextBuilder().build()) {
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       aMusher = context.getBean(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 478c731e..d222cd0a 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
@@ -1,7 +1,6 @@
 package org.example.coffee.grind;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -12,7 +11,7 @@ public class BMusherTest {
   public void init() {
 
     BMusher bMusher;
-    try (BeanContext context = new BeanContextBuilder().build()) {
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       bMusher = context.getBean(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 2ffb5cf7..731193ac 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
@@ -1,7 +1,6 @@
 package org.example.coffee.qualifier;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -11,7 +10,7 @@ public class StoreManagerWithFieldQualifierTest {
   @Test
   public void test() {
 
-    try (BeanContext context = new BeanContextBuilder().build()) {
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       StoreManagerWithFieldQualifier manager = context.getBean(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 5608dc1f..073ccb58 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
@@ -1,7 +1,6 @@
 package org.example.coffee.qualifier;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -11,7 +10,7 @@ public class StoreManagerWithNamedTest {
   @Test
   public void test() {
 
-    try (BeanContext context = new BeanContextBuilder().build()) {
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       StoreManagerWithNamed manager = context.getBean(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 0b7d15cb..1df23183 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
@@ -1,7 +1,6 @@
 package org.example.coffee.qualifier;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -11,7 +10,7 @@ public class StoreManagerWithQualifierTest {
   @Test
   public void test() {
 
-    try (BeanContext context = new BeanContextBuilder().build()) {
+    try (BeanContext context = BeanContext.newBuilder().build()) {
       StoreManagerWithQualifier manager = context.getBean(StoreManagerWithQualifier.class);
       String store = manager.store();
       assertThat(store).isEqualTo("blue");
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 d504162d..94167b91 100644
--- a/inject-test/src/test/java/org/example/iface/NestedInterfaceTest.java
+++ b/inject-test/src/test/java/org/example/iface/NestedInterfaceTest.java
@@ -1,7 +1,6 @@
 package org.example.iface;
 
 import io.avaje.inject.BeanContext;
-import io.avaje.inject.BeanContextBuilder;
 import io.avaje.inject.SystemContext;
 import org.junit.jupiter.api.Test;
 
@@ -23,7 +22,7 @@ public class NestedInterfaceTest {
   @Test
   void test_provided() {
 
-    try (BeanContext context = new BeanContextBuilder()
+    try (BeanContext context = BeanContext.newBuilder()
       .withMock(Some.Nested.class, nested -> when(nested.doNested()).thenReturn("myMock"))
       .build()) {
 
diff --git a/inject/src/test/java/io/avaje/inject/BeanContextBuilderTest.java b/inject/src/test/java/io/avaje/inject/BeanContextBuilderTest.java
index 23159fcf..8fc7e752 100644
--- a/inject/src/test/java/io/avaje/inject/BeanContextBuilderTest.java
+++ b/inject/src/test/java/io/avaje/inject/BeanContextBuilderTest.java
@@ -15,7 +15,7 @@ public class BeanContextBuilderTest {
   @Test
   public void noDepends() {
 
-    BeanContextBuilder.FactoryOrder factoryOrder = new BeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
+    DBeanContextBuilder.FactoryOrder factoryOrder = new DBeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
     factoryOrder.add(bc("1", null, null));
     factoryOrder.add(bc("2", null, null));
     factoryOrder.add(bc("3", null, null));
@@ -27,7 +27,7 @@ public class BeanContextBuilderTest {
   @Test
   public void name_depends() {
 
-    BeanContextBuilder.FactoryOrder factoryOrder = new BeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
+    DBeanContextBuilder.FactoryOrder factoryOrder = new DBeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
     factoryOrder.add(bc("two", null, "one"));
     factoryOrder.add(bc("one", null, null));
     factoryOrder.orderFactories();
@@ -38,7 +38,7 @@ public class BeanContextBuilderTest {
   @Test
   public void name_depends4() {
 
-    BeanContextBuilder.FactoryOrder factoryOrder = new BeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
+    DBeanContextBuilder.FactoryOrder factoryOrder = new DBeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
     factoryOrder.add(bc("1", null, "3"));
     factoryOrder.add(bc("2", null, "4"));
     factoryOrder.add(bc("3", null, "4"));
@@ -52,7 +52,7 @@ public class BeanContextBuilderTest {
   @Test
   public void nameFeature_depends() {
 
-    BeanContextBuilder.FactoryOrder factoryOrder = new BeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
+    DBeanContextBuilder.FactoryOrder factoryOrder = new DBeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
     factoryOrder.add(bc("1", "a", "3"));
     factoryOrder.add(bc("2", null, "4,a"));
     factoryOrder.add(bc("3", null, "4"));
@@ -66,7 +66,7 @@ public class BeanContextBuilderTest {
   @Test
   public void feature_depends() {
 
-    BeanContextBuilder.FactoryOrder factoryOrder = new BeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
+    DBeanContextBuilder.FactoryOrder factoryOrder = new DBeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
     factoryOrder.add(bc("two", null, "myfeature"));
     factoryOrder.add(bc("one", "myfeature", null));
     factoryOrder.orderFactories();
@@ -77,7 +77,7 @@ public class BeanContextBuilderTest {
   @Test
   public void feature_depends2() {
 
-    BeanContextBuilder.FactoryOrder factoryOrder = new BeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
+    DBeanContextBuilder.FactoryOrder factoryOrder = new DBeanContextBuilder.FactoryOrder(Collections.emptySet(), true, true);
     factoryOrder.add(bc("two", null, "myfeature"));
     factoryOrder.add(bc("one", "myfeature", null));
     factoryOrder.add(bc("three", "myfeature", null));