A few years ago I watched a team spend half a day chasing a mystery bug that was really just two objects that looked identical but were not the same instance. One service created its own HTTP client with new, another service got a different client from Spring, and suddenly connection pools, timeouts, and retries behaved inconsistently.
That is the kind of problem Spring‘s container is meant to prevent: one place defines an object, and everything else receives that same managed instance.
When I need that level of certainty, I reach for Spring‘s @Bean annotation. It is the "I want this exact object in the container" tool. It shines when you are wiring third-party libraries, when you want to control construction (builders, factories, environment-based config), or when you need to expose interfaces while returning concrete implementations.
In this post I will show you how @Bean really behaves at runtime, how it differs from component scanning, and how to build a small Spring Boot app where beans are created explicitly, injected cleanly, and overridden safely for tests. Along the way I will call out the failure modes I see most often and the debug tactics I use when wiring goes sideways.
@Bean in one sentence (and why I still care in 2026)
@Bean marks a method whose return value should be registered as a Spring bean. That sounds simple, but it opens a surprisingly large set of patterns:
- You can put object creation where it belongs: in configuration code, not scattered across constructors.
- You can wrap libraries you cannot annotate (SDK clients, HTTP clients, metrics registries, proprietary JARs).
- You can decide the exact implementation, scope, name, and lifecycle callbacks.
- You can swap implementations per environment (profiles/conditions) without rewriting consumer code.
I think about @Bean like a high-quality factory shelf in a workshop. Component scanning is like telling Spring: "anything with this sticker belongs on the shelf." @Bean is you placing a specific tool on the shelf yourself: same shelf, different level of intent.
Here is a quick comparison I use when deciding between component scanning and explicit beans:
Component scanning (@Component, @Service, …)
@Bean) —
Your own classes with simple constructors
Mostly implicit
High, if packages are stable
Needs qualifiers and careful scanning
Often via @MockBean
@TestConfiguration + overriding beans My rule of thumb: if the class is mine and has no special creation logic, I annotate it and let scanning register it. If the class is not mine or needs conditional construction, I define it via @Bean.
How Spring turns a @Bean method into a container-managed bean
When Spring starts, it builds an application context (the IoC container). During startup it processes configuration classes and registers bean definitions. A @Bean method is not "called randomly"; Spring records that method as the source of a bean definition and then calls it when creating the bean.
Two details matter a lot in real projects:
1) Bean identity is by name, not by type.
- By default, the bean name is the method name:
httpClient()becomeshttpClient. - You can override the name:
@Bean("billingHttpClient").
2) @Configuration changes the semantics of calling @Bean methods.
If your @Bean methods live in a class annotated with @Configuration, Spring usually enhances that class so that calling one @Bean method from another returns the managed singleton rather than constructing a new instance.
That enhancement is where many double-instantiation bugs come from. If you turn off proxying (more on that soon) or place @Bean methods in a plain @Component, Spring may run in "lite mode" where calling otherBeanMethod() can create a fresh object instead of fetching the managed one.
proxyBeanMethods: the knob you should understand
In modern Spring (Spring Framework 6 / Spring Boot 3+), you will see:
@Configuration(proxyBeanMethods = true)(the historical default)@Configuration(proxyBeanMethods = false)(a common modern choice)
Here is how I decide:
What you get
—
proxyBeanMethods = true Inter-bean method calls stay singleton-safe
proxyBeanMethods = false Less runtime enhancement work
@Bean method from another expecting the container singleton If you choose proxyBeanMethods = false, use method parameters to link beans instead of calling methods directly. That pattern is both cleaner and less fragile.
One more practical note: the performance difference is usually not dramatic for small apps. Where I see it matter is in large applications with lots of configuration classes where the proxying overhead can be noticeable (think small-but-real differences, not miracles). I still pick proxyBeanMethods = false when I can, because it reduces "hidden" coupling between bean methods.
A runnable example: explicit bean wiring for a small campus portal app
I will build a tiny app that:
- Exposes a REST endpoint:
POST /enroll - Uses a service that needs a clock, an ID generator, and a notification sender
- Creates those dependencies via
@Beanmethods - Demonstrates
@Primaryand@Qualifier
Build file (Maven)
pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
dev.example
bean-demo
1.0.0
org.springframework.boot
spring-boot-starter-parent
3.3.0
21
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
Domain + service layer
EnrollmentRequest.java:
package dev.example.beandemo;
public record EnrollmentRequest(String studentEmail, String courseCode) {}
EnrollmentReceipt.java:
package dev.example.beandemo;
import java.time.Instant;
public record EnrollmentReceipt(String enrollmentId, Instant enrolledAt) {}
EnrollmentIdGenerator.java:
package dev.example.beandemo;
public interface EnrollmentIdGenerator {
String nextId();
}
UuidEnrollmentIdGenerator.java:
package dev.example.beandemo;
import java.util.UUID;
public final class UuidEnrollmentIdGenerator implements EnrollmentIdGenerator {
@Override
public String nextId() {
return "enr_" + UUID.randomUUID();
}
}
NotificationSender.java:
package dev.example.beandemo;
public interface NotificationSender {
void sendEnrollmentConfirmation(String studentEmail, String courseCode, String enrollmentId);
}
ConsoleNotificationSender.java:
package dev.example.beandemo;
public final class ConsoleNotificationSender implements NotificationSender {
private final String fromLabel;
public ConsoleNotificationSender(String fromLabel) {
this.fromLabel = fromLabel;
}
@Override
public void sendEnrollmentConfirmation(String studentEmail, String courseCode, String enrollmentId) {
System.out.printf("[%s] Sent enrollment confirmation to %s for %s (id=%s)%n",
fromLabel, studentEmail, courseCode, enrollmentId);
}
}
EnrollmentService.java:
package dev.example.beandemo;
import java.time.Clock;
import java.time.Instant;
public final class EnrollmentService {
private final Clock clock;
private final EnrollmentIdGenerator idGenerator;
private final NotificationSender notificationSender;
public EnrollmentService(Clock clock,
EnrollmentIdGenerator idGenerator,
NotificationSender notificationSender) {
this.clock = clock;
this.idGenerator = idGenerator;
this.notificationSender = notificationSender;
}
public EnrollmentReceipt enroll(EnrollmentRequest request) {
String enrollmentId = idGenerator.nextId();
Instant enrolledAt = Instant.now(clock);
notificationSender.sendEnrollmentConfirmation(
request.studentEmail(),
request.courseCode(),
enrollmentId
);
return new EnrollmentReceipt(enrollmentId, enrolledAt);
}
}
Notice what I did: none of these classes are annotated with @Component. That is intentional. I want to show the pure Java objects style, then register them explicitly with @Bean.
Configuration with @Bean
AppConfig.java:
package dev.example.beandemo;
import java.time.Clock;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class AppConfig {
@Bean
public Clock appClock() {
// Explicit seam for tests: swap this bean with a fixed Clock.
return Clock.systemUTC();
}
@Bean
public EnrollmentIdGenerator enrollmentIdGenerator() {
return new UuidEnrollmentIdGenerator();
}
@Bean
@Qualifier("studentNotifications")
public NotificationSender studentNotificationSender() {
return new ConsoleNotificationSender("campus-portal");
}
@Bean
public EnrollmentService enrollmentService(
Clock appClock,
EnrollmentIdGenerator enrollmentIdGenerator,
@Qualifier("studentNotifications") NotificationSender notificationSender
) {
// Note the pattern: I do not call other @Bean methods.
// I ask Spring for dependencies via parameters.
return new EnrollmentService(appClock, enrollmentIdGenerator, notificationSender);
}
}
A few important points:
@Configuration(proxyBeanMethods = false)is safe here because I am wiring dependencies through method parameters.- Bean names default to method names:
appClock,enrollmentIdGenerator,studentNotificationSender,enrollmentService. - I used
@Qualifierto give a role name to a bean. This becomes more valuable when you have multiple implementations of an interface.
Web layer
EnrollmentController.java:
package dev.example.beandemo;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EnrollmentController {
private final EnrollmentService enrollmentService;
public EnrollmentController(EnrollmentService enrollmentService) {
this.enrollmentService = enrollmentService;
}
@PostMapping("/enroll")
@ResponseStatus(HttpStatus.CREATED)
public EnrollmentReceipt enroll(@RequestBody EnrollmentRequest request) {
return enrollmentService.enroll(request);
}
}
And the Spring Boot entry point:
BeanDemoApplication.java:
package dev.example.beandemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BeanDemoApplication {
public static void main(String[] args) {
SpringApplication.run(BeanDemoApplication.class, args);
}
}
Run it and call:
POST http://localhost:8080/enroll- Body:
{ "studentEmail": "[email protected]", "courseCode": "CS-205" }
You will get a JSON receipt with an ID and timestamp, and the console will print a notification line.
Bean method parameters are dependency injection too (and I rely on this heavily)
One of the most underused features of @Bean methods is that their parameters are autowired by Spring. That means you can wire graphs without ever calling another @Bean method.
This is the pattern I try to default to:
- Each
@Beanmethod describes one object. - Dependencies are listed as parameters.
- The method body only constructs and configures that object.
Why I like it:
- It works the same whether
proxyBeanMethodsis true or false. - It reads like a dependency list (almost like a constructor).
- It makes "who depends on what" obvious, which is what you need when debugging.
A small but important detail: Spring resolves bean method parameters using the same rules as constructor injection. That means all the usual concepts apply:
- By type first
- If multiple candidates exist, by qualifier/name
- If still ambiguous, fail fast at startup
If you want a bean to depend on a specific named bean, you can do any of these:
- Use
@Qualifier("name") - Use
@Resource(name = "name")(Jakarta) - Name the bean explicitly via
@Bean("name")and then qualify it
I personally stick to @Qualifier for consistency.
Naming, @Primary, and @Qualifier: staying sane with multiple beans
Real projects rarely have one implementation per interface. You might have:
- Two
NotificationSenders: one real email sender, one audit logger - Two clocks: system UTC and a business-time clock
- Two HTTP clients: internal and external (different timeouts/retries)
Spring must choose one candidate when injecting by type. If it finds multiple matches and you did not clarify, you will get a startup error.
Here are the patterns I recommend.
Pattern 1: Use @Qualifier as a role label
This is what I did above. When you inject, you specify the role:
@Qualifier("studentNotifications") NotificationSender sender
I prefer role labels over implementation names because they survive refactors. studentNotifications stays true even if the sender switches from console to SMTP.
If you want to lean into this approach, you can define multiple notification beans in your config:
@Bean
@Qualifier("studentNotifications")
public NotificationSender studentNotificationSender() {
return new ConsoleNotificationSender("students");
}
@Bean
@Qualifier("auditNotifications")
public NotificationSender auditNotificationSender() {
return new ConsoleNotificationSender("audit");
}
And then inject whichever role you need.
Pattern 2: Use @Primary for the default
If you have a default bean that most places should receive, mark it:
@Bean
@Primary
public NotificationSender primaryNotificationSender() {
return new ConsoleNotificationSender("primary");
}
Then only the special cases need a qualifier.
The failure mode I watch for: teams mark something @Primary early in the project and later forget it exists. Months later someone adds a second implementation and is confused why injection always picks the primary. If you use @Primary, I recommend documenting it in the configuration class with a short comment about why it is the default.
Pattern 3: Name beans explicitly when method names are not stable
Method names often get renamed during refactors. If the name is part of your app‘s contract (externalized config, test overrides, or conditional wiring), lock it in:
@Bean("appClock")
public Clock clock() {
return Clock.systemUTC();
}
When I am working with a team, explicit names reduce accidental breakage.
Lifecycle, scopes, and manual construction without losing container features
One reason I like @Bean is that it gives you manual construction and Spring lifecycle management.
Lifecycle hooks: initMethod, destroyMethod, and @PostConstruct
If a library client needs startup/shutdown hooks (opening a pool, closing sockets), you can wire that cleanly.
Example with destroyMethod:
@Bean(destroyMethod = "close")
public SomeClient someClient() {
return new SomeClient();
}
I use this often with clients that implement AutoCloseable but are not managed by Spring otherwise.
You can also rely on JSR-250 style callbacks:
@PostConstructto run initialization after dependencies are set@PreDestroyto clean up on shutdown
Those work whether the bean came from scanning or @Bean.
One gotcha: if you return an interface type from @Bean and the implementation has a close() method, Spring only knows what to call if you specify destroyMethod (or it can infer destroy methods in some cases). When in doubt, I make it explicit.
Scope: singleton is default, but you should know the alternatives
Most application services should be singletons. Still, @Bean supports scopes:
singleton(default): one instance per application contextprototype: a new instance each time the container creates it- Web scopes like request/session (in web apps)
I rarely recommend prototype for typical business services because it makes reasoning about state harder and can surprise you during injection.
If you truly need per-request state, I prefer request scope.
Example:
import org.springframework.context.annotation.Scope;
import org.springframework.web.context.WebApplicationContext;
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public RequestContext requestContext() {
return new RequestContext();
}
Be careful: if you inject a request-scoped bean into a singleton, Spring typically uses a proxy so the singleton can hold a stable reference while the actual target changes per request. That works, but it is a concept you should understand because it affects debugging (you will see a proxy class in the debugger) and sometimes equality checks.
Conditional beans: profiles and properties without rewriting code
A big part of the practical value of @Bean is conditional wiring. I think of this as "same interface, different construction rules".
@Profile: the straightforward switch
If you have a dev notification sender and a prod sender, profiles are a clean fit.
import org.springframework.context.annotation.Profile;
@Bean
@Profile("dev")
@Qualifier("studentNotifications")
public NotificationSender devStudentNotificationSender() {
return new ConsoleNotificationSender("dev-students");
}
@Bean
@Profile("prod")
@Qualifier("studentNotifications")
public NotificationSender prodStudentNotificationSender() {
return new SmtpNotificationSender("smtp.school.edu", 587);
}
The rest of the app injects @Qualifier("studentNotifications") and does not care which profile is active.
My advice: do not overuse profiles for every tiny difference. Profiles are great when the environment truly changes behavior (like real email vs console) but can be heavy-handed for small tuning.
Property-based conditions (especially in Spring Boot)
In Spring Boot, property-based conditions are common: enable something only when a flag is set, or only when a property exists.
A useful mental model:
- Profiles are for environment identity (dev/test/prod)
- Properties are for feature flags and configuration knobs (enable email, set base URL, choose implementation)
Even if you do not write your own condition annotations, you should recognize this pattern:
- Provide a default bean when nothing else is configured
- Allow applications to override it by defining another bean
This is exactly how many Boot auto-configurations work.
Overriding beans safely for tests (without teaching tests about production wiring)
When people say "I like @Bean because it is testable", what they usually mean is: it is easy to replace.
I try to keep tests focused on behavior, not on wiring. A good test override does two things:
- It swaps the dependency cleanly
- It does not require rewriting production constructors
Option 1: @TestConfiguration that defines replacement beans
If you provide a bean of the same type and name (or same qualifier) in the test context, you can replace behavior.
Example: replace the clock so timestamps are deterministic.
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
@TestConfiguration
public class FixedClockTestConfig {
@Bean
public Clock appClock() {
return Clock.fixed(Instant.parse("2026-02-01T12:00:00Z"), ZoneOffset.UTC);
}
}
Then in your test:
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
@SpringBootTest
@Import(FixedClockTestConfig.class)
class EnrollmentServiceTest {
// … inject EnrollmentService and assert exact timestamps
}
I like this because it does not require mocking frameworks, and it keeps your seam (the Clock) explicit.
Option 2: @MockBean for behavioral stubbing
If you want to assert that notifications were sent without printing to console, @MockBean is fine.
The trade-off: your test becomes aware that the notification sender is a Spring bean. That is often acceptable, but I still prefer @TestConfiguration when the replacement is simple.
Option 3: slice tests and small contexts
For configuration-heavy projects, I sometimes test wiring itself using smaller contexts rather than booting the entire application.
The idea is: validate that the bean graph composes and that conditions behave as expected.
This style is especially useful when you ship a library (starter-like modules) or when you have complex conditional wiring.
Common pitfalls I see with @Bean (and how I avoid them)
These are the mistakes that show up repeatedly.
Pitfall 1: Calling another @Bean method when proxyBeanMethods = false
Symptom: you think you are sharing a singleton but you accidentally create two instances.
Example of what not to do:
@Bean
public A a() { return new A(); }
@Bean
public B b() {
return new B(a()); // This can create a new A if not proxied.
}
Fix: use parameters.
@Bean
public B b(A a) {
return new B(a);
}
This is the single highest ROI habit you can adopt with @Bean.
Pitfall 2: Multiple beans of the same type without qualifiers
Symptom: startup failure complaining about multiple candidates.
Fix options:
- Add
@Primaryfor the default - Add
@Qualifieron both bean definition and injection point - Inject a collection (
List) if you really need all of them
Injecting a list is a great pattern for plugin-like behavior. It is also a clean way to do "do these 3 things" without hardcoding a chain.
Pitfall 3: Using bean names as a hidden API
Bean names can be convenient. They can also become a hidden contract.
If an ops script, a property, or a test relies on a bean being named httpClient, and someone renames the method to externalHttpClient during refactoring, you get breakage.
My approach:
- If a name is truly a contract, I use
@Bean("stableName"). - If it is not a contract, I avoid referencing it externally.
Pitfall 4: Circular dependencies
Circular dependencies can show up with scanning or @Bean, but explicit configuration can make them easier to notice.
If you have A depends on B and B depends on A, it is usually a design smell. Sometimes the fix is as simple as introducing a third component that both depend on, or making one dependency lazy.
I try not to reach for @Lazy as my first fix. I treat it like a circuit breaker: useful, but it should prompt a second look at the design.
Pitfall 5: Bean creation that reads environment too early
It is tempting to do this:
@Bean
public Client client() {
String url = System.getenv("SERVICE_URL");
return new Client(url);
}
This works, but I prefer letting Spring own configuration binding so that:
- You can set values via properties, env vars, or config files consistently
- You can validate configuration
- You can override easily in tests
In practice, I either inject Environment or, better, bind to a properties class and inject that.
Debug tactics when wiring goes sideways
When the container fails to start, I want fast answers to three questions:
1) Which bean failed to create?
2) Why did Spring choose that bean (or fail to choose)?
3) Where did the dependency graph become ambiguous or invalid?
Here are the tactics I use.
Turn on debug logs for conditions and auto-configuration
In Spring Boot, running with debug mode helps you see why certain beans were created or not created.
This is often the fastest way to explain: "why did I get this implementation instead of that one?"
Inspect the bean graph with Actuator (when available)
If you include Actuator in a real service, the /beans endpoint can be very helpful during development.
I do not leave it open in production without careful security controls, but for internal environments it can be a huge time-saver.
Ask Spring directly: inject ApplicationContext
If I need to debug something in code, I will temporarily inject ApplicationContext in a diagnostic component and log what beans exist for a type.
Example (diagnostic-only):
import java.util.Map;
import org.springframework.context.ApplicationContext;
public final class BeanInspector {
private final ApplicationContext ctx;
public BeanInspector(ApplicationContext ctx) {
this.ctx = ctx;
}
public Map notificationSenders() {
return ctx.getBeansOfType(NotificationSender.class);
}
}
I treat this as a tool, not as an architectural pattern. In production code, dependency lookup is usually a smell.
Learn to read the exception chain
Spring exceptions can be long, but they are usually precise if you scan for:
Caused by:lines- The first mention of "Unsatisfied dependency"
- The "consider defining a bean" hints
I specifically look for the injection point: which constructor parameter, or which bean method parameter, was being resolved.
Advanced @Bean patterns I actually use
Once the basics are solid, @Bean becomes a nice place to express higher-level composition patterns.
Pattern: create an adapter around a third-party client
Instead of injecting a vendor SDK throughout your app, wrap it.
- Your app depends on your interface
- The
@Beanconstructs the vendor client and then constructs your adapter - Tests can replace the adapter or the underlying client
This reduces lock-in and keeps vendor details out of business logic.
Pattern: expose an interface but return a concrete implementation
This is exactly what we did with EnrollmentIdGenerator and NotificationSender.
I like to keep consumer code interface-first, and keep the implementation choice in configuration.
Pattern: supply default beans that can be overridden
In larger systems (or shared modules), I often provide defaults:
- If the application defines its own bean, it wins
- Otherwise, a safe default exists
This gives you a plug-and-play module without forcing every app to define every bean.
Pattern: build objects with builders and validate configuration
Many libraries use builder patterns. @Bean is a perfect home for builder configuration.
The practical benefit: you can keep configuration local, and you can fail fast if required settings are missing.
When not to use @Bean
I like @Bean, but I do not want it everywhere.
I usually avoid @Bean when:
- The class is yours, simple, and stable: component scanning is less ceremony.
- You want very discoverable dependencies for new teammates:
@Serviceand@Repositoryare idiomatic and easier to grep. - The object graph is trivial: you do not need a config class for three plain services.
I also watch out for over-centralization. A single mega configuration class with 80 beans becomes its own kind of complexity. When configuration grows, I split it by domain: EnrollmentConfig, NotificationConfig, TimeConfig, and so on.
Performance considerations (realistic, not magical)
I think about performance in two buckets: startup and runtime.
Startup
proxyBeanMethods = falsecan reduce some startup overhead in configuration-heavy apps.- Fewer reflective scans and fewer proxies generally mean slightly faster startup.
In practice, I treat this as incremental: nice to have, but not the primary reason to choose a style.
Runtime
- Singleton beans are cheap once created.
- The container does not "re-resolve" dependencies on every call; injection is done once.
- Proxies (AOP, scoped proxies, transactional proxies) can add a small amount of overhead, but usually not enough to matter compared to I/O.
If you are optimizing runtime performance, you will almost always get more value from reducing network calls and database work than from micro-optimizing bean wiring.
Expansion Strategy
When I expand a small app into a production-ready service, I treat @Bean configuration as a place to add seams and operational safety.
Here is the strategy I follow:
- Start with the simplest possible configuration: create only what you need.
- Add seams for time, randomness, and external calls early:
Clock, ID generators, and clients. - Decide on naming conventions: role-based qualifiers (like
studentNotifications) rather than implementation-based names. - Split configuration by domain as it grows: it keeps files readable and reduces merge conflicts.
- Keep constructors clean: business classes should not read environment variables or build clients.
This keeps the codebase predictable. When something breaks, there is one obvious place to look: the configuration layer.
If Relevant to Topic
When you start deploying and operating services, @Bean configuration interacts with production concerns more than most people expect.
Here are the production considerations I keep in mind:
- Observability: if you create clients in
@Bean, also configure metrics, tracing, and logging there so behavior is consistent across the app. - Timeouts and retries: these belong in the client bean configuration, not scattered across call sites.
- Safe defaults: provide conservative defaults (timeouts set, connection pools bounded) so local dev does not accidentally teach the system bad habits.
- Environment parity: keep production vs dev differences explicit via profiles/properties rather than hidden conditionals in business code.
- Test realism: prefer replacing beans with lightweight real implementations (fixed clock, in-memory sender) over mocking everything.
If you take one idea from all of this, it is this: @Bean is not just a way to create objects. It is a way to make object creation a deliberate, reviewable part of your architecture, so you do not end up debugging a mystery bug caused by two "identical" instances that were never meant to exist in the first place.


