Intermediate Reading time: ~9 min

Bean Management

@Component, @Service, @Repository, @Configuration, @Bean, @Primary, @Qualifier

Bean Management

1. Definition

Spring Framework bean management refers to the registration, configuration, and lifecycle control of container-managed objects. Spring offers two approaches: automatic discovery with stereotype annotations (@Component, @Service, @Repository) and explicit declaration with @Bean methods in @Configuration classes. The combination of both approaches forms the configuration foundation of modern Spring applications.

Beyond bean registration, Spring provides mechanisms for disambiguation (@Primary, @Qualifier), bean definition modification (BeanFactoryPostProcessor), and bean instance customization (BeanPostProcessor).


2. Core Concepts

Stereotype Annotations

Spring defines four main stereotype annotations, each a specialization of @Component:

Annotation Purpose Extra behavior
@Component General bean Base component marker
@Service Business logic layer Semantic marker (no extra logic)
@Repository Data access layer Exception translation (SQL → DataAccessException)
@Controller Web layer HTTP request mapping (with @RequestMapping family)

@Service and @Component are technically identical — the difference is semantic: it signals to developers and tools what role the class plays.

@Configuration and @Bean

A @Configuration class is a configuration source where @Bean methods create explicit bean definitions:

@Configuration
public class AppConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

When @Bean vs @Component?

  • @Component: for your own classes where you can add the annotation
  • @Bean: for third-party classes you cannot modify, or when complex initialization is needed

@Primary and @Qualifier

When an interface has multiple implementations, Spring cannot automatically resolve:

public interface NotificationSender { void send(String msg); }

@Service
@Primary
public class EmailSender implements NotificationSender { ... }

@Service
public class SmsSender implements NotificationSender { ... }
  • @Primary — default implementation when no explicit selection is made
  • @Qualifier("smsSender") — named selection at injection point
@Service
public class AlertService {
    // Gets @Primary (EmailSender)
    private final NotificationSender defaultSender;

    // Gets SmsSender via explicit @Qualifier
    private final NotificationSender smsSender;

    public AlertService(
            NotificationSender defaultSender,
            @Qualifier("smsSender") NotificationSender smsSender) {
        this.defaultSender = defaultSender;
        this.smsSender = smsSender;
    }
}

Custom Qualifier Annotation

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
public @interface Sms {}

@Service @Sms
public class SmsSender implements NotificationSender { ... }

// Usage
public AlertService(@Sms NotificationSender sender) { ... }

3. Practical Usage

Component Scanning

@Configuration
@ComponentScan(
    basePackages = "com.example",
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.ANNOTATION,
        classes = Deprecated.class
    )
)
public class AppConfig {}

Conditional Bean Registration

@Configuration
public class CacheConfig {
    @Bean
    @ConditionalOnProperty(name = "cache.enabled", havingValue = "true")
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("items");
    }
}

Bean Collection Injection

@Service
public class NotificationRouter {
    private final List<NotificationSender> senders; // all implementations

    public NotificationRouter(List<NotificationSender> senders) {
        this.senders = senders;
    }

    public void notifyAll(String msg) {
        senders.forEach(s -> s.send(msg));
    }
}

@Order and @Priority

@Service @Order(1)
public class EmailSender implements NotificationSender { ... }

@Service @Order(2)
public class SmsSender implements NotificationSender { ... }
// List<NotificationSender> starts with EmailSender

4. Code Examples

Full Layered Application

// Repository layer
@Repository
public class JpaUserRepository implements UserRepository {
    private final EntityManager em;

    public JpaUserRepository(EntityManager em) {
        this.em = em;
    }

    @Override
    public User findById(Long id) {
        return em.find(User.class, id);
    }
}

// Service layer
@Service
public class UserService {
    private final UserRepository userRepo;

    public UserService(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public User getUser(Long id) {
        return userRepo.findById(id);
    }
}

@Bean initMethod / destroyMethod

@Configuration
public class DataSourceConfig {
    @Bean(initMethod = "start", destroyMethod = "close")
    public HikariDataSource dataSource() {
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl("jdbc:h2:mem:test");
        return ds;
    }
}

BeanPostProcessor Example

@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        if (bean instanceof Auditable) {
            System.out.println("Initializing auditable bean: " + beanName);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        return bean; // AOP proxies are created here
    }
}

BeanFactoryPostProcessor Example

@Component
public class CustomPropertyPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) {
        BeanDefinition bd = factory.getBeanDefinition("dataSource");
        bd.getPropertyValues().addPropertyValue("maxPoolSize", "20");
    }
}

5. Trade-offs

Aspect @Component (scan) @Bean (explicit)
Simplicity ✅ Less code ❌ More boilerplate
Transparency ❌ Scattered ✅ Visible in one place
Third-party classes ❌ Cannot modify ✅ Anything registrable
IDE navigation ✅ Annotation → class ✅ Method → return type
Conditional reg. @Conditional annotations @ConditionalOnProperty etc.
Aspect @Primary @Qualifier
Default ✅ Global default ❌ Must specify at every injection
Precision ❌ Only one allowed ✅ Any can be selected
Maintainability ✅ Simple ❌ String-based = fragile
Custom qualifier ✅ Type-safe annotation

BeanPostProcessor vs BeanFactoryPostProcessor

Aspect BeanPostProcessor BeanFactoryPostProcessor
When does it run? After bean instantiation After bean definitions loaded
What does it modify? Bean instances Bean definitions (metadata)
Typical purpose AOP proxy, validation, logging Property resolution, bean def modification

6. Common Mistakes

❌ DAO Class Without @Repository

// BAD — no exception translation
@Component
public class UserDao { ... }

// GOOD — DataAccessException mapping is automatic
@Repository
public class UserDao { ... }

❌ @Bean Method Not in @Configuration Class

// BAD — in @Component, @Bean methods run in "lite mode"
// Inter-bean references do NOT maintain singleton semantics!
@Component
public class AppConfig {
    @Bean
    public ServiceA serviceA() {
        return new ServiceA(commonDep()); // NEW instance on every call!
    }
    @Bean
    public CommonDep commonDep() { return new CommonDep(); }
}

// GOOD — @Configuration CGLIB proxy ensures singleton
@Configuration
public class AppConfig { ... }

❌ @Qualifier String Typo

// BAD — runtime error if name doesn't match
@Qualifier("smsSennder") // Typo!
NotificationSender sender;

// GOOD — custom qualifier annotation = compile-time safety
@Sms NotificationSender sender;

❌ @Autowired Dependency in BeanPostProcessor

// BAD — BPP is instantiated early, dependencies may not be ready
@Component
public class MyBPP implements BeanPostProcessor {
    @Autowired
    private SomeService service; // May be null or not proxied!
}

❌ Multiple @Primary on Same Interface

// BAD — NoUniqueBeanDefinitionException
@Service @Primary
public class EmailSender implements NotificationSender { ... }

@Service @Primary
public class PushSender implements NotificationSender { ... }

7. Deep Dive

Stereotype Annotation Inheritance

@Service, @Repository, @Controller all contain @Component as a meta-annotation. Spring component scanning looks for the presence of @Component (either directly or via meta-annotations). This is why custom stereotype annotations work:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface UseCase {}

@UseCase
public class PlaceOrderUseCase { ... } // registered as a bean

BeanPostProcessor Internals

BeanPostProcessor is part of the bean pipeline:

  1. Bean instantiation (constructor)
  2. Dependency injection (setter/field)
  3. postProcessBeforeInitialization() — all BPPs
  4. @PostConstruct / InitializingBean.afterPropertiesSet()
  5. postProcessAfterInitialization() — all BPPs (AOP proxy created here)
  6. Bean ready for use

Important: If a BPP wraps the bean (e.g., returns a proxy), the original bean reference is lost. This is exactly how Spring AOP works.

BeanFactoryPostProcessor and Property Placeholder

PropertySourcesPlaceholderConfigurer (responsible for resolving @Value("${...}")) is itself a BeanFactoryPostProcessor. This explains why you cannot use @Value in a BeanFactoryPostProcessor — it hasn't run yet when the BFPP is instantiated.

Bean Overriding

Since Spring Boot 2.1+, bean overriding is disabled by default (spring.main.allow-bean-definition-overriding=false). If two @Bean definitions produce the same name, BeanDefinitionOverrideException is thrown. This prevents accidental override.

@Lazy

@Service
@Lazy
public class HeavyService { ... }

// Only instantiated when first injected/looked up

@Lazy is useful for startup time optimization but can hide configuration errors (not fail-fast).

Related topic: The Spring Design Patterns chapter (→ Chapter 16) discusses in detail how Bean Management patterns relate to GoF patterns (Factory, Singleton, Template Method).


8. Interview Questions

  1. What is the difference between @Component and @Bean? @Component is class-level, registered by component scan. @Bean is method-level in @Configuration class, works for third-party classes too.

  2. What is the difference between @Service and @Repository? Both are @Component specializations. @Repository adds exception translation (SQL exception → DataAccessException). @Service is purely a semantic marker.

  3. How do you resolve multiple implementations of the same interface? @Primary (global default), @Qualifier("name") (explicit name), custom qualifier annotation (type-safe), or List<T> injection (all of them).

  4. What is BeanPostProcessor? A hook into the bean lifecycle. postProcessBeforeInitialization and postProcessAfterInitialization. AOP proxies are created in postProcessAfterInitialization.

  5. What is BeanFactoryPostProcessor? Modifies bean definitions before instantiation. E.g., PropertySourcesPlaceholderConfigurer for @Value resolution.

  6. Why is @Configuration important (vs @Component) for @Bean methods? @Configuration gets a CGLIB proxy → inter-bean references maintain singleton semantics. In @Component, @Bean methods run in "lite mode" — no proxy, every call creates a new instance.

  7. What is bean overriding and how does Spring Boot handle it? When two beans get the same name. Spring Boot 2.1+ forbids it by default (BeanDefinitionOverrideException). Enable with spring.main.allow-bean-definition-overriding=true.


9. Glossary

Term Meaning
@Component General bean marker annotation, registered by component scan
@Service @Component specialization for business logic layer
@Repository @Component specialization for data access + exception translation
@Configuration Configuration class with CGLIB proxy
@Bean Method-level bean definition in @Configuration class
@Primary Designates the default implementation
@Qualifier Selects a bean by name at injection point
BeanPostProcessor Modifies bean instances in the lifecycle
BeanFactoryPostProcessor Modifies bean definitions before instantiation
Component scanning Automatic bean discovery by scanning packages
Exception translation Converts SQL exceptions to DataAccessException (@Repository)
Lite mode @Bean method in @Component, without CGLIB proxy
Bean overriding Overriding a bean with the same name (disabled in Spring Boot)

10. Cheatsheet

Stereotype annotations:
  @Component    →  General bean
  @Service      →  Business logic (semantic)
  @Repository   →  Data access + exception translation
  @Controller   →  Web layer + request mapping

@Bean vs @Component:
  @Component = class-level, component scan
  @Bean      = method-level, @Configuration, third-party

Disambiguation:
  @Primary              →  global default
  @Qualifier("name")    →  explicit selection
  Custom @Qualifier     →  type-safe selection
  List<Interface>       →  all implementations

Lifecycle hooks:
  BeanFactoryPostProcessor  →  modify bean definitions
  BeanPostProcessor         →  modify bean instances
  @PostConstruct            →  initialization after DI
  @PreDestroy               →  cleanup (singleton only)

Bean pipeline:
  Constructor → DI → BPP.before → @PostConstruct → BPP.after → Ready

Common mistakes:
  ✗ @Bean method in @Component (lite mode)
  ✗ @Qualifier string typo
  ✗ Multiple @Primary on same interface
  ✗ @Autowired in BeanPostProcessor
  ✗ DAO without @Repository

Spring Boot defaults:
  Bean overriding = disabled (2.1+)
  @Lazy = not default (eager init)

🎮 Games

10 questions