diff --git a/.gitignore b/.gitignore
index 92ed340a..d3b5e517 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,6 @@ build/
 inject-generator/avaje-inject
 inject-generator/avaje-inject-generator
 inject-generator/avaje-processors.txt
+*.csv
+inject-generator/util/events/ListString$Publisher$DI.java
+inject-generator/util/events/ListString$Publisher.java
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/async/BackgroundBean.java b/blackbox-test-inject/src/main/java/org/example/myapp/async/BackgroundBean.java
new file mode 100644
index 00000000..4d5bbea4
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/async/BackgroundBean.java
@@ -0,0 +1,31 @@
+package org.example.myapp.async;
+
+import java.time.Instant;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.avaje.inject.AsyncBean;
+import io.avaje.lang.Nullable;
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+import jakarta.inject.Singleton;
+
+@AsyncBean
+@Singleton
+@Named("single")
+public class BackgroundBean {
+
+  final Instant initTime;
+  final String threadName = Thread.currentThread().getName();
+
+  public BackgroundBean(@Nullable AtomicInteger intyAtomic) throws InterruptedException {
+    this.initTime = Instant.now();
+
+    if (intyAtomic != null) {
+      intyAtomic.incrementAndGet();
+    }
+
+    Thread.sleep(200);
+  }
+}
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/async/BackgroundBeanFactory.java b/blackbox-test-inject/src/main/java/org/example/myapp/async/BackgroundBeanFactory.java
new file mode 100644
index 00000000..883f86bd
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/async/BackgroundBeanFactory.java
@@ -0,0 +1,21 @@
+package org.example.myapp.async;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.avaje.inject.AsyncBean;
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+import io.avaje.lang.Nullable;
+import jakarta.inject.Named;
+
+@Factory
+@AsyncBean
+public class BackgroundBeanFactory {
+
+  @Bean
+  @Named("factory")
+  BackgroundBean lazyInt(@Nullable AtomicInteger intyAtomic) throws InterruptedException {
+    System.out.println("StartedInit BackgroundBean() " + Thread.currentThread().getName());
+    return new BackgroundBean(intyAtomic);
+  }
+}
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/async/MyUseOfBackground.java b/blackbox-test-inject/src/main/java/org/example/myapp/async/MyUseOfBackground.java
new file mode 100644
index 00000000..f9bef939
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/async/MyUseOfBackground.java
@@ -0,0 +1,16 @@
+package org.example.myapp.async;
+
+import io.avaje.inject.AsyncBean;
+import io.avaje.inject.Component;
+import jakarta.inject.Named;
+
+@AsyncBean
+@Component
+public class MyUseOfBackground {
+
+  private final BackgroundBean backgroundBean;
+
+  public MyUseOfBackground(@Named("single") BackgroundBean backgroundBean) {
+    this.backgroundBean = backgroundBean;
+  }
+}
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/async/AsyncTest.java b/blackbox-test-inject/src/test/java/org/example/myapp/async/AsyncTest.java
new file mode 100644
index 00000000..7942c998
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/async/AsyncTest.java
@@ -0,0 +1,49 @@
+package org.example.myapp.async;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.Test;
+
+import io.avaje.inject.BeanScope;
+
+class AsyncTest {
+
+  @Test
+  void test() {
+    var start = Instant.now();
+    var inty = new AtomicInteger();
+    try (var scope = BeanScope.builder().bean(AtomicInteger.class, inty).build()) {
+
+      // the async beans shouldn't slowdown initialization
+      assertThat(Duration.between(start, Instant.now()).toMillis()).isLessThan(300);
+
+      // prove it's not just lazy
+      var beforeGet = Instant.now();
+      var bean = scope.get(BackgroundBean.class, "single");
+      assertThat(inty.get()).isEqualTo(2);
+      assertThat(bean.initTime.isBefore(beforeGet)).isTrue();
+      assertThat(bean.threadName).isNotEqualTo(Thread.currentThread().getName());
+    }
+  }
+
+  @Test
+  void testFactory() {
+    var start = Instant.now();
+    var inty = new AtomicInteger();
+    try (var scope = BeanScope.builder().bean(AtomicInteger.class, inty).build()) {
+      // the async beans shouldn't slowdown initialization
+      assertThat(Duration.between(start, Instant.now()).toMillis()).isLessThan(300);
+
+      var bean = scope.get(BackgroundBean.class, "factory");
+      // this works on my local but not on the CI for some unknown reason.
+      // var beforeGet = Instant.now();
+      // assertThat(bean.initTime.isBefore(beforeGet)).isTrue();
+      assertThat(inty.get()).isEqualTo(2);
+      assertThat(bean.threadName).isNotEqualTo(Thread.currentThread().getName());
+    }
+  }
+}
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 458d4239..6ed3e78b 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
@@ -7,6 +7,7 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 import java.util.stream.Stream;
 
 import javax.lang.model.element.Element;
@@ -38,6 +39,7 @@ final class BeanReader {
   private final boolean prototype;
   private final boolean primary;
   private final boolean secondary;
+  private final boolean async;
   private final boolean lazy;
   private final boolean proxy;
   private final BeanAspects aspects;
@@ -58,6 +60,7 @@ final class BeanReader {
     this.primary = PrimaryPrism.isPresent(beanType);
     this.secondary = !primary && SecondaryPrism.isPresent(beanType);
     this.lazy = !FactoryPrism.isPresent(beanType) && LazyPrism.isPresent(beanType);
+    this.async = !FactoryPrism.isPresent(beanType) && AsyncBeanPrism.isPresent(beanType);
     final var beantypes = BeanTypesPrism.getOptionalOn(beanType);
     beantypes.ifPresent(p -> Util.validateBeanTypes(beanType, p.value()));
     this.typeReader =
@@ -128,13 +131,17 @@ BeanAspects aspects() {
   }
 
   boolean registerProvider() {
-    return prototype || lazy;
+    return prototype || async || lazy;
   }
 
   boolean lazy() {
     return lazy;
   }
 
+  boolean async() {
+    return async;
+  }
+
   boolean importedComponent() {
     return importedComponent;
   }
@@ -304,7 +311,7 @@ void buildAddFor(Append writer) {
   }
 
   void buildRegister(Append writer) {
-    if (prototype || lazy) {
+    if (prototype || lazy || async) {
       return;
     }
     writer.indent("      ");
@@ -349,15 +356,20 @@ void prototypePostConstruct(Append writer, String indent) {
 
   private void lifeCycleNotSupported(String lifecycle) {
     if (registerProvider()) {
-      logError(
-        beanType,
-        "%s scoped bean does not support the %s lifecycle method",
-        prototype ? "@Prototype" : "@Lazy",
-        lifecycle);
+      String scope;
+      if (lazy) scope = "@Lazy";
+      if (async) scope = "@AsyncBean";
+      else scope = "@Prototype";
+
+      logError(beanType, "%s scoped bean does not support the %s lifecycle method", scope, lifecycle);
     }
   }
 
   private Set<String> importTypes() {
+    importTypes.add(type);
+    if (async) {
+      importTypes.add(CompletableFuture.class.getCanonicalName());
+    }
     importTypes.add(type);
     typeReader.extraImports(importTypes);
     requestParams.addImports(importTypes);
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
index fb52c460..f2f7f622 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
@@ -7,6 +7,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 
 import static io.avaje.inject.generator.Constants.CONDITIONAL_DEPENDENCY;
 import static io.avaje.inject.generator.ProcessingContext.asElement;
@@ -21,6 +22,7 @@ final class MethodReader {
   private final boolean prototype;
   private final boolean primary;
   private final boolean secondary;
+  private final boolean async;
   private final boolean lazy;
   private final String returnTypeRaw;
   private final UType genericType;
@@ -49,9 +51,11 @@ final class MethodReader {
       prototype = PrototypePrism.isPresent(element);
       primary = PrimaryPrism.isPresent(element);
       secondary = SecondaryPrism.isPresent(element);
+      async = AsyncBeanPrism.isPresent(element) || AsyncBeanPrism.isPresent(element.getEnclosingElement());
       lazy = LazyPrism.isPresent(element) || LazyPrism.isPresent(element.getEnclosingElement());
       conditions.readAll(element);
     } else {
+      async = false;
       prototype = false;
       primary = false;
       secondary = false;
@@ -103,8 +107,8 @@ final class MethodReader {
       this.initMethod = lifecycleReader.initMethod();
       this.destroyMethod = lifecycleReader.destroyMethod();
     }
-    if (lazy && prototype) {
-      APContext.logError("Cannot use both @Lazy and @Prototype");
+    if ((async || lazy) && prototype) {
+      APContext.logError("Cannot use both @AsyncBean/@Lazy and @Prototype");
     }
   }
 
@@ -230,7 +234,10 @@ void builderAddBeanProvider(Append writer) {
       writer.append(".asSecondary()");
     }
 
-    writer.indent(".registerProvider(() -> {").eol();
+    writer
+        .indent(".registerProvider(")
+        .append("%s() -> {", async ? "CompletableFuture.supplyAsync(" : "")
+        .eol();
 
     startTry(writer, "  ");
     writer.indent(indent).append("  return ");
@@ -243,7 +250,7 @@ void builderAddBeanProvider(Append writer) {
     }
     writer.append(");").eol();
     endTry(writer, "  ");
-    writer.indent(indent).append("  });").eol();
+    writer.indent(indent).append("  }%s);", async ? ")::join" : "").eol();
     writer.indent(indent).append("}").eol();
   }
 
@@ -340,6 +347,10 @@ void addImports(ImportTypeMap importTypes) {
     if (optionalType) {
       importTypes.add(Constants.OPTIONAL);
     }
+
+    if (async) {
+      importTypes.add(CompletableFuture.class.getCanonicalName());
+    }
     conditions.addImports(importTypes);
   }
 
@@ -429,6 +440,10 @@ boolean isProtoType() {
     return prototype && !Util.isProvider(returnTypeRaw);
   }
 
+  boolean isAsync() {
+    return async;
+  }
+
   boolean isLazy() {
     return lazy;
   }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
index 17a048b7..93639335 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
@@ -14,6 +14,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 
 import javax.lang.model.type.TypeKind;
 import javax.tools.JavaFileObject;
@@ -143,7 +144,7 @@ private void writeFactoryBeanMethod(MethodReader method) {
     method.buildConditional(writer);
     method.buildAddFor(writer);
     method.builderGetFactory(writer, beanReader.hasConditions());
-    if (method.isLazy() || method.isProtoType() || method.isUseProviderForSecondary()) {
+    if (method.isAsync() || method.isLazy() || method.isProtoType() || method.isUseProviderForSecondary()) {
       method.builderAddBeanProvider(writer);
     } else {
       method.startTry(writer);
@@ -177,7 +178,9 @@ private void writeAddFor(MethodReader constructor) {
       indent += "  ";
 
       final String registerProvider;
-      if (beanReader.lazy()) {
+      if (beanReader.async()) {
+        registerProvider = "registerProvider(CompletableFuture.supplyAsync";
+      } else if (beanReader.lazy()) {
         registerProvider = "registerProvider";
       } else {
         registerProvider = "asPrototype().registerProvider";
@@ -196,18 +199,18 @@ private void writeAddFor(MethodReader constructor) {
       beanReader.prototypePostConstruct(writer, indent);
       writer.indent("        return bean;").eol();
       if (!constructor.methodThrows()) {
-        writer.indent("      });").eol();
+        writer.indent("      }").append(beanReader.async() ? ")::join);" : ");").eol();
       }
     }
     writeObserveMethods();
     constructor.endTry(writer);
+    writer.append("    }");
 
     if (beanReader.registerProvider() && constructor.methodThrows()) {
-      writer.append("     }");
-      writer.append(");").eol();
+      writer.append("%s);", beanReader.async() ? ")::join" : "").eol();
+      writer.append("    }");
     }
 
-    writer.append("    }");
     writer.eol();
   }
 
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java b/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java
index 3401e29f..7e9ce0ab 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java
@@ -3,6 +3,7 @@
 @GeneratePrism(value = Aspect.Import.class, name = "AspectImportPrism")
 @GeneratePrism(Assisted.class)
 @GeneratePrism(AssistFactory.class)
+@GeneratePrism(AsyncBean.class)
 @GeneratePrism(Bean.class)
 @GeneratePrism(Component.class)
 @GeneratePrism(Component.Import.class)
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/async/BackgroundBean.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/async/BackgroundBean.java
new file mode 100644
index 00000000..fe07f287
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/async/BackgroundBean.java
@@ -0,0 +1,12 @@
+package io.avaje.inject.generator.models.valid.async;
+
+import io.avaje.inject.AsyncBean;
+import jakarta.inject.Inject;
+import jakarta.inject.Provider;
+import jakarta.inject.Singleton;
+
+@Singleton
+@AsyncBean
+public class BackgroundBean {
+  @Inject Provider<Integer> intProvider;
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/async/BackgroundBeanFactory.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/async/BackgroundBeanFactory.java
new file mode 100644
index 00000000..d4c283f4
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/async/BackgroundBeanFactory.java
@@ -0,0 +1,18 @@
+package io.avaje.inject.generator.models.valid.async;
+
+import io.avaje.inject.AsyncBean;
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+import jakarta.inject.Named;
+
+@Factory
+@AsyncBean
+public class BackgroundBeanFactory {
+
+  @Bean
+  @Named("factory")
+  BackgroundBean lazyInt() throws InterruptedException {
+
+    return new BackgroundBean();
+  }
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyFactory.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyFactory.java
index 931e6723..9dfb8d29 100644
--- a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyFactory.java
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyFactory.java
@@ -9,7 +9,7 @@
 public class LazyFactory {
 
   @Bean
-  Integer lazyInt() {
+  Integer lazyInt() throws Exception {
     return 0;
   }
 }
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 531f3872..200a418a 100644
--- a/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
+++ b/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
@@ -84,7 +84,7 @@ void beanScope_all_superClasses() {
         .findFirst().orElse(null);
 
       assertThat(inhEntry.keys())
-        .containsExactly(name(InhOne.class), name(InhBase.class), name(InhBaseBase.class),
+        .containsExactlyInAnyOrder(name(InhOne.class), name(InhBase.class), name(InhBaseBase.class),
           name(InhBaseIface2.class), name(InhBaseIface3.class), name(InhBaseIface.class));
     }
   }
@@ -100,7 +100,7 @@ void beanScope_all_interfaces() {
         .findFirst().orElse(null);
 
       assertThat(extendIfaces.keys())
-        .containsExactly(name(ConcreteExtend.class), name(IfaceExtend.class), name(IfaseBase.class));
+        .containsExactlyInAnyOrder(name(ConcreteExtend.class), name(IfaceExtend.class), name(IfaseBase.class));
     }
   }
 
@@ -115,7 +115,7 @@ void beanScope_all_includesGenericInterfaces() {
         .findFirst().orElse(null);
 
       assertThat(hazRepo.keys())
-        .containsExactly(name(HazRepo.class), name(HazRepo$DI.TYPE_RepositoryHazLong));
+        .containsExactlyInAnyOrder(name(HazRepo.class), name(HazRepo$DI.TYPE_RepositoryHazLong));
     }
   }
 
@@ -130,7 +130,7 @@ void beanScope_all_interfaceWithParameter() {
         .findFirst().orElse(null);
 
       assertThat(hazRepo.keys())
-        .containsExactly(name(MyParam.class), name(IfaceParam.class), name(IfaceParamParent.class));
+        .containsExactlyInAnyOrder(name(MyParam.class), name(IfaceParam.class), name(IfaceParamParent.class));
     }
   }
 
diff --git a/inject/src/main/java/io/avaje/inject/AsyncBean.java b/inject/src/main/java/io/avaje/inject/AsyncBean.java
new file mode 100644
index 00000000..19a2f03f
--- /dev/null
+++ b/inject/src/main/java/io/avaje/inject/AsyncBean.java
@@ -0,0 +1,8 @@
+package io.avaje.inject;
+
+import java.lang.annotation.*;
+
+/** Limits the types exposed by this bean to the given types. */
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface AsyncBean {}
\ No newline at end of file