Spring @Bean Annotation With Practical Examples (Spring Boot 3 / Java 21)

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:

Concern

Component scanning (@Component, @Service, …)

Explicit bean (@Bean) —

— Best for

Your own classes with simple constructors

Third-party classes, complex construction Creation logic

Mostly implicit

Fully explicit Refactoring safety

High, if packages are stable

High, if config is centralized Multiple implementations

Needs qualifiers and careful scanning

Very straightforward: return different objects Testing overrides

Often via @MockBean

Often via @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() becomes httpClient.
  • 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:

Setting

What you get

What you must avoid —

proxyBeanMethods = true

Inter-bean method calls stay singleton-safe

Slight startup overhead; more magic proxyBeanMethods = false

Less runtime enhancement work

Do not call one @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 @Bean methods
  • Demonstrates @Primary and @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 @Qualifier to 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 @Bean method 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 proxyBeanMethods is 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:

  • @PostConstruct to run initialization after dependencies are set
  • @PreDestroy to 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 context
  • prototype: 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 @Primary for the default
  • Add @Qualifier on 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 @Bean constructs 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: @Service and @Repository are 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 = false can 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.

Scroll to Top