Beans, Scopes & Component Scanning¶
A bean is simply an object whose lifecycle the Spring container manages. There are two ways to tell the container about one, and choosing between them is mostly about who owns the class.
Way 1: stereotype annotations on your own classes¶
@Component // generic
@Service // business logic
@Repository // data access (also enables exception translation)
@Controller / @RestController // web layer
@Configuration // a source of @Bean definitions
All of these are meta-annotated with @Component, so scanning finds them the same way.
The specific stereotypes are mostly documentation, with two real effects:
@Repository beans get persistence-exception translation (vendor exceptions become
Spring's DataAccessException hierarchy), and @Controller/@RestController beans are
picked up by Spring MVC's handler mapping.
Way 2: @Bean methods for classes you do not own¶
You cannot annotate java.time.Clock, an HttpClient, or a third-party SDK client. Write a
factory method instead:
@Configuration
class InfrastructureConfig {
@Bean
Clock clock() {
return Clock.systemUTC();
}
@Bean
HttpClient httpClient() {
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.build();
}
@Bean
PricingEngine pricingEngine(Clock clock, List<PricingRule> rules) { // parameters are injected
return new PricingEngine(clock, rules);
}
}
The bean's name is the method name (clock, httpClient). Parameters of @Bean
methods are resolved exactly like constructor parameters.
Scopes¶
| Scope | One instance per… | Typical use |
|---|---|---|
singleton (default) |
container | Services, repositories, controllers, clients — almost everything |
prototype |
injection point / getBean call |
Stateful helpers that must not be shared |
request |
HTTP request | Per-request context (web apps only) |
session |
HTTP session | Per-user state in server-rendered apps |
Singletons are shared by every thread handling requests. That is the single most
important consequence of scopes: a singleton must be thread-safe, which in practice
means stateless — only final references to other beans and immutable configuration.
@Service
class ReportService {
private final List<String> lines = new ArrayList<>(); // BUG: shared by all requests
String build(Order order) {
lines.clear();
lines.add("Order " + order.id());
return String.join("\n", lines);
}
}
Under concurrent load, two requests interleave clear() and add() and produce each
other's reports. Move lines into the method as a local variable.
Controlling component scanning¶
@SpringBootApplication scans its own package and below. To register beans only in
some situations, use conditions and profiles rather than moving packages:
@Component
@Profile("dev")
class FakeEmailSender implements EmailSender { ... } // only when the dev profile is active
@Component
@ConditionalOnProperty(name = "features.audit.enabled", havingValue = "true")
class AuditListener { ... } // only when a property is set
@Profile is covered with configuration in the next lesson. @ConditionalOn… annotations
are the same mechanism auto-configuration uses (Level 3, lesson 1).
Worked example: plug-in rules¶
A pricing engine that discovers its rules as beans:
public interface PricingRule {
BigDecimal apply(Order order, BigDecimal current);
}
@Component
@Order(1)
class BulkDiscount implements PricingRule {
public BigDecimal apply(Order order, BigDecimal current) {
return order.quantity() >= 10 ? current.multiply(new BigDecimal("0.90")) : current;
}
}
@Component
@Order(2)
class RoundToCents implements PricingRule {
public BigDecimal apply(Order order, BigDecimal current) {
return current.setScale(2, RoundingMode.HALF_UP);
}
}
@Service
class PricingEngine {
private final List<PricingRule> rules; // injected in @Order order
PricingEngine(List<PricingRule> rules) { this.rules = rules; }
BigDecimal price(Order order) {
BigDecimal price = order.unitPrice().multiply(BigDecimal.valueOf(order.quantity()));
for (PricingRule rule : rules) price = rule.apply(order, price);
return price;
}
}
Adding a seasonal rule is now a new class, nothing else. The ordering matters here (you
must round after discounting), so it is stated explicitly with @Order.
How It Actually Works¶
Why @Configuration classes are special. Look at this:
@Configuration
class AppConfig {
@Bean DataSource dataSource() { return new HikariDataSource(); }
@Bean JdbcTemplate jdbcTemplate() { return new JdbcTemplate(dataSource()); }
@Bean TransactionManager txManager() { return new DataSourceTransactionManager(dataSource()); }
}
In plain Java, dataSource() is called twice and creates two pools. In Spring it
creates one. At startup the container generates a CGLIB subclass of AppConfig and
overrides every @Bean method: the override first checks the singleton cache and returns
the existing bean if present, and only calls your original method the first time. That is
why @Configuration classes and @Bean methods must not be final.
If you write @Configuration(proxyBeanMethods = false) (Boot's own auto-configurations
do), no subclass is generated and calling dataSource() directly really would create a
second instance. In that "lite" mode you should receive dependencies as @Bean method
parameters instead — which is the clearer style anyway, and it starts slightly faster.
Scoped beans inside singletons. A singleton is created once, so if it injects a
request-scoped bean directly, which request's instance should it hold? Spring solves
this by injecting a scoped proxy: a stand-in object that, on every method call, looks up
the real instance for the current request and delegates. Web scopes are proxied by
default for this reason. A prototype bean injected into a singleton, however, is not
proxied — you get exactly one instance forever. If you really want a fresh one each time,
inject ObjectProvider<MyPrototype> and call getObject() when needed.
Bean names. A scanned class TaskService gets the name taskService. Two classes with
the same simple name in different packages collide and fail startup with a
ConflictingBeanDefinitionException; give one an explicit name
(@Service("legacyTaskService")).
Common mistakes¶
- Mutable fields in singletons (caches in a
HashMap, "current user" fields). Use local variables,ConcurrentHashMap, or a proper cache abstraction. - Making
@Configurationclasses or@Beanmethodsfinal— the CGLIB subclass cannot override them. - Expecting a prototype bean to be new on every call when it is injected into a singleton. It is created once, at injection time.
- Scanning too widely with
@ComponentScan("com"), which can pick up test configurations or library classes. Keep the default.
Exercise¶
- Implement the pricing engine above with two rules and an
@SpringBootTest(or a plain unit test that builds the list by hand) proving the discount is applied before rounding. - Add a third rule,
WeekendSurcharge, that uses an injectedClock. Register theClockvia a@Beanmethod and test the rule with a fixed clock. - In a
@Configurationclass, call one@Beanmethod from another and logSystem.identityHashCodeof the result in both places. Then switch toproxyBeanMethods = falseand observe the difference. - Write the thread-unsafe
ReportServiceabove and a test that callsbuildfrom 50 threads with anExecutorService. Show that it produces wrong output, then fix it.