If you’ve ever swapped @Configuration for @Component on a Spring config class and thought “it’s basically the same thing”, this article is for you. They’re not the same. And the difference can silently break your application in ways that are hard to debug.
Let’s walk through a real example.
Sample setup for example:
We have a simple payment system with two implementations of a PaymentService interface:
public interface PaymentService {
void pay(double amount);
}
public class PaypalPaymentService implements PaymentService {
public void pay(double amount) {
System.out.println("PAYPAL\nAmount: " + amount + " paid.");
}
}
public class StripePaymentService implements PaymentService {
public void pay(double amount) {
System.out.println("STRIPE\nAmount: " + amount + " paid.");
}
}
And an OrderService That depends on whichever PaymentService is active:
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void placeOrder(double amount) {
paymentService.pay(amount);
}
}
The configuration reads which payment provider to use from application.yaml:
payment:
service:
default: stripe
The Config Class:
@Configuration
public class AppConfig {
@Value("${payment.service.default}")
private String paymentDefault;
@Bean
public PaymentService paymentService() {
if (paymentDefault.equals("paypal")) {
return new PaypalPaymentService();
} else {
return new StripePaymentService();
}
}
@Bean
public OrderService orderService() {
return new OrderService(paymentService()); // calling paymentService() directly
}
}
Here you can notice that orderService() calls paymentService() directly as a Java method and not through injection. This is the line that behaves completely differently depending on whether you use @Configuration or @Component .
How @Configuration Works (The Right Way):
When you annotate a class with @ConfigurationSpring enhances it at runtime using a CGLIB proxy. This proxy ensures that @Bean methods behave correctly.
Instead of calling @Bean methods directly; the proxy intercepts each call. This allows Spring to enforce singleton behavior (or other scopes), even when one @Bean method calls another within the same class.
In simple terms, what looks like a normal method call is actually redirected to the Spring container, which returns the already managed bean instead of creating a new instance.
(I’ll dive deeper into Spring Boot internals (like CGLIB proxy) as I continue learning as well.)
To verify this, add identity hash prints:
(An identity hash (often seen via something like System.identityHashCode(obj) in Java) is a number that represents the identity of an object)
@Bean
public PaymentService paymentService() {
PaymentService service = paymentDefault.equals("paypal")
? new PaypalPaymentService()
: new StripePaymentService();
System.out.println("[paymentService bean] instance hash: " + System.identityHashCode(service));
return service;
}
@Bean
public OrderService orderService() {
PaymentService serviceForOrder = paymentService();
System.out.println("[orderService -> paymentService()] instance hash: " + System.identityHashCode(serviceForOrder));
return new OrderService(serviceForOrder);
}
Output with @Configuration:
[paymentService bean] instance hash: 777113684
[orderService -> paymentService()] instance hash: 777113684
paymentService() It is called once. The CGLIB proxy intercepts the second call from orderService() and returns the existing singleton. Both references point to the same object.
What Happens With @Component (The Trap):
Now swap @Configuration for @Component:
@Component // ← changed
public class AppConfig {
// ... same code
}
When you replace @Configuration with @ComponentSpring stops enhancing the class with a CGLIB proxy, so it behaves like a normal Java class.
Spring does NOT intercept
@Beanmethod callsSo when
orderService()callspaymentService()→ It’s just a direct method call → the method runs again like normal Java
That means: A new object is created each time the method is called.
Output with @Component:
[paymentService bean] instance hash: 777113684
[paymentService bean] instance hash: 1081635795
[orderService -> paymentService()] instance hash: 1081635795
Three lines. paymentService() printed twice, proof it ran twice.
Let’s break it down:
First line
Spring calls
paymentService()to create the beanInstance created →
777113684
Second line
Somewhere else (like another bean setup),
paymentService()is called againSince there’s no proxy → method runs again
New instance →
1081635795
Third line
Inside
orderService(), you callpaymentService()Again, a direct method call
It creates (or reuses within that call path) the same new instance →
1081635795
OrderService is wired with a PaymentServiceObject that Spring knows absolutely nothing about.
Why This Is Not Good:
In this sample example, the app still works, StripePaymentService It is a plain class with no Spring dependencies, so the unmanaged instance behaves the same as the managed one.
But in a real application, your services often carry Spring-managed behaviour:
@Transactional— Spring applies transactions through a proxy. An unmanaged instance bypasses this entirely. Your database operations won’t be wrapped in a transaction.@Cacheable— Caching is applied through a proxy. The unmanaged instance will never cache anything.@Async— Asynchronous execution won’t work on an unmanaged instance.
These failures are silent. Your application starts, runs, and logs no errors. You only discover the problem when transactions aren’t rolling back, or caches aren’t being hit.
The safest general rule: always use @Configuration for classes that define @Bean methods.
The difference between @Configuration and@Component It’s not just a label… it changes how Spring manages your object graph at a fundamental level. One small annotation swap can introduce a bug that’s invisible until it causes real damage in production.
Used AI to refine the content. The concepts in this article are based on my own understanding and hands-on exploration, but it’s always best to try things out yourself.