Java Beans & Spring Beans — what, why, scopes and lifecycle

This post explains what a Java Bean is, how Spring treats “beans”, why registering beans with the container is useful, the different bean scopes, common pitfalls, and the Spring bean lifecycle with code examples you can copy.

What is a Java Bean?

A Java Bean is a simple Java class that follows these common conventions:

Example:

public class Person {
    private String name;
    private int age;

    public Person() {}

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

JavaBean conventions make a class easy to introspect, serialize, and manipulate by IDE tools, frameworks, and libraries (e.g., JSP, JSF, older frameworks, and some serialization libraries).

Spring Bean vs Java Bean

Example of Spring-managed bean via stereotype:

@Service
public class OrderService {
    private final PaymentService paymentService;

    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

Example of Spring-managed bean via Java configuration:

@Configuration
public class AppConfig {
    @Bean
    public PaymentService paymentService() {
        return new PaymentService();
    }

    @Bean
    public OrderService orderService(PaymentService paymentService) {
        return new OrderService(paymentService);
    }
}

Why register beans with the container?

Registration tells Spring to create, configure, and manage an object for you. Benefits:

Manual wiring quickly becomes unmanageable for medium/large apps — Spring registration reduces boilerplate and centralizes object creation.

How to register beans

  1. Stereotype annotations (@Component, @Service, @Repository, @Controller)
    • Use @ComponentScan or @SpringBootApplication to auto-discover.
@Component
public class EmailService { }
  1. Java @Configuration classes with @Bean methods (explicit registration):
@Configuration
public class AppConfig {
    @Bean
    public CacheService cacheService() {
        return new CacheService();
    }
}
  1. XML configuration (legacy):
<bean id="emailService" class="com.example.EmailService" />

Bean scopes — what they mean and when to use them

Scope controls how many instances of a bean are created and how long they live.

Common scopes (Spring Core + Web):

Example: prototype scope

@Component
@Scope("prototype")
public class ShoppingCart {
    private final List<String> items = new ArrayList<>();
    public void add(String item) { items.add(item); }
}

Request scope example (web):

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private final String requestId = UUID.randomUUID().toString();
    public String getRequestId() { return requestId; }
}

Singleton vs Prototype gotcha

If a singleton bean depends on a prototype bean and the prototype is injected directly, that prototype instance is created once at container startup and reused — defeating prototype semantics.

Bad (unexpected behavior):

@Service  // singleton
public class OrderService {
    @Autowired
    private ShoppingCart cart; // injected once → reused across requests
}

Solutions:

Example with provider:

@Service
public class OrderService {
    @Autowired
    private ObjectProvider<ShoppingCart> cartProvider;

    public void process() {
        ShoppingCart cart = cartProvider.getIfAvailable(); // new instance each time
    }
}

Thread-safety and scope

Bean lifecycle (creation → destruction)

Spring manages a bean’s lifecycle with well-defined callbacks and extension points. The following flow applies to singleton beans (prototype has different destruction semantics):

  1. Instantiation — container creates the bean instance (calls constructor).
  2. Populate properties — dependency injection (@Autowired, setter injection).
  3. Aware callbacks — BeanNameAware#setBeanName, BeanFactoryAware#setBeanFactory, etc.
  4. BeanPostProcessor#postProcessBeforeInitialization — pre-init hooks.
  5. Initialization — @PostConstruct methods, InitializingBean.afterPropertiesSet(), or custom init-method.
  6. BeanPostProcessor#postProcessAfterInitialization — post-init hooks (where proxies are often created).
  7. Bean ready to use.
  8. Destruction — on context close: @PreDestroy, DisposableBean.destroy(), or custom destroy-method.

Example with lifecycle callbacks

@Component
public class DatabaseConnectionPool implements InitializingBean, DisposableBean {

    @Autowired private DataSourceConfig config;
    private List<Connection> connections;

    @PostConstruct
    public void init() {
        // runs after dependencies are injected
        connections = new ArrayList<>();
    }

    @Override
    public void afterPropertiesSet() {
        // final init step — safe to create connections
        for (int i = 0; i < config.getPoolSize(); i++) {
            connections.add(createConnection());
        }
    }

    @PreDestroy
    public void cleanup() {
        // runs before destruction
        connections.forEach(c -> closeQuietly(c));
    }

    @Override
    public void destroy() {
        // final cleanup
    }
}

Notes:

Common best practices

Quick checklist for migrating code to Spring-managed beans