skip to content

Explain bean overriding and precedence when a @TestConfiguration defines a bean that already exists in the primary context. What errors can occur and how do you control which bean wins?

level: principalimportance: should knowfreq 35%

answer

  1. same NAME => BeanDefinitionOverrideException (off by default 2.1)
  2. same TYPE, diff names => NoUniqueBeanDefinitionException
  3. flag: allow-bean-definition-overriding
  4. @TestBean/@MockBean replace without the flag
  5. auto-config backs off via @ConditionalOnMissingBean

basics

~20 s

Since Spring Boot 2.1, defining a duplicate bean name throws BeanDefinitionOverrideException by default. To override in tests you either enable spring.main.allow-bean-definition-overriding=true, match by bean name so the test definition replaces the original, or use @MockBean/@TestBean which are built to replace beans safely.

solid answer

~40 s

By default (since Boot 2.1) bean-definition overriding is **disabled**, so a `@TestConfiguration` `@Bean` whose name collides with an existing bean fails context startup with `BeanDefinitionOverrideException`. Options: (1) set `spring.main.allow-bean-definition-overriding=true` for the test — then the later-registered definition wins, and imported/test definitions are typically registered after the primary ones, so the test bean overrides; ordering is fragile, so rely on it deliberately. (2) Prefer replacement-aware tools: `@MockBean`/`@TestBean` remove the original definition and register the double regardless of the flag. (3) If you only need *selection among multiple candidates*, use `@Primary` on the test bean or `@Qualifier` at injection points rather than true overriding. For autowiring ambiguity (two beans, same type, no override), Spring throws `NoUniqueBeanDefinitionException` unless one is `@Primary` or the injection is qualified.

code

java · 26 lines
java
// Case 1: overriding a USER-DEFINED bean by same name -> needs the flag
@SpringBootTest(properties = "spring.main.allow-bean-definition-overriding=true")
class OverrideByNameTest {
    @TestConfiguration
    static class Override {
        @Bean
        PaymentGateway paymentGateway() {   // same name as production bean
            return new FakePaymentGateway(); // later definition wins
        }
    }
}

// Case 2: substituting an AUTO-CONFIGURED bean -> no flag needed.
// The auto-config bean uses @ConditionalOnMissingBean and backs off.
@TestConfiguration
class ClockOverride {
    @Bean
    Clock clock() { return Clock.fixed(Instant.EPOCH, ZoneOffset.UTC); }
}

// Case 3: choose among multiple candidates without overriding
@TestConfiguration
class PrimarySelection {
    @Bean @Primary                       // wins by-type injection; original still exists
    NotificationChannel testChannel() { return new RecordingChannel(); }
}

go deeper

for a junior

Know that duplicate bean names error out by default and there's a property to allow overriding.

for a middle

Distinguish the override exception from injection ambiguity and know @MockBean replaces beans.

for a senior

Explain registration ordering, auto-config back-off via @ConditionalOnMissingBean, and @Primary vs true override.

for a principal

Set a team policy that avoids global override-enabling, prefers replacement-aware tools, keeps the context cache from fragmenting, and reasons about precedence deliberately rather than by accident.

## Two distinct failure modes — don't conflate them ### 1. BeanDefinitionOverrideException (registration time) Happens when **two bean definitions share the same bean name**. Since Spring Boot 2.1, `spring.main.allow-bean-definition-overriding` defaults to **false**, so the second registration throws `BeanDefinitionOverrideException` during context startup. This is a *definition-name* collision, independent of type. ### 2. NoUniqueBeanDefinitionException (injection time) Happens when you inject **by type** and there are **multiple beans of that type** (different names). Spring can't choose one. Resolve with `@Primary` (marks a default winner) or `@Qualifier` (names the target at the injection site). ## Controlling which bean wins ### Option A — enable overriding ```properties spring.main.allow-bean-definition-overriding=true ``` With overriding on, a **later** bean definition of the same name replaces an earlier one. Registration order matters: the primary configuration is processed, then `@Import`ed/`@TestConfiguration` definitions — so test definitions generally register **after** and thus win. But relying on ordering is brittle; treat it as an explicit, documented choice, not luck. ### Option B — replacement-aware annotations (preferred) `@MockBean`, `@SpyBean` (older) and `@TestBean` (Boot 3.4+, the successor) **remove** the existing definition and register the test double in its place. They work **without** the overriding flag because they don't create a name collision — they replace. This is the cleanest way to swap a single collaborator. ### Option C — selection instead of override If you can tolerate both beans coexisting, don't override at all: - Mark the test bean `@Primary` so it's chosen for by-type injection. - Or use `@Qualifier("...")` at the injection point. This avoids `BeanDefinitionOverrideException` entirely because names differ. ## Interaction with context caching Each distinct override strategy that changes the bean set changes the **context cache key**. A test that flips `allow-bean-definition-overriding` or adds a `@MockBean` gets a **separate cached context** from tests that don't. At scale, standardize the approach so contexts are shared. ## @Primary vs override — subtle point `@Primary` does **not** remove the other bean; both exist, and `@Primary` only breaks *ambiguity* for by-type injection. If some code injects by name or iterates all beans of a type, both still appear. Override/replace actually removes the original. ## Auto-configuration precedence nuance User-defined beans generally win over auto-configured ones because auto-configuration classes use `@ConditionalOnMissingBean`. A `@TestConfiguration` bean of that type therefore causes the auto-configured bean to back off — no override exception, because the auto-config bean was conditional. This is a clean, flag-free way to substitute infrastructure beans in tests. ## Practical decision tree 1. Replacing one collaborator with a double → `@TestBean`/`@MockBean`. 2. Substituting an **auto-configured** bean → just define your own `@Bean` (conditional backs off). 3. Overriding a **user-defined** bean by the same name → enable the flag, or rename + `@Primary`. 4. Multiple candidates, choose one → `@Primary`/`@Qualifier`. ## Gotchas - Enabling overriding globally can **mask accidental** duplicate beans — the whole reason Boot disabled it by default. Scope it to the specific test. - Registration-order reliance is fragile across refactors. - `@Primary` doesn't eliminate `BeanDefinitionOverrideException` if names actually collide — it's an *ambiguity* tool, not an *override* tool. - Mixing a `@TestConfiguration` override and a `@MockBean` of the same type: `@MockBean` still replaces it.

  • Why did Spring Boot 2.1 disable bean-definition overriding by default?
    To surface accidental duplicate bean definitions early instead of silently letting one clobber another. The fail-fast BeanDefinitionOverrideException prevents subtle bugs from unintended overrides during refactors or dependency upgrades.
  • How can a @TestConfiguration bean replace an auto-configured bean without any override flag?
    Auto-configuration beans are typically guarded by @ConditionalOnMissingBean. Defining your own bean of that type makes the auto-configured one back off, so there's no name collision and no override — it's a supported substitution path.
  • Does @Primary solve a BeanDefinitionOverrideException?
    No. @Primary resolves by-type injection *ambiguity* among distinctly named beans. If two definitions share the same name, you still get BeanDefinitionOverrideException; @Primary only helps when both beans coexist under different names.

saying these in an interview costs you the question

  • Confusing BeanDefinitionOverrideException (name collision) with NoUniqueBeanDefinitionException (type ambiguity).
  • Thinking @Primary overrides/removes a same-named bean.
  • Believing overriding is enabled by default in modern Spring Boot.
  • Assuming you always need the flag to substitute an auto-configured bean.

context