Senior 11 min · May 23, 2026

Spring Autowiring — @Autowired, @Qualifier, @Primary, and Why Field Injection Is a Code Smell

Master Spring autowiring: @Autowired, @Qualifier, @Primary, constructor vs field injection.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Prefer constructor injection: explicit dependencies, immutable fields, testable without Spring
  • @Primary marks a bean as the default when multiple candidates exist for the same type
  • @Qualifier('beanName') selects a specific bean by name when @Primary isn't enough
  • Field injection (@Autowired on fields) hides dependencies and makes unit testing painful
  • @Autowired(required=false) lets you inject optional dependencies — use sparingly
✦ Definition~90s read
What is Spring Autowiring?

Spring autowiring is the mechanism by which the Spring IoC container automatically resolves and injects dependencies into beans. When a bean has a dependency (constructor parameter, @Autowired field, or @Autowired setter), Spring looks in its bean registry for a matching bean — first by type, then by name if there are multiple candidates.

Spring autowiring is like a smart receptionist that automatically connects your phone calls to the right department.

This automatic wiring eliminates the need for manual bean lookup and makes dependency relationships declarative.

Autowiring resolution proceeds in a defined order: (1) find all beans assignable to the required type; (2) if exactly one found, inject it; (3) if multiple found, check for @Primary — inject the primary bean; (4) if no @Primary, check if any bean name matches the parameter/field name; (5) if multiple or no match, check @Qualifier annotations; (6) if still ambiguous, throw NoUniqueBeanDefinitionException. Understanding this resolution order explains most autowiring bugs.

The three injection styles — constructor, setter, and field — all use the same underlying resolution mechanism. The difference is when injection happens (constructor injection happens during instantiation, field/setter injection happens after via reflection) and what guarantees they provide (constructor injection guarantees dependencies are non-null when the constructor body executes; field injection injects after construction so the object exists in a partially-initialized state briefly).

Plain-English First

Spring autowiring is like a smart receptionist that automatically connects your phone calls to the right department. You say 'I need someone from accounting' and Spring finds the right bean. @Primary is the default extension (always rings first), @Qualifier is dialing a direct number, and constructor injection is publishing your phone requirements on your business card — transparent and auditable.

Field injection is one of those things that seems harmless until it's 2 AM and you're trying to unit test a service that has six private @Autowired fields and won't instantiate without a Spring context. I've reviewed hundreds of codebases and the single most consistent quality indicator is how teams handle dependency injection. Teams that use constructor injection consistently write more testable, more maintainable services. Teams that use field injection everywhere inevitably end up with services that have twelve dependencies and nobody noticed because you can't see them in the constructor signature.

The field-vs-constructor debate was settled years ago in the Spring community. The Spring team officially recommends constructor injection for mandatory dependencies. IntelliJ IDEA warns about field injection. Yet I still see production codebases with @Autowired fields everywhere, often because a tutorial used field injection and everyone copy-pasted from it.

Beyond the injection style debate, there are real production problems caused by misconfigured autowiring. Ambiguous dependency resolution (multiple beans of the same type) causes startup failures. Wrong @Qualifier selects the wrong implementation silently. @Primary on the wrong bean causes subtle bugs where the wrong database is used for certain operations. These aren't theoretical concerns — I've seen all of them in production outage post-mortems.

This article covers every aspect of Spring autowiring with production-quality examples. We'll go through @Autowired semantics, @Primary and @Qualifier for disambiguation, constructor vs field vs setter injection trade-offs, handling optional dependencies, and the right patterns for complex multi-implementation scenarios like feature flags and strategy patterns.

Constructor Injection — The Only Way to Do It Right

Constructor injection is the canonical form of dependency injection in modern Spring applications. With constructor injection, all required dependencies are listed as constructor parameters. Spring resolves them and passes them when creating the bean. The result: dependencies are guaranteed non-null by the time the constructor body executes, fields can be declared final (immutable), and the class is a plain Java object that can be instantiated in unit tests without Spring.

The immutability benefit is underrated. When you declare fields as private final, you get compile-time guarantees that the dependency is set once and never changed. This eliminates an entire class of bugs where a test accidentally changes a shared dependency. It also makes thread-safety analysis easier — you know the field won't change after construction.

Testability is the killer feature. With constructor injection, a unit test creates the class with mock dependencies: new OrderService(mockRepository, mockPaymentGateway). No Spring context, no @SpringBootTest, no slow startup. The test runs in milliseconds. With field injection, you either need a Spring test context (slow) or use reflection hacks like ReflectionTestUtils.setField() (fragile and verbose). Teams that use field injection often end up writing @SpringBootTest integration tests for what should be pure unit tests, slowing CI pipelines significantly.

Spring Boot 3.x generates a warning for field injection when used with spring-boot-devtools or certain analyzers. IntelliJ IDEA marks field injection as a warning by default ('Field injection is not recommended'). The writing is on the wall — if you're writing field injection today, you're accumulating technical debt.

One common objection to constructor injection is 'what if I have many dependencies?' — if your constructor has more than 4-5 parameters, that's a signal the class has too many responsibilities (violates Single Responsibility Principle), not a reason to switch to field injection. Refactor the class to extract responsibilities, or group related dependencies into a configuration object.

Field injection with @Autowired is a code smell
Field injection (@Autowired on private fields) makes classes impossible to instantiate without Spring, hides dependencies from callers, prevents final declarations, and breaks unit testing. Lombok's @RequiredArgsConstructor generates constructor injection automatically from final fields — use it.
Production Insight
With constructor injection and Lombok's @RequiredArgsConstructor, a service class needs zero injection boilerplate: declare final fields, add @RequiredArgsConstructor, Spring sees the generated constructor automatically.
Key Takeaway
Constructor injection = immutable fields + visible dependencies + unit-testable without Spring. Make it your default and never look back.

@Primary and @Qualifier — Resolving Ambiguous Dependencies

When your application context contains multiple beans of the same type, Spring needs to know which one to inject. Two mechanisms handle this: @Primary marks a bean as the default choice, and @Qualifier specifies an exact bean by name at the injection point. Understanding when to use each — and their interaction — prevents the subtle 'wrong bean injected' bugs that are hard to catch without integration tests.

@Primary is a declaration on the bean itself. It says 'if there are multiple beans of my type, prefer me when the injection point doesn't specify otherwise.' This is appropriate when you have a clear default implementation and only occasionally need a different one. The @Primary bean is the fallback when there's ambiguity. If you @Autowire a type and there's one @Primary candidate and several non-primary ones, the @Primary bean wins without needing @Qualifier at every injection point.

@Qualifier is a declaration at the injection point. It says 'inject specifically the bean with this name, regardless of @Primary.' @Qualifier overrides @Primary. This is the right tool when you have multiple beans of the same type and the correct choice depends on the context: a main database vs an audit database, an external payment gateway vs an internal mock, a caching repository vs a direct repository.

A pattern that emerges in complex applications is interface + multiple implementations + strategy selection. For example, a NotificationService interface implemented by EmailNotification, SmsNotification, and PushNotification. Rather than @Qualifier at every injection point, inject a Map<String, NotificationService> and Spring will give you all implementations keyed by bean name. Or inject a List<NotificationService> and Spring gives you all implementations. This pattern enables registering new notification channels without changing injection code.

For feature flags and A/B testing, combine @ConditionalOnProperty with @Primary to switch implementations based on configuration. This is cleaner than @Qualifier because it doesn't require changing injection points — just flip the configuration. The @Primary annotation on the feature-flagged bean activates it when the condition is true, and the default implementation's @Primary activates when the condition is false.

Inject List or Map to get all implementations
Spring automatically injects all beans of type T into List<T> (in definition order, or @Order-specified order). Map<String, T> gives you bean-name-keyed access to all implementations. This is the cleanest pattern for strategy/plugin architectures where you want to iterate over or select from all implementations.
Production Insight
Never rely on @Primary for correctness-critical injection points like DataSources. Always use @Qualifier explicitly. @Primary is for convenience defaults, not for safety guarantees.
Key Takeaway
@Primary = default when ambiguous. @Qualifier = explicit selection. Use @Qualifier for anything where injecting the wrong bean causes a correctness bug, not just a preference mismatch.

Optional Dependencies and @Autowired(required=false)

Not all dependencies are mandatory. Spring supports optional injection through @Autowired(required=false), Optional<T> injection, and @Nullable. These patterns let you build beans that degrade gracefully when optional infrastructure isn't available — a monitoring agent, a cache, a feature-flag service — without failing the entire context startup.

@Autowired(required=false) sets the field/setter to null if no matching bean exists. This is appropriate for optional plugins or extensions that enhance behavior but aren't required for the bean to function. The downside is you must null-check before every use, which is easy to forget. A missing null check on an 'optional' dependency causes NPEs only when the optional bean happens to be absent — like in certain test environments — which makes bugs intermittent.

The cleaner approach is Optional<T> injection. Spring 4.3+ supports injecting Optional<MyService>. If a matching bean exists, Optional.isPresent() returns true. If not, you have an empty Optional. This forces the caller to explicitly handle the absent case through the Optional API, making 'this might be null' visible in the code.

For infrastructure dependencies that should exist in production but might be absent in tests, @ConditionalOnBean at the configuration level is better than optional injection everywhere. Create a configuration class that conditionally creates a null-object implementation (a no-op bean) when the real infrastructure isn't available. This way, your service always has a non-null dependency — it just might be a no-op implementation. No null checks needed.

ObjectProvider<T> is the most powerful optional injection mechanism. It's lazy (doesn't try to resolve the bean until you call get()), optional (getIfAvailable() returns null rather than throwing if the bean is absent), and handles multiple beans (stream() gives you all matching beans). In library code where you don't control the consumer's application context, ObjectProvider<T> is the safest way to express optional dependencies.

Prefer null-object pattern over null checks
Instead of Optional<T> or @Autowired(required=false) requiring null checks throughout your code, register a no-op implementation in profiles or with @ConditionalOnMissingBean. Your service always gets a non-null dependency — it just might do nothing. This keeps business logic clean of infrastructure concerns.
Production Insight
We used ObjectProvider<FeatureFlagService> in all our core services during a gradual rollout of a new feature flag system. Before the FeatureFlagService bean was added to the context (it was added incrementally), all services gracefully fell back to 'feature disabled' behavior.
Key Takeaway
Optional<T> injection makes optional dependencies explicit in code. ObjectProvider<T> is the most flexible option for library code. Null-object pattern is cleanest for long-term maintainability.

Custom Qualifiers and Annotation-Based Injection

String-based @Qualifier('beanName') works but has a critical weakness: it's not refactor-safe. Rename the bean and the qualifier string silently becomes stale — no compile-time error, just a runtime NoSuchBeanDefinitionException. Custom qualifier annotations solve this by replacing string names with type-safe annotation types that refactoring tools understand.

A custom qualifier is a meta-annotation: you create an annotation annotated with @Qualifier, then use your custom annotation at both the bean definition and injection point. When Spring sees your annotation on a field, it looks for beans also annotated with the same annotation. If you rename the annotation class, the compiler catches all usages immediately.

Custom qualifiers are also more expressive than string names. Instead of @Qualifier('primaryDatabase') and @Qualifier('auditDatabase'), you can have @PrimaryDatabase and @AuditDatabase as proper annotation types. The intent is clear, the type is safe, and the tooling support is first-class.

For complex selection scenarios, custom qualifiers can carry attributes. A @DataSource(tenant='us-east') combined with a @DataSource(tenant='eu-west') lets you select datasources by attribute rather than by string name. This is particularly powerful for multi-tenant architectures and region-aware deployments.

Beyond qualifiers, Spring supports JSR-330 annotations (javax.inject / jakarta.inject) as alternatives to Spring's own annotations. @Inject works like @Autowired, @Named works like @Qualifier('name'). These are portable across CDI-compliant containers (like WildFly or Quarkus), but in a pure Spring Boot application, using Spring's own annotations is standard and recommended.

Create custom qualifiers for multi-DataSource applications
In any application with multiple DataSources (main, audit, read-replica, reporting), create a custom qualifier annotation for each. String-based @Qualifier is error-prone — a typo causes a runtime error, a rename causes a silent bug. Custom qualifier annotations give you compile-time safety and IDE navigation.
Production Insight
We had a production incident where @Qualifier('replicaDataSource') at an injection point was never updated when the bean was renamed to 'readReplicaDataSource'. The string silently fell through to @Primary (the main database), causing read queries to hit the write master for three weeks before we noticed the replica was idle.
Key Takeaway
Custom qualifier annotations are type-safe, refactor-friendly, and more expressive than string-based @Qualifier. Create them for any multi-instance bean scenario where correctness matters.

Setter Injection — When It's Actually Appropriate

Setter injection gets unfairly lumped together with field injection in 'bad DI practices' discussions. They're different. Field injection uses reflection to set a private field after construction — coupling the class to Spring's reflection machinery and making the field look like it could be null to any reader. Setter injection uses an explicit public method, which means: the dependency is visible in the API, it can be called in tests without Spring, and it can be null-checked in the setter.

The legitimate use case for setter injection is optional dependencies with a default value. If a dependency might not exist, you can initialize the field to a no-op default and let the setter overwrite it if the real bean is available. This is cleaner than Optional<T> for cases where you always want a non-null value:

``java @Autowired(required = false) public void setMetricsCollector(MetricsCollector collector) { this.metricsCollector = collector; // only called if bean exists } ``

Here, the field has a no-op MetricsCollector initialized in the class (or constructor), and Spring calls the setter if a MetricsCollector bean exists. You never have null — you have either the real implementation or the no-op.

The Spring Framework documentation itself recommends setter injection for optional dependencies and constructor injection for mandatory dependencies. This nuanced guidance gets lost in the 'always use constructor injection' shorthand that circulates online. The real rule is: mandatory dependencies → constructor injection; optional dependencies with defaults → setter injection (or ObjectProvider).

Another legitimate setter injection use case is circular dependency resolution as a last resort. If two beans genuinely need each other (and you can't restructure), one can use constructor injection and the other setter injection. Spring creates both via constructors first, then resolves setter dependencies — breaking the circular creation requirement. But again, treat this as a design smell, not a solution.

Setter injection for optional deps with defaults is valid
Constructor injection for mandatory. Setter injection for optional with defaults. Field injection for neither — it has the downsides of setter injection (post-construction) without the upside (visible setter, default initialization). The 'never use setter injection' rule is an oversimplification.
Production Insight
We use setter injection with @Autowired(required=false) for our observability hooks: tracing, metrics, and audit logging. In unit tests these beans don't exist, so all fields stay at NOOP implementations. In production they're replaced with real implementations without any test-specific configuration.
Key Takeaway
Setter injection is valid for optional dependencies with sensible defaults. The pattern: initialize to NOOP in field declaration, @Autowired(required=false) setter overwrites with real implementation when available.

Autowiring in Configuration Classes and @Bean Methods

Configuration classes (@Configuration) have their own autowiring mechanics that differ subtly from @Component beans. Understanding these differences prevents subtle bugs in complex configurations.

In a @Configuration class, @Bean methods can have parameters. Spring treats these parameters exactly like constructor injection — it resolves them from the application context. This means you can autowire any bean in the context as a @Bean method parameter:

``java @Bean public MyService myService(DataSource dataSource, CacheManager cacheManager) { return new MyService(dataSource, cacheManager); } ``

This is actually the preferred way to express dependencies in configuration classes — explicit parameters make the dependency graph visible. The alternative, @Autowired fields in @Configuration classes, works but is less clear.

A critical subtlety with @Configuration classes: they're processed by CGLIB subclassing (when annotated with @Configuration, not @Component). This means that when one @Bean method calls another @Bean method within the same configuration class, Spring intercepts the call and returns the existing singleton bean from the context, not a new instance. This 'inter-bean method call interception' is what makes patterns like this work:

``java @Bean public ServiceA serviceA() { return new ServiceA(sharedDataSource()); } @Bean public ServiceB serviceB() { return new ServiceB(sharedDataSource()); } @Bean public DataSource sharedDataSource() { return new HikariDataSource(config); } ``

Both serviceA and serviceB call sharedDataSource(), but they get the same DataSource instance — Spring intercepts the call and returns the singleton. This doesn't work if the class is annotated with @Component instead of @Configuration — in that case, sharedDataSource() is a regular method call and creates a new DataSource each time.

For large applications, splitting configurations into multiple @Configuration classes that import each other (via @Import) or use @Bean method parameters for cross-configuration dependencies is much cleaner than one massive configuration class. @Import(OtherConfig.class) makes dependencies between configurations explicit and navigable.

@Component vs @Configuration: CGLIB interception only applies to @Configuration
If you replace @Configuration with @Component on a configuration class, CGLIB interception is disabled. Calling one @Bean method from another within the class creates new instances each time — the singleton contract is broken. You'll end up with multiple instances of what should be singletons, causing configuration inconsistencies. Always use @Configuration for configuration classes.
Production Insight
Split large configuration classes by concern: DataSourceConfig, SecurityConfig, MessagingConfig, ServiceConfig. Use @Import to express dependencies. This makes the configuration dependency graph navigable and reduces merge conflicts.
Key Takeaway
@Configuration enables CGLIB interception — @Bean method calls within the class return the singleton. @Component does not. Always use @Configuration for config classes, not @Component.

Field Injection — The Silent Sabotage That Will Haunt Your Tests

Drop the @Autowired on private fields. Now. That cute shortcut creates a hidden coupling that explodes the first time you need to write a unit test without booting the entire Spring context. Field injection bypasses constructors entirely, so you cannot mark dependencies as final. No final means no immutability. No immutability means every test becomes a brittle mess of reflection hacks or PowerMock dependencies. Worse: Spring Boot can still inject the field even if it's null, silently passing your null checks until runtime. The WHY is simple — field injection hides the dependency graph. A class with seven @Autowired fields looks clean in the IDE but creates a constructor with seven invisible parameters. Junior devs love it because it feels fast. Senior devs hate it because it kills testability. Never use field injection in production code. Period. If you inherit a codebase riddled with it, refactor incrementally during maintenance cycles. Your future self will thank you when a simple configuration change doesn't trigger a cascade of NullPointerExceptions.

FieldInjectionNightmare.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge — java tutorial
// BAD — Hidden dependency, breaks tests
@Component
public class OrderService {
    @Autowired
    private PaymentGateway paymentGateway; // Not final, not testable without Spring

    public void processOrder(Order order) {
        paymentGateway.charge(order.getTotal()); // NPE if Spring isn't running
    }
}

// GOOD — Constructor injection, immutable, testable
@Component
public class OrderService {
    private final PaymentGateway paymentGateway;

    public OrderService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }

    public void processOrder(Order order) {
        paymentGateway.charge(order.getTotal());
    }
}
Output
No compile-time error with field injection.
Runtime NullPointerException when PaymentGateway bean is missing.
Constructor injection throws BeanCreationException immediately on startup.
Production Trap:
Field injection won't fail during compilation if the bean is missing. Your integration tests might pass because Spring Boot auto-configures everything. Then it explodes at 3 AM in production when a deployment removes a transitive dependency.
Key Takeaway
Inject through constructors. Keep fields final. Blind field injection is technical debt masquerading as convenience.

Autowire Resolution Order — Why Your Bean Is Not the One Spring Picks

Spring's autowiring resolution follows a strict hierarchy that catches most devs off guard. When Spring encounters an @Autowired dependency, it first looks for a single matching bean by type. If it finds exactly one, done. If it finds multiple, the drama begins. Spring then checks for @Primary — one bean wearing the crown. If no @Primary exists, it falls back to @Qualifier for explicit naming. If neither is present, it silently tries to match by field name against bean names. That last step is the trap. Your interface has two implementations: CreditCardProcessor and PayPalProcessor. Your field is named processor. Spring picks whichever bean it finds first in the scanning order — usually alphabetical. That's not a feature, it's a bug waiting to happen. Always use @Qualifier or @Primary for clarity. Never rely on field name matching in production. The resolution order is: type -> @Primary -> @Qualifier -> field name. Know it. Use it. Code defensively.

ResolutionOrder.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// io.thecodeforge — java tutorial
public interface PaymentGateway {
    boolean processPayment(double amount);
}

@Component
public class StripeGateway implements PaymentGateway {
    public boolean processPayment(double amount) { return true; }
}

@Component
@Primary
public class PayPalGateway implements PaymentGateway {
    public boolean processPayment(double amount) { return true; }
}

@Component
public class CheckoutService {
    private final PaymentGateway paymentGateway;

    // Spring picks PayPalGateway because @Primary overrides type matching
    public CheckoutService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
}

@Component
public class AdminCheckoutService {
    private final PaymentGateway stripeGateway; // Field name matches bean name

    // Spring picks StripeGateway by field name fallback
    public AdminCheckoutService(PaymentGateway stripeGateway) {
        this.stripeGateway = stripeGateway;
    }
}
Output
CheckoutService uses PayPalGateway (due to @Primary).
AdminCheckoutService uses StripeGateway (due to field name matching 'stripeGateway').
Resolution Pitfall:
Field name matching is case-sensitive and fragile. Rename your field, and you silently switch implementations with zero warnings. Always make resolution explicit.
Key Takeaway
Resolution order: type → @Primary → @Qualifier → field name. Fix ambiguity before the compiler does.
● Production incidentPOST-MORTEMseverity: high

Wrong DataSource Injected in Multi-Tenant App

Symptom
Audit database was empty. Main database audit_log table was growing unexpectedly. Data was correct but in the wrong place. Discovered three weeks after deployment during a compliance audit.
Assumption
The developer assumed the AuditRepository was injecting the auditDataSource because the repository class was in the 'audit' package with a specific @Repository name.
Root cause
A new developer added @Primary to the mainDataSource bean while following a tutorial, without realizing the auditDataSource was supposed to be the primary source for the audit service module. The AuditRepository was getting the mainDataSource because it was @Primary, overriding the @Qualifier('auditDataSource') that had been removed during a refactor.
Fix
Removed @Primary from mainDataSource. Added explicit @Qualifier('mainDataSource') and @Qualifier('auditDataSource') to all injection points that dealt with multiple DataSource beans. Added an integration test that verifies each repository class connects to the correct DataSource by checking the JDBC URL after injection.
Key lesson
  • @Primary should be used sparingly and intentionally.
  • In systems with multiple beans of the same type where correctness matters (DataSources, message queues), use @Qualifier at every injection point.
  • Never rely on @Primary for correctness — use it only for default/convenience disambiguation.
Production debug guideSymptom → root cause → fix for the most common autowiring failures5 entries
Symptom · 01
NoUniqueBeanDefinitionException: expected single matching bean but found multiple
Fix
Spring found multiple beans of the requested type and couldn't decide which to inject. Run curl http://localhost:8080/actuator/beans | jq to list all beans and find duplicates. Solutions in order of preference: (1) add @Qualifier at the injection point to select a specific bean by name; (2) add @Primary to the bean that should be the default; (3) if the ambiguity is wrong (shouldn't have two beans), find where the duplicate is registered and remove it. Check @ComponentScan scope — it might be picking up beans from a jar dependency.
Symptom · 02
NoSuchBeanDefinitionException: No qualifying bean of type X
Fix
Spring can't find any bean of the required type. Check: (1) is the class annotated with @Component/@Service/@Repository/@Controller or returned from a @Bean method? (2) Is its package included in @ComponentScan? (3) Is it conditionally created (@ConditionalOnProperty etc.) and the condition is false? (4) Is the required type an interface — and the implementation not implementing that interface? Run --debug mode at startup to see all auto-configuration decisions.
Symptom · 03
UnsatisfiedDependencyException wrapping BeanCreationException during startup
Fix
This is usually a chain of dependency failures. Read the full stack trace from the bottom — the root cause is at the bottom, not the top. Common causes: a dependency bean's @PostConstruct threw an exception; a required property is missing (causing @Value injection to fail); a @ConditionalOnMissingBean condition prevented a required bean from being created. Add spring.main.banner-mode=off and logging.level.org.springframework=DEBUG to get verbose wiring logs.
Symptom · 04
Wrong implementation injected — @Autowired injects unexpected bean
Fix
Check the bean name. Spring falls back to bean name matching when there are multiple candidates. If your field is named 'userService' and there are two beans UserServiceImpl and CachingUserService (both implementing UserService), Spring injects whichever bean name matches 'userService'. To be explicit, add @Qualifier. Check if there's a @Primary on a bean you didn't intend. List all beans of the type with context.getBeanNamesForType(UserService.class) in a CommandLineRunner.
Symptom · 05
Field @Autowired is null at runtime (inside a method call)
Fix
The class with the @Autowired field was instantiated with 'new' instead of retrieved from the Spring context. Spring only injects fields of beans it manages. Instantiating a class with 'new SomeService()' gives you an instance with all @Autowired fields null. Fix: inject the dependency as a constructor parameter, or get the instance from the ApplicationContext, or annotate the class properly and let Spring create it.
★ Autowiring Debug Cheat SheetFast commands to diagnose autowiring issues
Find all beans of a specific type
Immediate action
Query the actuator beans endpoint
Commands
curl -s http://localhost:8080/actuator/beans | jq '[.contexts[].beans | to_entries[] | select(.value.type | contains("UserService"))]'
curl -s http://localhost:8080/actuator/beans | jq '.contexts[].beans["userServiceImpl"]'
Fix now
Check 'aliases', 'scope', and 'resource' fields to understand where the bean is registered and whether it's the expected one
Application fails to start with NoUniqueBeanDefinitionException+
Immediate action
Find all candidates and add @Qualifier
Commands
java -jar app.jar --debug 2>&1 | grep -A5 'NoUniqueBeanDefinitionException'
grep -r '@Component\|@Service\|@Repository\|@Bean' src/ | grep -i 'myservice'
Fix now
Add @Qualifier('specificBeanName') at the injection point, or add @Primary to the default implementation
Verify injection is working in production without restarting+
Immediate action
Hit the conditions endpoint to see auto-configuration decisions
Commands
curl -s http://localhost:8080/actuator/conditions | jq '.contexts[].positiveMatches | keys[]' | grep -i myfeature
curl -s http://localhost:8080/actuator/beans | jq '.contexts[].beans | with_entries(select(.value.aliases | length > 0))'
Fix now
If expected bean not present, check @ConditionalOn* conditions — the conditions endpoint shows why beans were/weren't created
Injection Style Comparison
AspectConstructor InjectionSetter InjectionField Injection
Immutability✅ final fields possible❌ fields must be mutable❌ fields must be non-final
NPE safety✅ guaranteed non-null⚠️ must check if required=false⚠️ null if bean absent
Unit testability✅ new MyClass(mock)✅ myClass.setSvc(mock)❌ needs reflection or Spring
Circular deps❌ fails fast (good)✅ can resolve (risky)✅ can resolve (risky)
Visibility✅ explicit in signature✅ explicit setter method❌ hidden private field
Optional deps⚠️ needs Optional<T>✅ @Autowired(required=false)⚠️ @Autowired(required=false)
Spring coupling❌ no Spring import needed❌ no Spring import needed✅ requires @Autowired
Recommended forMandatory dependenciesOptional with defaultsNever in new code

Key takeaways

1
Use constructor injection for all mandatory dependencies. It gives you immutable fields, visible dependencies, and unit-testable classes without Spring context.
2
@Primary marks the default bean for type-based injection resolution. @Qualifier at the injection point explicitly selects a specific bean and overrides @Primary. Use @Qualifier for correctness-critical injection (DataSources, payment gateways).
3
Field injection (@Autowired on private fields) hides dependencies, prevents final declarations, and requires Spring or reflection for unit testing. Don't use it in new code.
4
Inject List<T> to get all implementations in @Order-specified order, or Map<String, T> to get all implementations keyed by bean name. This is the cleanest strategy pattern implementation.
5
In @Configuration classes (not @Component), calling one @Bean method from another returns the singleton from the context via CGLIB interception
not a new instance. This is why you can share beans by calling @Bean methods within a @Configuration class.

Common mistakes to avoid

6 patterns
×

Using @Autowired on private fields (field injection)

Symptom
Unit tests require @SpringBootTest or ReflectionTestUtils.setField(), CI runs take 30+ seconds for what should be millisecond unit tests
Fix
Switch to constructor injection. Add Lombok @RequiredArgsConstructor and declare fields as final to eliminate boilerplate
×

Adding @Primary to a DataSource without considering all consumers

Symptom
Repositories that were injecting by type start getting the wrong DataSource silently — data goes to wrong database
Fix
Remove @Primary from DataSources in multi-datasource configurations. Use @Qualifier at every DataSource injection point. Add integration tests that verify each repository's DataSource URL
×

Using string-based @Qualifier('beanName') that gets stale on refactoring

Symptom
Runtime NoSuchBeanDefinitionException after renaming a bean — the string qualifier wasn't updated
Fix
Create custom qualifier annotations (@PrimaryDb, @AuditDb) that are type-safe and refactor-safe
×

Instantiating beans with 'new' and expecting @Autowired to work

Symptom
NullPointerException at runtime on @Autowired fields — the object was created outside Spring's context
Fix
Either inject the bean via Spring (retrieve from ApplicationContext or inject as a dependency), or remove @Autowired and use constructor injection with explicit parameters
×

@Autowired on a @Configuration class field instead of @Bean method parameter

Symptom
Configuration dependency isn't visible in @Bean method signatures, making the dependency graph hard to follow
Fix
Move dependencies to @Bean method parameters — Spring resolves them from context automatically, and the method signature explicitly shows what the bean needs
×

Not handling NoUniqueBeanDefinitionException by understanding why multiple beans exist

Symptom
Adding @Primary as a quick fix without understanding why two beans of the same type exist — masking a registration bug
Fix
Investigate why multiple beans exist. Often it's a @ComponentScan picking up a test bean in production, or a bean registered twice. Fix the root cause before reaching for @Primary
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What's the difference between @Autowired, @Resource, and @Inject?
Q02JUNIOR
Why is constructor injection preferred over field injection?
Q03SENIOR
How does Spring resolve ambiguous dependencies when multiple beans of th...
Q04SENIOR
When would you use @Qualifier vs @Primary?
Q05SENIOR
What happens when you call a @Bean method from another @Bean method in a...
Q06SENIOR
How do you inject all beans of a particular type?
Q07SENIOR
How do you handle circular dependencies in Spring? What's the recommende...
Q08SENIOR
What is ObjectProvider and when do you use it over @Autowired?
Q01 of 08JUNIOR

What's the difference between @Autowired, @Resource, and @Inject?

ANSWER
@Autowired is Spring-specific, resolves by type first then by name. @Resource is JSR-250, resolves by name first then by type — it's on jakarta.annotation. @Inject is JSR-330 (jakarta.inject), resolves by type like @Autowired and requires a @Named qualifier for name-based selection. In Spring Boot applications, @Autowired is standard. @Inject is used when writing code portable across CDI containers. @Resource is useful when you need name-first resolution behavior.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Do I need @Autowired on constructor parameters in Spring Boot 3.x?
02
Can @Autowired inject a bean from a parent context?
03
What's the difference between @ComponentScan and @Import?
04
Can I @Autowire a class that implements multiple interfaces?
05
What happens if I annotate a bean with both @Primary and @Qualifier?
🔥

That's Spring Boot. Mark it forged?

11 min read · try the examples if you haven't

Previous
Spring Boot Bean Lifecycle
17 / 21 · Spring Boot
Next
Scheduling Tasks with @Scheduled