Beans and dependency injection

Understand how Spring constructs application objects, chooses dependencies, and manages their lifetime.

In this article

A Spring bean is an object managed by the application context. The context reads bean definitions, resolves dependencies, creates objects, and applies lifecycle callbacks and post-processors. A class does not become a bean merely because another bean uses it. An object created with new remains an ordinary Java object unless you explicitly register it.

Make dependencies visible

Constructor injection makes required collaborators part of an object's construction contract. A service cannot exist without the dependencies it needs, and a unit test can supply them without starting Spring. With one constructor, Spring does not require @Autowired.

Consider these application type fragments in a scanned package. They assume Spring's @Component and @Service. The senders return a delivery description so that the selected implementation is visible without making a network call:

interface ReceiptSender {
	String send(String orderId);
}

@Component("email")
class EmailReceiptSender implements ReceiptSender {
	public String send(String orderId) {
		return "email receipt for " + orderId;
	}
}

@Component("archive")
class ArchiveReceiptSender implements ReceiptSender {
	public String send(String orderId) {
		return "archived receipt for " + orderId;
	}
}

@Service
class ReceiptService {
	private final ReceiptSender sender;

	ReceiptService(ReceiptSender sender) {
		this.sender = sender;
	}

	public String issue(String orderId) {
		return sender.send(orderId);
	}
}

The context cannot construct ReceiptService. Both email and archive match ReceiptSender, neither is primary, and the parameter name sender does not select either bean. Startup fails with an unsatisfied dependency caused by NoUniqueBeanDefinitionException. This summary is illustrative, not verbatim log output:

ReceiptService needs one ReceiptSender.
Eligible beans: email, archive.
No single candidate selected; context startup fails.

Replace the constructor with this fragment. Qualifier comes from org.springframework.beans.factory.annotation:

ReceiptService(@Qualifier("email") ReceiptSender sender) {
	this.sender = sender;
}

Startup now resolves the dependency. Calling issue("o-42") on the injected ReceiptService returns email receipt for o-42. Both sender beans still exist. The qualifier narrows this injection point; it does not remove archive from the context.

Fig 1Injection selects one registered candidate
STUDY MODEL
Inspect one part at a timeDependency
  1. Dependency

    ReceiptService requires a ReceiptSender.

  2. Selected bean

    The email qualifier selects EmailReceiptSender, bean name email.

  3. Other bean

    ArchiveReceiptSender, bean name archive, remains registered but is not injected at this point.

The email qualifier selects the dependency; it does not unregister the archive bean.

How to read this model

A connector selector beside two registered component holders. The object is a teaching analogy. Inspecting a part highlights its role; it does not execute or simulate the software. Cabinet trays are records, workpieces are data, and latches are checks. Unless an explanation states a quantity, compartments and sample workpieces do not denote data counts. Part dimensions are not a quantitative scale.

@Primary identifies a genuine default for single-valued injection across callers. Prefer a qualifier when a particular caller needs a specific role. Missing dependencies also normally fail startup. Keep dependencies in final fields, but remember that a final reference does not make the collaborator thread-safe.

Choose who declares the bean

Stereotypes such as @Component, @Service, and @Repository register application classes through component scanning. Their names communicate responsibility. @Repository also participates in persistence exception translation when the relevant infrastructure is enabled.

An @Bean method declares an object explicitly, which fits library classes you cannot annotate and construction that needs configuration. This alternative registration fragment assumes the types above, with @Service removed from ReceiptService so it is not registered twice:

@Configuration(proxyBeanMethods = false)
class ReceiptConfiguration {
	@Bean
	ReceiptService receiptService(@Qualifier("email") ReceiptSender sender) {
		return new ReceiptService(sender);
	}
}

With proxyBeanMethods = false, calling another @Bean method directly is an ordinary Java call, not a lookup of the managed bean. Method parameters avoid that distinction and make the dependency explicit. Do not register the same implementation through both scanning and a factory method unless you intend two bean definitions.

Scope controls reuse, not concurrency

The default singleton scope means one instance per bean definition per container. It does not mean one instance per Java class across the JVM. Request threads commonly call the same singleton service concurrently.

A currentUser field on a singleton allows one request to overwrite another request's state. The following interleaving needs no unusual timing:

Step Request A Request B Shared singleton field
1 Assigns currentUser = "alice" alice
2 Pauses before creating the receipt Assigns currentUser = "bob" bob
3 Reads currentUser and creates Bob's receipt bob

Keep request data in method arguments and local variables. For example, issue(String orderId) above stores no order ID on the service between calls.

Design What is reused? Where does request data belong?
Stateless singleton service One service shared by requests A and B Separate method arguments and locals for A and B
Singleton with a mutable currentUser field One service and one shared field Unsafe without an appropriate concurrency design
@RequestScope bean in a web application One target instance for request A, another for B State belongs to that request, not later background work

Request scope is a lifetime rule, not a general thread-safety guarantee. Session scope also does not serialize simultaneous requests from the same session.

Prototype scope creates an instance each time the container resolves that bean. A prototype injected into a singleton is resolved once during the singleton's construction. It is not recreated for each service call. An ObjectProvider can obtain a new instance when needed. A scoped proxy can instead resolve the current request-scoped target, but only while that scope is active.

Lifecycle and dependency cycles

Initialization callbacks such as @PostConstruct run after dependency injection. Destruction callbacks such as @PreDestroy release managed resources when the context closes normally. Boot 4 uses the jakarta.annotation package. The container does not call destruction callbacks for prototype instances, so their owner must release resources.

Do not rely on transactional or asynchronous interception inside initialization callbacks. The bean is still being initialized, and the final proxy is not the object executing that callback.

A constructor cycle has no valid construction order. If OrderService requires PaymentService and the reverse is also true, extract the shared rule or move orchestration into a third service. Setter injection or lazy lookup may postpone the cycle, but it leaves the responsibilities tangled.

Continue with Java essentials, configuration, and proxies.

Sources

Keep track of your reading.

Your reading progress stays in this browser.