Repository: avaje/avaje-inject (java)

Issue:
Revisit `@Priority`

The current implementation of `@Priority` and `BeanScope#listByPriority` presents several critical issues, leading to **confusing behavior** and **limiting its practical application**, especially in scenarios involving multiple modules and complex bean hierarchies.

---

The core problem lies in the **inconsistent and unexpected interactions** between `@Priority`, `@Primary`, `@Secondary`, and the `BeanScope` methods, particularly `listByPriority` and bean injection. This makes it difficult to reliably control bean selection and ordering.

**Specific Issues:**

1.  **Limited `@Priority` Placement:** The `@Priority` annotation **cannot be applied to factory beans**. This significantly reduces its utility for scenarios where bean creation is managed by factories.
2.  **`BeanScope#listByPriority` Ignores `@Primary` / `@Secondary`:** When using `BeanScope#listByPriority`, the order **does not respect** the explicit preference provided by `@Primary` or `@Secondary` annotations. This undermines the intent of these annotations for bean disambiguation.
3.  **`@Priority` Ineffectiveness in Wiring:** When multiple beans of the same type are available, `@Priority` has **no effect** on which bean is injected. This leads to unpredictable bean resolution during dependency injection.
4.  **Runtime Annotation Reading:** As a compile-time framework that uses annotation processing, reading `@Priority` annotations at **runtime** to extract their value runs counter to the idea of the entire project. This information should be processed during **compilation**.
5. I cannot stress enough how much the runtime annotation reading _personally annoys me_ now that it was pointed out
---

###  Desired Behavior

We expect `@Priority` to provide a **consistent and intuitive mechanism** for ordering beans and influencing bean selection during injection and when using `BeanScope#listByPriority`. Specifically:

* `@Priority` should be **applicable to factory beans**.
* `BeanScope#listByPriority` should **respect and integrate** with `@Primary` and `@Secondary` annotations to provide a consistent ordering.
* `@Priority` should **influence bean selection** during dependency injection when multiple beans of the same type are present, potentially in conjunction with `@Primary` and `@Secondary`.
* Annotation processing for `@Priority` should occur at **compile-time**.
---

Consider the following scenarios, which demonstrate the problematic behavior:

#### Scenario 1: `@Priority` and Wiring Inconsistency

Imagine a setup where different modules contribute `LobbyController` implementations, and we want to prioritize `SMPShardLobbyController`.

```java
@Slf4j
@Singleton
@Priority(1)
public final class SMPShardLobbyController extends LobbyController {
    @Inject
    public SMPShardLobbyController(
    ) {
        super();
    }

    @Override
    public boolean canTeleport(Player player) {
        log.info("SMPShard Can Teleport");
        return super.canTeleport(player);
    }
}
```

```java
@Singleton
@Priority(3)
public class LobbyController {
    @Inject
    public LobbyController(

    ) {

    }

    public boolean teleport(final Player player) {
      // impl
    }

    /**
     * Check whether a player can teleport to the lobby or not.
     * @param player the player
     * @return true if they can teleport false otherwise.
     */
    public boolean canTeleport(final Player player) {
        return true;
    }
}
```

```java
@Singleton
public final class LobbyCommand implements BaseCommand {
    private final LobbyController lobbyController;

    @Inject
    public LobbyCommand(
        final LobbyController lobbyController
    ) {
        this.lobbyController = lobbyController;
    }

    @Command("lobby|hub|l")
    @CommandDescription("Bring yourself back to the lobby")
    public void lobby(final PlayerSource source) {
        lobbyController.teleport(source.source());
    }
}
```

Problem: `BeanScope#get(LobbyController.class)` can often not return the `SMPShardLobbyController` as desired, even with `@Priority(1)`


#### Scenario 2: `BeanScope#listByPriority` doesn't respect `@Primary` or `@Secondary`

Consider a situation with `Gson` beans provided by different factories, using `@Primary` and `@Secondary` for disambiguation.

```java
@Slf4j
@Factory
public final class GsonProvider {
    private final Environment environment;

    @Inject
    public GsonProvider(
        final Environment environment
    ) {
        this.environment = environment;
    }

    @Primary @Bean
    @Named("base")
    public Gson provideGson() {
        return new GsonBuilder()
            /* impl */
            .create();
    }
}
```
```java
@Factory
public final class LayerGsonProvider {
    private final Gson baseGson;

    @Inject
    public LayerGsonProvider(
        @Named("base") final Gson gson
    ) {
        this.baseGson = gson;
    }

    @Secondary @Bean
    @Named("bukkit")
    public Gson layerGson() {
        return this.baseGson
            .newBuilder()
            .registerTypeAdapter(Location.class, new LocationSerializer())
            .create();
    }
}
```

Problem: When calling `BeanScope#listByPriority(Gson.class)`, the returned list does not reflect the order implied by `@Primary` or `@Secondary`. The base `Gson` (marked `@Primary`) and bukkit `Gson` (marked `@Secondary`) will not necessarily appear in the expected priority order.


Pull request description:
Determine Priority Annotations at Compile Time
- read the `@Priority` annotations at compile time
- listByPriority will now sort `@Primary`/`@Secondary` beans
- simplify duplicate type matching logic
- Add support for using `@Priority` on factory `@Bean` methods

Resolves #854 

Patch:
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 47bf9f54..820d10f8 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
@@ -54,6 +54,7 @@ final class BeanReader {
   private boolean suppressGeneratedImport;
   private Set<UType> allUTypes;
   private final boolean delayed;
+  private final Integer priority;
 
   BeanReader(TypeElement beanType, boolean factory, boolean importedComponent) {
     this.beanType = beanType;
@@ -69,6 +70,7 @@ final class BeanReader {
         || importedComponent && ProcessingContext.isImportedPrototype(actualType);
     this.primary = PrimaryPrism.isPresent(actualType);
     this.secondary = !primary && SecondaryPrism.isPresent(actualType);
+    this.priority = Util.priority(actualType);
     var beanTypes =
       BeanTypesPrism.getOptionalOn(actualType)
         .map(BeanTypesPrism::value)
@@ -368,6 +370,8 @@ final class BeanReader {
       writer.append("asPrimary().");
     } else if (secondary) {
       writer.append("asSecondary().");
+    } else if (priority != null) {
+      writer.append("asPriority(%s).", priority);
     }
     writer.append("register(bean);").eol();
   }
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 8a17d282..aee034a2 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
@@ -4,6 +4,7 @@ import static io.avaje.inject.generator.APContext.logWarn;
 import static io.avaje.inject.generator.Constants.CONDITIONAL_DEPENDENCY;
 import static io.avaje.inject.generator.ProcessingContext.asElement;
 
+import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -37,6 +38,7 @@ final class MethodReader {
   private final boolean prototype;
   private final boolean primary;
   private final boolean secondary;
+  private final Integer priority;
   private final boolean lazy;
   private final boolean proxyLazy;
   private final TypeElement lazyProxyType;
@@ -68,6 +70,7 @@ final class MethodReader {
       prototype = PrototypePrism.isPresent(element);
       primary = PrimaryPrism.isPresent(element);
       secondary = SecondaryPrism.isPresent(element);
+      priority = Util.priority(element);
       lazy = LazyPrism.isPresent(element) || LazyPrism.isPresent(element.getEnclosingElement());
       conditions.readAll(element);
       this.lazyProxyType = lazy ? Util.lazyProxy(element) : null;
@@ -76,6 +79,7 @@ final class MethodReader {
       prototype = false;
       primary = false;
       secondary = false;
+      priority = null;
       lazy = false;
       this.proxyLazy = false;
       this.lazyProxyType = null;
@@ -273,6 +277,8 @@ final class MethodReader {
       writer.append(".asPrototype()");
     } else if (secondary) {
       writer.append(".asSecondary()");
+    } else if (priority != null) {
+      writer.append(".asPriority(%s)", priority);
     }
 
     if (proxyLazy) {
@@ -331,6 +337,8 @@ final class MethodReader {
         writer.append(".asPrimary()");
       } else if (secondary) {
         writer.append(".asSecondary()");
+      } else if (priority != null) {
+        writer.append(".asPriority(%s)", priority);
       } else if (prototype) {
         writer.append(".asPrototype()");
       }
@@ -525,8 +533,15 @@ final class MethodReader {
     return lazy;
   }
 
+  /**
+   * Use a Provider with Secondary or Priority annotation.
+   *
+   * <p> As a Provider, the bean won't be instantiated unless it is needed
+   * by being wired as the highest priority, or wired in a List/Set/Map, or
+   * by being accessed via {@link io.avaje.inject.BeanScope#list(Type)} etc.
+   */
   boolean isUseProviderForSecondary() {
-    return secondary && !optionalType && !Util.isProvider(returnTypeRaw);
+    return (secondary || priority != null) && !optionalType && !Util.isProvider(returnTypeRaw);
   }
 
   boolean isPublic() {
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Util.java b/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
index 7e7feb50..bc1f46b5 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
@@ -430,6 +430,23 @@ final class Util {
   }
 
   public static String shortNameLazyProxy(TypeElement lazyProxyType) {
-    return shortName(lazyProxyType.getQualifiedName().toString())
-      .replace(".", "_");  }
+    return shortName(lazyProxyType.getQualifiedName().toString()).replace(".", "_");
+  }
+
+  static Integer priority(Element element) {
+    for (final var mirror : element.getAnnotationMirrors()) {
+      if (isPriorityAnnotation(mirror) && mirror.getElementValues().size() == 1) {
+        var value = mirror.getElementValues().values().iterator().next().getValue();
+        if (value instanceof Integer) {
+          var val = (Integer) value;
+          return val <= Integer.MIN_VALUE + 1 ? Integer.MIN_VALUE + 2 : val;
+        }
+      }
+    }
+    return null;
+  }
+
+  private static boolean isPriorityAnnotation(AnnotationMirror mirror) {
+    return mirror.getAnnotationType().asElement().getSimpleName().toString().contains("Priority");
+  }
 }
diff --git a/inject/src/main/java/io/avaje/inject/BeanEntry.java b/inject/src/main/java/io/avaje/inject/BeanEntry.java
index 889757e4..9e75ada9 100644
--- a/inject/src/main/java/io/avaje/inject/BeanEntry.java
+++ b/inject/src/main/java/io/avaje/inject/BeanEntry.java
@@ -12,12 +12,12 @@ public interface BeanEntry {
   /**
    * Priority of externally supplied bean.
    */
-  int SUPPLIED = 2;
+  int SUPPLIED = Integer.MIN_VALUE;
 
   /**
    * Priority of <code>@Primary</code> bean.
    */
-  int PRIMARY = 1;
+  int PRIMARY = Integer.MIN_VALUE + 1;
 
   /**
    * Priority of normal bean.
@@ -27,7 +27,7 @@ public interface BeanEntry {
   /**
    * Priority of <code>@Secondary</code> bean.
    */
-  int SECONDARY = -1;
+  int SECONDARY = Integer.MAX_VALUE;
 
   /**
    * Return the bean name.
@@ -45,7 +45,7 @@ public interface BeanEntry {
   Class<?> type();
 
   /**
-   * Return the priority indicating if the bean is Supplied Primary, Normal or Secondary.
+   * Return the wiring priority of the bean.
    */
   int priority();
 
diff --git a/inject/src/main/java/io/avaje/inject/BeanScope.java b/inject/src/main/java/io/avaje/inject/BeanScope.java
index 0a480b4a..eff8f680 100644
--- a/inject/src/main/java/io/avaje/inject/BeanScope.java
+++ b/inject/src/main/java/io/avaje/inject/BeanScope.java
@@ -198,21 +198,21 @@ public interface BeanScope extends AutoCloseable {
    */
   <T> List<T> list(Type type);
 
-  /**
-   * Return the list of beans that implement the interface sorting by priority.
-   */
-  <T> List<T> listByPriority(Class<T> type);
+  /** Return the list of beans that implement the class sorting by priority. */
+  default <T> List<T> listByPriority(Class<T> type) {
+    return listByPriority((Type) type);
+  }
+
+  /** Return the list of beans that implement the type sorting by priority. */
+  <T> List<T> listByPriority(Type type);
 
   /**
-   * Return the beans that implement the interface sorting by the priority annotation used.
-   * <p>
-   * The priority annotation will typically be either <code>javax.annotation.Priority</code>
-   * or <code>jakarta.annotation.Priority</code>.
-   *
-   * @param type     The interface type of the beans to return
-   * @param priority The priority annotation used to sort the beans
+   * @deprecated use {@link #listByPriority(Class)}
    */
-  <T> List<T> listByPriority(Class<T> type, Class<? extends Annotation> priority);
+  @Deprecated(forRemoval = true)
+  default <T> List<T> listByPriority(Class<T> type, Class<? extends Annotation> priority) {
+    return listByPriority(type);
+  }
 
   /**
    * Return the beans for this type mapped by their qualifier name.
diff --git a/inject/src/main/java/io/avaje/inject/Priority.java b/inject/src/main/java/io/avaje/inject/Priority.java
index 937d9b14..f7115e71 100644
--- a/inject/src/main/java/io/avaje/inject/Priority.java
+++ b/inject/src/main/java/io/avaje/inject/Priority.java
@@ -1,26 +1,26 @@
 package io.avaje.inject;
 
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 
-import static java.lang.annotation.ElementType.TYPE;
-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 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.
- * </p>
+ * The <code>Priority</code> annotation can be applied to classes to indicate the wiring priority of
+ * a bean to resolve cases where multiple beans of the same type exist.
+ *
+ * <p>Beans can be returned using other Priority annotation such as <code>
+ * jakartak.annotation.Priority
+ * </code> or any custom priority annotation that has an <code>int value()</code> attribute.
  *
- * @see BeanScope#listByPriority(Class)
- * @see BeanScope#listByPriority(Class, Class)
+ * @see BeanScope#listByPriority(Type)
  */
 @Documented
 @Retention(RUNTIME)
-@Target(TYPE)
+@Target({TYPE, METHOD})
 public @interface Priority {
   int value();
 }
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 56caf92f..dae14918 100644
--- a/inject/src/main/java/io/avaje/inject/spi/Builder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/Builder.java
@@ -59,16 +59,22 @@ public interface Builder {
 
   /**
    * Register the next bean as having Primary priority.
-   * Highest priority, will be used over any other matching beans.
+   * Highest priority, wired over any other matching beans.
    */
   Builder asPrimary();
 
   /**
    * Register the next bean as having Secondary priority.
-   * Lowest priority, only used if no other matching beans are available.
+   * Lowest priority, wired when no other matching beans are available.
    */
   Builder asSecondary();
 
+  /**
+   * Register the next bean as having the given priority. Wired only if no other higher priority
+   * matching beans are available.
+   */
+  Builder asPriority(int priority);
+
   /**
    * Register the next bean as having Prototype scope.
    */
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 15710b96..2f3d3269 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
@@ -1,11 +1,10 @@
 package io.avaje.inject.spi;
 
-import io.avaje.inject.BeanEntry;
-import io.avaje.inject.BeanScope;
-import jakarta.inject.Provider;
+import static java.util.stream.Collectors.toList;
 
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -13,6 +12,10 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 
+import io.avaje.inject.BeanEntry;
+import io.avaje.inject.BeanScope;
+import jakarta.inject.Provider;
+
 /**
  * Map of types (class types, interfaces and annotations) to a DContextEntry where the
  * entry holds a list of bean instances for that type.
@@ -121,6 +124,19 @@ final class DBeanMap {
     return (T) entry.get(name, currentModule);
   }
 
+  public <T> List<T> listByPriority(Type type) {
+
+    DContextEntry entry = beans.get(type.getTypeName());
+    if (entry == null) {
+      return List.of();
+    }
+
+    return entry.entries().stream()
+        .sorted(Comparator.comparingInt(DContextEntryBean::priority))
+        .map(e -> (T) e.bean())
+        .collect(toList());
+  }
+
   @SuppressWarnings("unchecked")
   <T> Provider<T> provider(Type type, String name) {
     DContextEntry entry = beans.get(type.getTypeName());
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 430a081a..56f60dd3 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java
@@ -7,7 +7,6 @@ import java.lang.System.Logger.Level;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -23,7 +22,6 @@ import org.jspecify.annotations.Nullable;
 import io.avaje.applog.AppLog;
 import io.avaje.inject.BeanEntry;
 import io.avaje.inject.BeanScope;
-import io.avaje.inject.Priority;
 
 @NullMarked
 final class DBeanScope implements BeanScope {
@@ -191,37 +189,8 @@ final class DBeanScope implements BeanScope {
   }
 
   @Override
-  public <T> List<T> listByPriority(Class<T> type) {
-    return listByPriority(type, Priority.class);
-  }
-
-  @Override
-  public <T> List<T> listByPriority(Class<T> type, Class<? extends Annotation> priorityAnnotation) {
-    List<T> list = list(type);
-    return list.size() > 1 ? sortByPriority(list, priorityAnnotation) : list;
-  }
-
-  private <T> List<T> sortByPriority(List<T> list, final Class<? extends Annotation> priorityAnnotation) {
-    boolean priorityUsed = false;
-    List<SortBean<T>> tempList = new ArrayList<>(list.size());
-    for (T bean : list) {
-      SortBean<T> sortBean = new SortBean<>(bean, priorityAnnotation);
-      tempList.add(sortBean);
-      if (!priorityUsed && sortBean.priorityDefined) {
-        priorityUsed = true;
-      }
-    }
-    if (!priorityUsed) {
-      // nothing with Priority annotation so return original order
-      return list;
-    }
-    Collections.sort(tempList);
-    // unpack into new sorted list
-    List<T> sorted = new ArrayList<>(tempList.size());
-    for (SortBean<T> sortBean : tempList) {
-      sorted.add(sortBean.bean);
-    }
-    return sorted;
+  public <T> List<T> listByPriority(Type type) {
+    return beans.listByPriority(type);
   }
 
   @Override
@@ -249,7 +218,7 @@ final class DBeanScope implements BeanScope {
     } finally {
       lock.unlock();
     }
-    log.log(INFO, "Wired beans in {0}ms", (System.currentTimeMillis() - start));
+    log.log(INFO, "Wired beans in {0}ms", System.currentTimeMillis() - start);
     return this;
   }
 
@@ -299,41 +268,4 @@ final class DBeanScope implements BeanScope {
       scope.shutdown();
     }
   }
-
-  private static class SortBean<T> implements Comparable<SortBean<T>> {
-
-    private final T bean;
-
-    private boolean priorityDefined;
-
-    private final int priority;
-
-    SortBean(T bean, Class<? extends Annotation> priorityAnnotation) {
-      this.bean = bean;
-      this.priority = initPriority(priorityAnnotation);
-    }
-
-    int initPriority(Class<? extends Annotation> priorityAnnotation) {
-      // Avoid adding hard dependency on javax.annotation-api by using reflection
-      try {
-        final Annotation ann = bean.getClass().getDeclaredAnnotation(priorityAnnotation);
-        if (ann != null) {
-          final int newPriority = (Integer) priorityAnnotation.getMethod("value").invoke(ann);
-          priorityDefined = true;
-          return newPriority;
-        }
-      } catch (Exception e) {
-        // If this happens, something has gone very wrong since a non-confirming @Priority was found...
-        throw new UnsupportedOperationException("Problem instantiating @Priority", e);
-      }
-      // Default priority as per javax.ws.rs.Priorities.USER
-      // User-level filter/interceptor priority
-      return 5000;
-    }
-
-    @Override
-    public int compareTo(SortBean<T> o) {
-      return Integer.compare(priority, o.priority);
-    }
-  }
 }
diff --git a/inject/src/main/java/io/avaje/inject/spi/DBeanScopeProxy.java b/inject/src/main/java/io/avaje/inject/spi/DBeanScopeProxy.java
index 384d4a56..9112e527 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBeanScopeProxy.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBeanScopeProxy.java
@@ -106,7 +106,7 @@ final class DBeanScopeProxy implements BeanScope {
   }
 
   @Override
-  public <T> List<T> listByPriority(Class<T> type) {
+  public <T> List<T> listByPriority(Type type) {
     if (delegate != null) {
       return delegate.listByPriority(type);
     } else {
@@ -114,15 +114,6 @@ final class DBeanScopeProxy implements BeanScope {
     }
   }
 
-  @Override
-  public <T> List<T> listByPriority(Class<T> type, Class<? extends Annotation> priority) {
-    if (delegate != null) {
-      return delegate.listByPriority(type, priority);
-    } else {
-      throw illegal("listByPriority");
-    }
-  }
-
   @Override
   public <T> Map<String, T> map(Type type) {
 
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 def27eb2..382595c2 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
@@ -170,6 +170,12 @@ class DBuilder implements Builder {
     return this;
   }
 
+  @Override
+  public Builder asPriority(int priority) {
+    beanMap.nextPriority(priority);
+    return this;
+  }
+
   @Override
   public Builder asPrototype() {
     beanMap.nextPrototype();
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 f16d0f93..09b8267a 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java
@@ -1,12 +1,13 @@
 package io.avaje.inject.spi;
 
-import jakarta.inject.Provider;
-
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
+import io.avaje.inject.BeanEntry;
+import jakarta.inject.Provider;
+
 /**
  * Entry for a given key (bean class, interface class or annotation class).
  * <p>
@@ -149,37 +150,29 @@ final class DContextEntry {
         match = entry;
         return;
       }
-      if (match.isSupplied()) {
+      if (match.priority() < entry.priority()) {
         // existing supplied match always wins
         return;
-      } else if (entry.isSupplied()) {
+      } else if (match.priority() > entry.priority()) {
         // new supplied wins
         match = entry;
         return;
       }
-      if (match.isSecondary() && !entry.isSecondary()) {
-        // secondary loses
-        match = entry;
-        return;
-      }
       if (match.isPrimary()) {
         if (entry.isPrimary()) {
-          throw new IllegalStateException("Expecting only 1 bean match but have multiple primary beans " + match.bean() + " and " + entry.bean());
+          throw new IllegalStateException(
+              "Expecting only 1 bean match but have multiple primary beans "
+                  + match.bean()
+                  + " and "
+                  + entry.bean());
         }
         // leave as is, current primary wins
         return;
-      }
-      if (entry.isSecondary()) {
-        if (match.isSecondary()) {
-          ignoredSecondaryMatch = entry;
-        }
-        return;
-      }
-      if (entry.isPrimary()) {
-        // new primary wins
-        match = entry;
+      } else if (impliedName) {
+        ignoredSecondaryMatch = entry;
         return;
       }
+
       // try to resolve match using qualifier name (including null)
       if (match.isNameEqual(name) && !entry.isNameEqual(name)) {
         ignoredSecondaryMatch = entry;
@@ -197,8 +190,12 @@ final class DContextEntry {
         match = entry;
         return;
       }
-      throw new IllegalStateException("Expecting only 1 bean match but have multiple matching beans " + match.bean()
-        + " and " + entry.bean() + ". Maybe need a rebuild is required after adding a @Named qualifier?");
+      throw new IllegalStateException(
+          "Expecting only 1 bean match but have multiple matching beans "
+              + match.bean()
+              + " and "
+              + entry.bean()
+              + ". Maybe need a rebuild is required after adding a @Named qualifier or @Priority annotation?");
     }
 
     private DContextEntryBean candidate() {
@@ -210,10 +207,17 @@ final class DContextEntry {
     }
 
     private void checkSecondary() {
-      if (match.isSecondary() && ignoredSecondaryMatch != null) {
-        throw new IllegalStateException("Expecting only 1 bean match but have multiple secondary beans " + match.bean() + " and " + ignoredSecondaryMatch.bean());
+      if (match.priority() != BeanEntry.SUPPLIED
+          && match.priority() != BeanEntry.NORMAL
+          && ignoredSecondaryMatch != null
+          && ignoredSecondaryMatch.priority() == match.priority()) {
+        throw new IllegalStateException(
+            "Expecting only 1 bean match but have multiple beans with the same priority"
+                + match.bean()
+                + " and "
+                + ignoredSecondaryMatch.bean()
+                + ". Maybe need a rebuild is required after adding a @Named qualifier or @Priority annotation?");
       }
     }
-
   }
 }
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 d1f2e7a7..8fdf1e99 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
@@ -11,22 +11,22 @@ class DContextEntryBean {
   /**
    * Create taking into account if it is a Provider or the bean itself.
    */
-  static DContextEntryBean of(Object source, String name, int flag, Class<? extends AvajeModule> currentModule) {
+  static DContextEntryBean of(Object source, String name, int priority, Class<? extends AvajeModule> currentModule) {
     if (source instanceof Provider) {
-      return new ProtoProvider((Provider<?>)source, name, flag, currentModule);
+      return new ProtoProvider((Provider<?>)source, name, priority, currentModule);
     } else {
-      return new DContextEntryBean(source, name, flag, currentModule);
+      return new DContextEntryBean(source, name, priority, currentModule);
     }
   }
 
   /**
    * Create an entry with supplied Providers using a 'Once' / 'one instance' provider.
    */
-  static DContextEntryBean supplied(Object source, String name, int flag) {
+  static DContextEntryBean supplied(Object source, String name, int priority) {
     if (source instanceof Provider) {
-      return new OnceBeanProvider((Provider<?>)source, name, flag, null);
+      return new OnceBeanProvider((Provider<?>)source, name, priority, null);
     } else {
-      return new DContextEntryBean(source, name, flag, null);
+      return new DContextEntryBean(source, name, priority, null);
     }
   }
 
@@ -37,12 +37,12 @@ class DContextEntryBean {
   protected final Object source;
   protected final String name;
   protected final Class<? extends AvajeModule> sourceModule;
-  private final int flag;
+  private final int priority;
 
-  private DContextEntryBean(Object source, String name, int flag, Class<? extends AvajeModule> currentModule) {
+  private DContextEntryBean(Object source, String name, int priority, Class<? extends AvajeModule> currentModule) {
     this.source = source;
     this.name = name;
-    this.flag = flag;
+    this.priority = priority;
     this.sourceModule = currentModule;
   }
 
@@ -51,13 +51,13 @@ class DContextEntryBean {
     return "Bean{" +
       "source=" + source +
       ", name='" + name + '\'' +
-      ", flag=" + flag +
+      ", priority=" + priority +
       ", sourceModule=" + sourceModule +
       '}';
   }
 
   final DEntry entry() {
-    return new DEntry(name, flag, bean());
+    return new DEntry(name, priority, bean());
   }
 
   /**
@@ -98,19 +98,15 @@ class DContextEntryBean {
   }
 
   final boolean isPrimary() {
-    return flag == BeanEntry.PRIMARY;
+    return priority == BeanEntry.PRIMARY;
   }
 
-  final boolean isSecondary() {
-    return flag == BeanEntry.SECONDARY;
-  }
-
-  final boolean isSupplied() {
-    return flag == BeanEntry.SUPPLIED;
+  final int priority() {
+    return priority;
   }
 
   final boolean isSupplied(String qualifierName) {
-    return flag == BeanEntry.SUPPLIED && (qualifierName == null || qualifierName.equals(name));
+    return priority == BeanEntry.SUPPLIED && (qualifierName == null || qualifierName.equals(name));
   }
 
   /**


Test patch:
diff --git a/inject-test/src/test/java/io/avaje/inject/xtra/SystemContextTest.java b/inject-test/src/test/java/io/avaje/inject/xtra/SystemContextTest.java
index 2f5019de..3e9a7c38 100644
--- a/inject-test/src/test/java/io/avaje/inject/xtra/SystemContextTest.java
+++ b/inject-test/src/test/java/io/avaje/inject/xtra/SystemContextTest.java
@@ -10,6 +10,7 @@ import org.example.coffee.priority.base.ABasei;
 import org.example.coffee.priority.base.BBasei;
 import org.example.coffee.priority.base.BaseIface;
 import org.example.coffee.priority.base.CBasei;
+import org.example.coffee.priority.base.PriorityFactory.DBasei;
 import org.junit.jupiter.api.Test;
 
 import javax.annotation.Priority;
@@ -23,11 +24,12 @@ public class SystemContextTest {
   public void getBeansByPriority() {
     try (BeanScope context = BeanScope.builder().build()) {
       final List<BaseIface> beans = context.listByPriority(BaseIface.class);
-      assertThat(beans).hasSize(3);
+      assertThat(beans).hasSize(4);
 
       assertThat(beans.get(0)).isInstanceOf(CBasei.class);
       assertThat(beans.get(1)).isInstanceOf(BBasei.class);
       assertThat(beans.get(2)).isInstanceOf(ABasei.class);
+      assertThat(beans.get(3)).isInstanceOf(DBasei.class);
     }
   }
 
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 f85e0f64..dbec44a9 100644
--- a/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
+++ b/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
@@ -68,7 +68,7 @@ class CoffeeMakerTest {
       assertThat(entry.qualifierName()).isEqualTo("B");
       assertThat(entry.keys()).containsExactlyInAnyOrder(name(BSomei.class), name(Somei.class));
       assertThat(entry.type()).isEqualTo(BSomei.class);
-      assertThat(entry.priority()).isEqualTo(0);
+      assertThat(entry.priority()).isEqualTo(1);
       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/priority/base/PriorityFactory.java b/inject-test/src/test/java/org/example/coffee/priority/base/PriorityFactory.java
new file mode 100644
index 00000000..a8564c93
--- /dev/null
+++ b/inject-test/src/test/java/org/example/coffee/priority/base/PriorityFactory.java
@@ -0,0 +1,23 @@
+package org.example.coffee.priority.base;
+
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+import io.avaje.inject.Priority;
+
+@Factory
+public class PriorityFactory {
+
+  @Bean
+  @Priority(69)
+  BaseIface iface() {
+    return new DBasei();
+  }
+
+  public static class DBasei implements BaseIface {
+
+    @Override
+    public String other() {
+      return "b";
+    }
+  }
+}
diff --git a/inject-test/src/test/java/org/example/coffee/priority/base/PriorityTest.java b/inject-test/src/test/java/org/example/coffee/priority/base/PriorityTest.java
index d0b13c86..48e9eca2 100644
--- a/inject-test/src/test/java/org/example/coffee/priority/base/PriorityTest.java
+++ b/inject-test/src/test/java/org/example/coffee/priority/base/PriorityTest.java
@@ -1,13 +1,15 @@
 package org.example.coffee.priority.base;
 
-import io.avaje.inject.xtra.ApplicationScope;
-import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
 
 import java.util.List;
 
-import static org.assertj.core.api.Assertions.assertThat;
+import org.example.coffee.priority.base.PriorityFactory.DBasei;
+import org.junit.jupiter.api.Test;
 
-public class PriorityTest {
+import io.avaje.inject.xtra.ApplicationScope;
+
+class PriorityTest {
 
   @Test
   void listByPriority() {
@@ -15,9 +17,15 @@ public class PriorityTest {
     assertExpectedOrder(sorted);
   }
 
+  @Test
+  void testGet() {
+    assertThat(ApplicationScope.get(BaseIface.class)).isInstanceOf(CBasei.class);
+  }
+
   private void assertExpectedOrder(List<BaseIface> sorted) {
     assertThat(sorted.get(0)).isInstanceOf(CBasei.class);
     assertThat(sorted.get(1)).isInstanceOf(BBasei.class);
     assertThat(sorted.get(2)).isInstanceOf(ABasei.class);
+    assertThat(sorted.get(3)).isInstanceOf(DBasei.class);
   }
 }
diff --git a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
index 52b6d504..ab8d76c6 100644
--- a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
+++ b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
@@ -305,12 +305,7 @@ class BeanScopeBuilderTest {
     }
 
     @Override
-    public <T> List<T> listByPriority(Class<T> type) {
-      return null;
-    }
-
-    @Override
-    public <T> List<T> listByPriority(Class<T> type, Class<? extends Annotation> priority) {
+    public <T> List<T> listByPriority(Type type) {
       return null;
     }