Java Beans: What, Why, and How

6 minute read

What is a Java Bean?

A Java Bean is simply a Java class that follows specific conventions:

Convention Requirement
No-arg constructor Must have a public default constructor
Private fields Properties are private
Getters/Setters Public accessor methods (getX(), setX(), isX() for booleans)
Serializable Often implements Serializable (optional in Spring)
// A simple Java Bean
@Getter
@Setter
public class Person {
    private String name;
    private int age;
    
    public Person() {}  // no-arg constructor
}

Spring Bean vs Java Bean

Java Bean Spring Bean
Follows getter/setter conventions Any object managed by Spring IoC container
Created by you with new Created and managed by Spring
No lifecycle management Has lifecycle (creation → initialization → use → destruction)
No dependency injection Dependencies injected automatically

Spring Bean = Any object that Spring creates, configures, and manages.

// This is a Spring Bean (managed by Spring)
@Component
public class OrderService {
    private final PaymentService paymentService;  // injected by Spring
    
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

Why Register Beans?

Registration tells Spring: “This object exists, manage it for me.”

Without Registration (Manual Wiring)

// You must create and wire everything manually
public class Application {
    public static void main(String[] args) {
        // Create dependencies manually
        DatabaseConnection db = new DatabaseConnection("jdbc:mysql://...");
        UserRepository userRepo = new UserRepository(db);
        EmailService emailService = new EmailService();
        UserService userService = new UserService(userRepo, emailService);
        OrderService orderService = new OrderService(userService);
        
        // 😩 Painful for large applications with 100+ classes
    }
}

With Registration (Spring Manages It)

@Component
public class DatabaseConnection { }

@Repository
public class UserRepository {
    @Autowired private DatabaseConnection db;
}

@Service
public class UserService {
    @Autowired private UserRepository userRepo;
    @Autowired private EmailService emailService;
}

// Spring automatically:
// 1. Finds all @Component classes
// 2. Creates instances in correct order
// 3. Injects dependencies
// 4. Manages lifecycle

Benefits of Bean Registration

Benefit Description
Dependency Injection Spring injects dependencies automatically
Lifecycle Management Spring handles creation, initialization, destruction
Singleton by Default One instance shared across application (saves memory)
Loose Coupling Classes don’t create their own dependencies
Easy Testing Swap real beans with mocks easily
Configuration Change behavior via properties without code changes

Bean Scopes: Why They Matter

Scope determines how many instances of a bean exist and how long they live.

Available Scopes

Scope Instances Lifetime Use Case
singleton 1 per container Application lifetime Stateless services (default)
prototype New instance per request Until garbage collected Stateful objects
request 1 per HTTP request Single HTTP request Request-specific data
session 1 per HTTP session User session User session data
application 1 per ServletContext Application lifetime Shared across servlets
websocket 1 per WebSocket WebSocket session WebSocket-specific data

Singleton (Default) — One Instance

@Service  // singleton by default
public class PaymentService {
    // Same instance injected everywhere
    // ⚠️ Must be thread-safe (no mutable instance state)
}
┌─────────────────────────────────────────────────┐
│  Spring Container                               │
│  ┌─────────────────────────────────────────┐   │
│  │  PaymentService (single instance)       │   │
│  └─────────────────────────────────────────┘   │
│         ▲              ▲              ▲        │
│         │              │              │        │
│  ┌──────┴──┐    ┌──────┴──┐    ┌──────┴──┐    │
│  │ OrderA  │    │ OrderB  │    │ OrderC  │    │
│  └─────────┘    └─────────┘    └─────────┘    │
└─────────────────────────────────────────────────┘
All orders share the SAME PaymentService instance

Prototype — New Instance Each Time

@Component
@Scope("prototype")
public class ShoppingCart {
    private List<Item> items = new ArrayList<>();
    // Each user gets their own cart instance
}
┌─────────────────────────────────────────────────┐
│  Spring Container                               │
│                                                 │
│  getBean(ShoppingCart.class) → new instance     │
│  getBean(ShoppingCart.class) → new instance     │
│  getBean(ShoppingCart.class) → new instance     │
│                                                 │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐      │
│  │  Cart 1  │  │  Cart 2  │  │  Cart 3  │      │
│  └──────────┘  └──────────┘  └──────────┘      │
└─────────────────────────────────────────────────┘
Each request gets a NEW ShoppingCart instance

Request Scope — Per HTTP Request

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private String correlationId;
    private long startTime;
    // Fresh instance for each HTTP request
}

Why Scope Matters: The Singleton + Prototype Problem

@Service  // singleton
public class OrderService {
    @Autowired
    private ShoppingCart cart;  // prototype
    
    // ⚠️ PROBLEM: cart is injected ONCE at startup
    // All requests share the same cart!
}

// Solution: Use Provider or ObjectFactory
@Service
public class OrderService {
    @Autowired
    private Provider<ShoppingCart> cartProvider;
    
    public void processOrder() {
        ShoppingCart cart = cartProvider.get();  // new instance each time
    }
}

Bean Lifecycle

Spring beans go through a well-defined lifecycle with hooks for custom logic.

Lifecycle Phases

┌─────────────────────────────────────────────────────────────────┐
│                     BEAN LIFECYCLE                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. INSTANTIATION                                               │
│     └── Spring calls constructor                                │
│              │                                                  │
│              ▼                                                  │
│  2. POPULATE PROPERTIES                                         │
│     └── @Autowired fields/setters injected                      │
│              │                                                  │
│              ▼                                                  │
│  3. BEAN NAME AWARE                                             │
│     └── setBeanName() if BeanNameAware                          │
│              │                                                  │
│              ▼                                                  │
│  4. BEAN FACTORY AWARE                                          │
│     └── setBeanFactory() if BeanFactoryAware                    │
│              │                                                  │
│              ▼                                                  │
│  5. PRE-INITIALIZATION (BeanPostProcessor)                      │
│     └── postProcessBeforeInitialization()                       │
│              │                                                  │
│              ▼                                                  │
│  6. INITIALIZATION                                              │
│     ├── @PostConstruct method                                   │
│     ├── InitializingBean.afterPropertiesSet()                   │
│     └── Custom init-method                                      │
│              │                                                  │
│              ▼                                                  │
│  7. POST-INITIALIZATION (BeanPostProcessor)                     │
│     └── postProcessAfterInitialization()                        │
│              │                                                  │
│              ▼                                                  │
│  8. BEAN READY FOR USE ✓                                        │
│              │                                                  │
│              ▼                                                  │
│  9. DESTRUCTION (on container shutdown)                         │
│     ├── @PreDestroy method                                      │
│     ├── DisposableBean.destroy()                                │
│     └── Custom destroy-method                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Lifecycle Callbacks Example

@Component
public class DatabaseConnectionPool implements InitializingBean, DisposableBean {
    
    private List<Connection> connections;
    
    @Autowired
    private DataSourceConfig config;  // injected before @PostConstruct
    
    // Called after dependency injection
    @PostConstruct
    public void init() {
        System.out.println("1. @PostConstruct: Initializing pool");
        connections = new ArrayList<>();
    }
    
    // InitializingBean interface method
    @Override
    public void afterPropertiesSet() {
        System.out.println("2. afterPropertiesSet: Creating connections");
        for (int i = 0; i < config.getPoolSize(); i++) {
            connections.add(createConnection());
        }
    }
    
    // Called before bean destruction
    @PreDestroy
    public void cleanup() {
        System.out.println("3. @PreDestroy: Closing connections");
        connections.forEach(Connection::close);
    }
    
    // DisposableBean interface method
    @Override
    public void destroy() {
        System.out.println("4. destroy: Final cleanup");
    }
}

Output on startup:

1. @PostConstruct: Initializing pool
2. afterPropertiesSet: Creating connections

Output on shutdown:

3. @PreDestroy: Closing connections
4. destroy: Final cleanup

Preferred Approach: Use Annotations

@Service
public class CacheService {
    
    private Cache cache;
    
    @PostConstruct
    public void initializeCache() {
        // Called once after all dependencies are injected
        cache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();
        System.out.println("Cache initialized");
    }
    
    @PreDestroy
    public void clearCache() {
        // Called before bean is destroyed (app shutdown)
        cache.invalidateAll();
        System.out.println("Cache cleared");
    }
}

Lifecycle Summary Table

Callback When Use Case
Constructor First Basic instantiation
@Autowired After constructor Dependency injection
@PostConstruct After injection Initialize resources, validate state
afterPropertiesSet() After @PostConstruct Alternative to @PostConstruct
@PreDestroy Before destruction Release resources, cleanup
destroy() After @PreDestroy Alternative to @PreDestroy

Why Lifecycle Matters

Scenario Lifecycle Hook
Open database connections on startup @PostConstruct
Start background thread/scheduler @PostConstruct
Validate required configuration @PostConstruct
Close connections on shutdown @PreDestroy
Stop background threads gracefully @PreDestroy
Flush caches/buffers @PreDestroy