Bean basics
A bean is an object managed by the Spring container.
A bean definition tells Spring how to create an object. The resulting bean instance can then be injected into another bean. By default, a bean is a singleton within one ApplicationContext and one bean definition; this is not the same as a JVM-wide Singleton-pattern object.
@Component
A general-purpose class that component scanning should register.
@Service
A component whose role is application or business service logic.
@Repository
A persistence component. Its additional database behaviour matters later in this series.
These stereotypes describe roles; @Service and @Repository are specialised forms of @Component.
It helps to separate two things that are often both called “a bean.” A bean definitionis Spring's recipe: type, name, scope and construction information. A bean instance is the actual Java object created from that recipe.
@Service
public class OrderService {
// Spring registers a bean definition while scanning.
// During context refresh, it creates the OrderService object.
}Registration phase
Spring scans metadata and records that an OrderService bean can be created.
Instantiation phase
Spring calls the constructor after it has worked out what every parameter should receive.
Component scanning does not inject an annotation into the object. It discovers a class, registers a definition, and later creates and wires the object through normal Java constructors.
What makes a bean different?
The Java object is ordinary; container ownership adds behaviour around it.
OrderService service =
new OrderService(notificationSender);@Service
public class OrderService {
public OrderService(NotificationSender notificationSender) {
// Spring calls this same Java constructor.
}
}Both values are instances of the same Java class. The second is a bean because the context owns its creation, dependency resolution, scope and lifecycle. Annotating a class does not prevent manual construction, although a manually created instance is not automatically registered with Spring.
Constructor injection
The constructor makes required dependencies explicit.
package com.example.orders.service;
import org.springframework.stereotype.Service;
import com.example.orders.notification.NotificationSender;
@Service
public class OrderService {
private final NotificationSender notificationSender;
public OrderService(NotificationSender notificationSender) {
this.notificationSender = notificationSender;
}
}Spring sees one constructor, resolves its parameters from the context and calls it. @Autowired is unnecessary when a class has a single constructor. The final field prevents the required dependency from being replaced after construction.
OrderService(
NotificationSender notificationSender
)
// Question Spring asks:
// "Which bean in this context can satisfy NotificationSender?"Injection is primarily type-based. Spring does not care about the local field name when one unique bean satisfies the type. It first identifies the constructor, then resolves each parameter, then invokes the constructor with those instances.
One constructor
No @Autowired required
Spring uses the only available constructor.
Several constructors
The choice must be unambiguous
Mark the intended injectable constructor or redesign the alternatives so construction is clear.
@Service
public class OrderService {
@Autowired
private NotificationSender notificationSender;
}A normal Java caller can construct this class without setting the field, so the object is temporarily incomplete. The dependency is also absent from the public construction contract. Constructor injection avoids both problems.
Multiple implementations
Matching the type is necessary, but sometimes not sufficient.
If email and SMS beans both implement NotificationSender, an unqualified injection point has two candidates. Spring knows the required type but cannot infer the business decision.
@Component
@Primary
public class EmailNotificationSender implements NotificationSender {
// ...
}
@Component
@Qualifier("sms")
public class SmsNotificationSender implements NotificationSender {
// ...
}
public OtpService(
@Qualifier("sms") NotificationSender notificationSender
) {
this.notificationSender = notificationSender;
}@Primarymarks email as the default candidate.@Qualifier("sms")explicitly narrows the OTP dependency to SMS.- An explicit qualifier wins over the primary choice at that injection point.
A parameter name can sometimes act as a fallback when it exactly matches a bean name and parameter-name metadata is present. That is a fragile place to encode a business rule; use a qualifier when the choice matters.
| Candidates for NotificationSender | Injection point | Result |
|---|---|---|
| None | Unqualified | Startup fails: the required dependency is missing. |
| Email only | Unqualified | Email is injected because the type has one candidate. |
| Email + SMS | Unqualified | Startup fails unless one candidate is preferred. |
| Email @Primary + SMS | Unqualified | Email is injected. |
| Email @Primary + SMS @Qualifier("sms") | @Qualifier("sms") | SMS is injected; the explicit qualifier narrows the candidates. |
Think of @Primary as the answer to “which candidate should be the general default?” Think of @Qualifieras part of the injection point's requirement: “I need the SMS-flavoured candidate here.”
Bean names and qualifier labels
A default bean name and a semantic qualifier are related but distinct.
@Component
public class EmailNotificationSender { }
// default bean name: emailNotificationSender
@Component
@Qualifier("sms")
public class SmsNotificationSender { }
// default bean name: smsNotificationSender
// qualifier metadata: smsSpring normally converts the class name to lower camel case for the default bean name. @Qualifier("sms") adds selection metadata; it does not rename the bean to sms. An injection point with the same qualifier selects candidates carrying that label.
@Component("smsSender")
public class SmsNotificationSender
implements NotificationSender {
}
// explicit bean name: smsSenderBean-name fallback can occasionally make a parameter named smsNotificationSender resolve successfully. That couples the choice to a local variable name and compiler metadata. For an intentional business distinction such as “OTP must use SMS,” a qualifier communicates the rule directly.
Scope and identity
Default singleton means one instance per definition per context.
@Service
class OrderService {
OrderService(NotificationSender sender) { /* ... */ }
}
@Service
class ReceiptService {
ReceiptService(NotificationSender sender) { /* ... */ }
}
// If email is the selected singleton bean, both constructors
// receive the same email bean object in this ApplicationContext.This is a container scope, not a JVM-wide guarantee. A second ApplicationContext creates its own singleton. Two different bean definitions can also produce two instances of the same Java class.
@Bean
NotificationSender customerEmailSender() {
return new EmailNotificationSender();
}
@Bean
NotificationSender operationsEmailSender() {
return new EmailNotificationSender();
}Run code after startup
CommandLineRunner is itself a bean.
Boot calls every CommandLineRunner after the context has started. A runner is useful for this non-web learning application because it lets us exercise injected services without manually asking the context for them.
EmailNotificationSender
↓ injected into
OrderService
↓ injected into
DemoRunner
↓ invoked by Boot after startupIf any arrow cannot be resolved, the context fails before DemoRunner.run is called. That fail-fast behaviour keeps an application from accepting work with an incomplete object graph.
Practice checkpoint
Write it before reading the solution.
Extend the order application with two notification implementations.
- Email must be the default for
OrderService. OtpServicemust always request SMS explicitly.- A runner must invoke both services after startup.
- Include correct package declarations and imports; the submission must compile as written.
Hints, in increasing order
- Place every class below
com.example.ordersso it is scanned. - Make email
@Primary; give SMS the qualifiersms. - Put
@Qualifier("sms")on theOtpServiceconstructor parameter. - Inject both services into a
CommandLineRunnercomponent.
Working solution
// NotificationSender.java
package com.example.orders.notification;
public interface NotificationSender {
void send(String destination, String message);
}
// EmailNotificationSender.java
package com.example.orders.notification;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
@Component
@Primary
public class EmailNotificationSender implements NotificationSender {
@Override
public void send(String destination, String message) {
System.out.println("EMAIL to " + destination + ": " + message);
}
}
// SmsNotificationSender.java
package com.example.orders.notification;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component
@Qualifier("sms")
public class SmsNotificationSender implements NotificationSender {
@Override
public void send(String destination, String message) {
System.out.println("SMS to " + destination + ": " + message);
}
}// OrderService.java
package com.example.orders.service;
import org.springframework.stereotype.Service;
import com.example.orders.notification.NotificationSender;
@Service
public class OrderService {
private final NotificationSender notificationSender;
public OrderService(NotificationSender notificationSender) {
this.notificationSender = notificationSender;
}
public void placeOrder(String orderId, String destination) {
notificationSender.send(
destination,
"Order " + orderId + " placed successfully"
);
}
}
// OtpService.java
package com.example.orders.service;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import com.example.orders.notification.NotificationSender;
@Service
public class OtpService {
private final NotificationSender notificationSender;
public OtpService(
@Qualifier("sms") NotificationSender notificationSender
) {
this.notificationSender = notificationSender;
}
public void sendOtp(String destination, String otp) {
notificationSender.send(destination, "OTP: " + otp);
}
}package com.example.orders.runner;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import com.example.orders.service.OrderService;
import com.example.orders.service.OtpService;
@Component
public class DemoRunner implements CommandLineRunner {
private final OrderService orderService;
private final OtpService otpService;
public DemoRunner(OrderService orderService, OtpService otpService) {
this.orderService = orderService;
this.otpService = otpService;
}
@Override
public void run(String... args) {
orderService.placeOrder("ORD-201", "customer@example.com");
otpService.sendOtp("+919876543210", "482731");
}
}EMAIL to customer@example.com: Order ORD-201 placed successfully
SMS to +919876543210: OTP: 482731Elapsed time · hint level (H0 means no hint; H1–Hn is the last hint used) · conceptual errors · mechanical errors · explanation confidence from 0–2.
Failure diagnosis
What happens if both implementations have only @Component?
Startup fails at the unqualified OrderService injection point because two beans satisfy NotificationSender. The failure happens while Spring creates the context, before the runner executes.