apache/maven · error · DIException

Circular references: {}

Error message

Circular references: {}

What it means

InjectorImpl.doBind keeps a LinkedHashSet ('current') as a stack of keys currently being registered. If binding a key re-enters doBind for a key already on that stack, it throws DIException 'Circular references:' followed by the full key path, where the repeated key marks the cycle. This guards the registration phase; instance-resolution cycles are reported separately by the resolution stack check.

Source

Thrown at impl/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java:171

        Key<?> key = Key.of(clazz, ReflectionUtils.qualifierOf(clazz));
        if (clazz.isInterface()) {
            bindings.computeIfAbsent(key, $ -> new HashSet<>());
            if (key.getQualifier() != null) {
                bindings.computeIfAbsent(Key.ofType(clazz), $ -> new HashSet<>());
            }
        } else if (!Modifier.isAbstract(clazz.getModifiers())) {
            Binding<?> binding = ReflectionUtils.generateImplicitBinding(key);
            doBind(key, binding);
        }
        return this;
    }

    private final LinkedHashSet<Key<?>> current = new LinkedHashSet<>();

    private Injector doBind(Key<?> key, Binding<?> binding) {
        if (!current.add(key)) {
            current.add(key);
            throw new DIException("Circular references: " + current);
        }
        try {
            doBindImplicit(key, binding);
            Class<?> cls = key.getRawType().getSuperclass();
            while (cls != Object.class && cls != null) {
                doBindImplicit(Key.of(cls, key.getQualifier()), binding);
                if (key.getQualifier() != null) {
                    bind(Key.ofType(cls), binding);
                }
                cls = cls.getSuperclass();
            }
            return this;
        } finally {
            current.remove(key);
        }
    }

    protected <U> Injector bind(Key<U> key, Binding<U> b) {

View on GitHub (pinned to e4093d4e12)

Solutions

  1. Read the key path in the message: the key that appears twice delimits the cycle; remove one of the two registration sites
  2. Move all bind* calls to top-level bootstrap code instead of performing them during binding callbacks
  3. If two modules must contribute bindings for the same key, let them register normally so the binding set handles precedence rather than re-entering doBind
  4. If no user code re-binds keys, report the framework-internal re-entrancy to maven-di

Example fix

// before: binding A re-enters binding A during registration
injector.bindInstance(A.class, makeA(injector)); // makeA() binds A.class again

// after: finish construction first, then register once
A a = makeA();
injector.bindInstance(A.class, a);
Defensive patterns

Strategy: try-catch

Try / catch

try {
    injector.bindInstance(A.class, a);
} catch (DIException e) {
    if (e.getMessage().startsWith("Circular references")) {
        // message lists the key cycle: remove one of the re-entrant bind sites
        throw new ConfigurationException("re-entrant binding detected: " + e.getMessage(), e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling bind/bindInstance/bindSupplier/bindImplicit for key K while K's own binding is still in progress — e.g. binding hooks, implicit-binding generation, or qualifier-aware superclass registration that re-binds the same key re-entrantly.

Common situations: Extension code that performs injector configuration from inside binding callbacks; repeated bootstrap of the same module against one injector; custom Scope or Binding implementations with side effects during registration.

Related errors


AI-assisted analysis of apache/maven@e4093d4e12 (2026-08-21). Data as JSON: /api/errors/e2cd0a864490b074. Report an issue: GitHub.