skip to content

How does bean resolution delegate from a child context to its parent, and what happens when the child defines a bean with the same name as one in the parent?

level: middleimportance: must knowfreq 48%

answer

  1. doGetBean: local first, then getParentBeanFactory()
  2. Same name in child = shadows parent (no error)
  3. One name → two instances (per context)
  4. getBean spans ancestors; getBeansOfType does NOT
  5. @Autowired collections DO span ancestors

basics

~20 s

When the child can't find a bean locally, it asks its parent (delegation upward). If the child defines a bean with the same name as the parent, the child's bean 'hides' the parent's for lookups made from the child — both instances can still exist independently.

solid answer

~40 s

Every context wraps a `HierarchicalBeanFactory`. On `getBean("x")`, `AbstractBeanFactory.doGetBean` checks the child's own bean definitions first; if absent, it calls `getParentBeanFactory().getBean("x")` and walks upward. So single-bean lookups (by name or by type) transparently span the hierarchy. When the child declares a bean with the **same name** as the parent, there's no error — the child's definition simply **shadows** the parent's for anything resolved through the child. The parent keeps and uses its own instance; the child uses its override. That means the same singleton name can back **two distinct instances**, one per context. This is a deliberate override technique (e.g., a DispatcherServlet child overriding a root bean). A subtle asymmetry: `getBean` delegates to ancestors, but `getBeansOfType(...)` on a context does **not** include ancestor beans by default.

code

java · 25 lines
java
@Configuration
class ParentConfig {
    @Bean Clock clock() { return Clock.systemUTC(); }        // parent version
}

@Configuration
class ChildConfig {
    @Bean Clock clock() { return Clock.fixed(EPOCH, UTC); }  // shadows parent for child
}

var parent = new AnnotationConfigApplicationContext(ParentConfig.class);
var child  = new AnnotationConfigApplicationContext();
child.setParent(parent);
child.register(ChildConfig.class);
child.refresh();

child.getBean(Clock.class);   // -> fixed clock  (local wins)
parent.getBean(Clock.class);  // -> systemUTC()  (parent never sees child)

// Asymmetry: getBean delegates up, getBeansOfType does not
var child2 = new AnnotationConfigApplicationContext();
child2.setParent(parent);
child2.refresh();
child2.getBean(Clock.class);            // OK - found in parent
child2.getBeansOfType(Clock.class);     // {} empty - ancestors not included

go deeper

for a junior

Explain that the child asks the parent when it can't find a bean, and that a same-named child bean 'wins' locally.

for a middle

Describe doGetBean's local-then-parent delegation and the shadowing semantics producing per-context instances.

for a senior

Call out the getBean-vs-getBeansOfType ancestor asymmetry and that @Autowired collections include ancestors while getBeansOfType does not.

for a principal

Reason about failure modes: duplicated singletons/state, accidental shadowing hiding config changes, and where to place shared dependencies given upward-only wiring.

## Delegation mechanics The container behind every `ApplicationContext` implements `HierarchicalBeanFactory`, which adds two methods over the plain `BeanFactory`: - `getParentBeanFactory()` — returns the parent factory (or `null`). - `containsLocalBean(name)` — true only if the bean exists **in this factory**, ignoring ancestors. On a lookup, `AbstractBeanFactory.doGetBean(name, ...)`: 1. Tries to resolve `name` **locally** (own singleton cache + own bean definitions). 2. If not found **and** a parent factory exists, it delegates: `parentBeanFactory.getBean(name, ...)`. 3. This repeats up the chain until resolved or the root throws `NoSuchBeanDefinitionException`. So **single-bean lookups traverse the hierarchy**: `getBean(String)`, `getBean(Class)`, and `@Autowired` single-dependency injection all consider ancestor beans (autowiring uses `BeanFactoryUtils.beanNamesForTypeIncludingAncestors`). ## Same-name override (shadowing) If the child registers a bean with the **same name** as one in the parent: - It is **not** a conflict/error — the two live in different factories. - Lookups from the **child** return the **child's** bean (local wins before delegation). - Lookups from the **parent** return the **parent's** bean (parent never consults the child). - Result: the same logical name can back **two separate instances**, one per context. Each context's beans wire against the version visible to *them*. This is a legitimate override pattern: put a default in the root, override it in a specific child. ## The `getBeansOfType` asymmetry (key gotcha) While `getBean` delegates upward, **`ApplicationContext.getBeansOfType(...)` does NOT include ancestor contexts by default** — it introspects only the local factory's beans. To span the hierarchy for a *collection* lookup you must use `BeanFactoryUtils.beansOfTypeIncludingAncestors(factory, type)`. This surprises people: `getBean(Foo.class)` finds a parent bean, but `getBeansOfType(Foo.class)` on the same child may return an empty map. Note: **`@Autowired` collection injection** (e.g. `List<Foo>`) *does* include ancestors, because `DefaultListableBeanFactory.findAutowireCandidates` uses the `...IncludingAncestors` helper. So the collection you get via autowiring can differ from what `getBeansOfType` returns. ## Practical implications - **Duplicated singletons:** the same `@Component` type scanned in both parent and child yields two instances — watch for state/caches you assumed were global. - **Wiring targets:** a parent bean can only inject other parent (ancestor) beans; it can never depend on a child bean, so keep shared dependencies at or above the level that needs them. - **Override with care:** shadowing is powerful but can hide bugs where you *thought* you were reconfiguring the shared bean but only overrode it in one child.

  • Why can getBean(Foo.class) succeed while getBeansOfType(Foo.class) returns empty on the same child?
    getBean does single-bean resolution and delegates to the parent factory when the bean is missing locally. getBeansOfType introspects only the local factory and excludes ancestors by default; use BeanFactoryUtils.beansOfTypeIncludingAncestors to span the hierarchy.
  • If the same @Component class is scanned in both parent and child, how many instances exist?
    Two — each context creates its own singleton in its own factory. They are independent objects; sharing requires defining the bean only in the parent and injecting it into the child.

saying these in an interview costs you the question

  • Saying a duplicate bean name across levels throws an error — it doesn't; the child shadows the parent.
  • Assuming getBeansOfType automatically includes parent beans (it doesn't by default).
  • Thinking a same-named singleton is one shared instance across both contexts.

context