BLOG POSTS
Spring IoC Bean Example Tutorial

Spring IoC Bean Example Tutorial

Spring’s Inversion of Control (IoC) container is the backbone of dependency injection in Spring applications, managing object lifecycles and their dependencies automatically. Understanding how to create and configure beans properly is crucial for building maintainable, testable applications that scale well in production environments. This tutorial will walk you through everything from basic bean configuration to advanced scenarios, covering XML configuration, annotations, Java-based configuration, and real-world troubleshooting scenarios that every developer encounters.

How Spring IoC Container Works

The Spring IoC container operates on the principle of dependency injection, where objects don’t create their dependencies directly but receive them from the container. The container reads configuration metadata (XML, annotations, or Java classes) to understand what beans to create and how to wire them together.

Here’s the basic flow:

  • Container reads configuration metadata
  • Creates bean definitions based on metadata
  • Instantiates beans following their scope and lifecycle
  • Injects dependencies between beans
  • Makes beans available for application use

The two main container types are BeanFactory (lightweight, lazy loading) and ApplicationContext (feature-rich, eager loading). ApplicationContext is what you’ll use 99% of the time since it provides additional features like event propagation, internationalization, and AOP integration.

Basic Bean Configuration Methods

Spring offers three primary ways to configure beans, each with distinct advantages depending on your project requirements and team preferences.

XML Configuration

Traditional XML configuration remains popular in enterprise environments where explicit configuration is preferred:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="com.example.service.UserService">
        <property name="userRepository" ref="userRepository"/>
        <property name="maxRetries" value="3"/>
    </bean>

    <bean id="userRepository" class="com.example.repository.JdbcUserRepository">
        <constructor-arg ref="dataSource"/>
    </bean>

    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/myapp"/>
        <property name="username" value="dbuser"/>
        <property name="password" value="dbpass"/>
    </bean>
</beans>

Annotation-Based Configuration

Annotations reduce XML verbosity and keep configuration close to the code:

@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Value("${app.max.retries:3}")
    private int maxRetries;
    
    public User createUser(String username, String email) {
        // Implementation here
        return userRepository.save(new User(username, email));
    }
}

@Repository
public class JdbcUserRepository implements UserRepository {
    
    private final JdbcTemplate jdbcTemplate;
    
    @Autowired
    public JdbcUserRepository(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    
    @Override
    public User save(User user) {
        // JDBC implementation
        String sql = "INSERT INTO users (username, email) VALUES (?, ?)";
        jdbcTemplate.update(sql, user.getUsername(), user.getEmail());
        return user;
    }
}

Java-Based Configuration

Type-safe configuration using Java classes provides compile-time checking:

@Configuration
@ComponentScan(basePackages = "com.example")
@PropertySource("classpath:application.properties")
public class AppConfig {
    
    @Value("${db.url}")
    private String dbUrl;
    
    @Value("${db.username}")
    private String dbUsername;
    
    @Value("${db.password}")
    private String dbPassword;
    
    @Bean
    @Primary
    public DataSource dataSource() {
        BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
        ds.setUrl(dbUrl);
        ds.setUsername(dbUsername);
        ds.setPassword(dbPassword);
        ds.setMaxTotal(20);
        ds.setMaxIdle(10);
        return ds;
    }
    
    @Bean
    @Profile("test")
    public DataSource testDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("schema.sql")
            .addScript("test-data.sql")
            .build();
    }
}

Step-by-Step Implementation Guide

Let’s build a complete example from scratch, demonstrating a realistic e-commerce order processing system.

Step 1: Project Setup

Add Spring dependencies to your Maven project:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.21</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.3.21</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.29</version>
    </dependency>
</dependencies>

Step 2: Create Domain Models

public class Order {
    private Long id;
    private String customerEmail;
    private BigDecimal totalAmount;
    private OrderStatus status;
    private LocalDateTime createdAt;
    
    // Constructors, getters, setters
    public Order(String customerEmail, BigDecimal totalAmount) {
        this.customerEmail = customerEmail;
        this.totalAmount = totalAmount;
        this.status = OrderStatus.PENDING;
        this.createdAt = LocalDateTime.now();
    }
    
    // Other methods...
}

public enum OrderStatus {
    PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}

Step 3: Create Service Layer

public interface OrderService {
    Order createOrder(String customerEmail, BigDecimal amount);
    Order getOrder(Long orderId);
    void processOrder(Long orderId);
}

@Service
@Transactional
public class OrderServiceImpl implements OrderService {
    
    private final OrderRepository orderRepository;
    private final EmailService emailService;
    private final PaymentService paymentService;
    
    @Autowired
    public OrderServiceImpl(OrderRepository orderRepository, 
                           EmailService emailService,
                           PaymentService paymentService) {
        this.orderRepository = orderRepository;
        this.emailService = emailService;
        this.paymentService = paymentService;
    }
    
    @Override
    public Order createOrder(String customerEmail, BigDecimal amount) {
        Order order = new Order(customerEmail, amount);
        Order savedOrder = orderRepository.save(order);
        emailService.sendOrderConfirmation(savedOrder);
        return savedOrder;
    }
    
    @Override
    @Transactional(readOnly = true)
    public Order getOrder(Long orderId) {
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId));
    }
    
    @Override
    public void processOrder(Long orderId) {
        Order order = getOrder(orderId);
        if (paymentService.processPayment(order)) {
            order.setStatus(OrderStatus.CONFIRMED);
            orderRepository.save(order);
            emailService.sendPaymentConfirmation(order);
        }
    }
}

Step 4: Repository Implementation

public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(Long id);
    List<Order> findByCustomerEmail(String email);
}

@Repository
public class JdbcOrderRepository implements OrderRepository {
    
    private final JdbcTemplate jdbcTemplate;
    private final RowMapper<Order> orderRowMapper;
    
    @Autowired
    public JdbcOrderRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.orderRowMapper = (rs, rowNum) -> {
            Order order = new Order(
                rs.getString("customer_email"),
                rs.getBigDecimal("total_amount")
            );
            order.setId(rs.getLong("id"));
            order.setStatus(OrderStatus.valueOf(rs.getString("status")));
            return order;
        };
    }
    
    @Override
    public Order save(Order order) {
        if (order.getId() == null) {
            String sql = "INSERT INTO orders (customer_email, total_amount, status, created_at) VALUES (?, ?, ?, ?)";
            KeyHolder keyHolder = new GeneratedKeyHolder();
            
            jdbcTemplate.update(connection -> {
                PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
                ps.setString(1, order.getCustomerEmail());
                ps.setBigDecimal(2, order.getTotalAmount());
                ps.setString(3, order.getStatus().name());
                ps.setTimestamp(4, Timestamp.valueOf(order.getCreatedAt()));
                return ps;
            }, keyHolder);
            
            order.setId(keyHolder.getKey().longValue());
        } else {
            String sql = "UPDATE orders SET status = ? WHERE id = ?";
            jdbcTemplate.update(sql, order.getStatus().name(), order.getId());
        }
        return order;
    }
    
    @Override
    public Optional<Order> findById(Long id) {
        String sql = "SELECT * FROM orders WHERE id = ?";
        try {
            Order order = jdbcTemplate.queryForObject(sql, orderRowMapper, id);
            return Optional.of(order);
        } catch (EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }
}

Step 5: Application Configuration

@Configuration
@ComponentScan(basePackages = "com.example")
@EnableTransactionManagement
@PropertySource("classpath:application.properties")
public class ApplicationConfig {
    
    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/ecommerce");
        config.setUsername("app_user");
        config.setPassword("app_password");
        config.setMaximumPoolSize(20);
        config.setMinimumIdle(5);
        config.setConnectionTimeout(30000);
        return new HikariDataSource(config);
    }
    
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
    
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

Step 6: Application Bootstrap

public class ECommerceApplication {
    
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        
        OrderService orderService = context.getBean(OrderService.class);
        
        // Create and process an order
        Order order = orderService.createOrder("customer@example.com", new BigDecimal("99.99"));
        System.out.println("Created order: " + order.getId());
        
        orderService.processOrder(order.getId());
        System.out.println("Order processed successfully");
    }
}

Bean Scopes and Lifecycle Management

Understanding bean scopes is crucial for memory management and application behavior, especially when deploying on resource-constrained environments like those provided by VPS hosting.

Scope Description Use Case Memory Impact
singleton One instance per container Stateless services, repositories Low
prototype New instance each time Stateful objects, commands High
request One per HTTP request Web request handlers Medium
session One per HTTP session User session data Medium
application One per ServletContext Application-wide cache Low

Example of scope configuration and lifecycle hooks:

@Component
@Scope("prototype")
public class OrderProcessor {
    
    @PostConstruct
    public void initialize() {
        System.out.println("OrderProcessor initialized: " + this.hashCode());
        // Initialize resources, connections, etc.
    }
    
    @PreDestroy
    public void cleanup() {
        System.out.println("OrderProcessor destroyed: " + this.hashCode());
        // Clean up resources, close connections, etc.
    }
    
    public void processOrder(Order order) {
        // Processing logic here
    }
}

@Configuration
public class ScopeConfig {
    
    @Bean
    @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public ExpensiveService expensiveService() {
        return new ExpensiveService();
    }
}

Real-World Use Cases and Examples

Microservices Configuration

When building microservices that need to scale on dedicated servers, proper bean configuration becomes critical:

@Configuration
@Profile("production")
public class ProductionConfig {
    
    @Bean
    @Primary
    public RestTemplate restTemplate() {
        RestTemplate template = new RestTemplate();
        
        // Connection pooling for better performance
        HttpComponentsClientHttpRequestFactory factory = 
            new HttpComponentsClientHttpRequestFactory();
        factory.setConnectTimeout(5000);
        factory.setReadTimeout(10000);
        
        template.setRequestFactory(factory);
        return template;
    }
    
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        LettuceConnectionFactory factory = new LettuceConnectionFactory(
            new RedisStandaloneConfiguration("redis-cluster.internal", 6379)
        );
        factory.setDatabase(0);
        return factory;
    }
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheManager.Builder builder = RedisCacheManager
            .RedisCacheManagerBuilder
            .fromConnectionFactory(connectionFactory)
            .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30)));
        return builder.build();
    }
}

Database Connection Management

@Configuration
public class DatabaseConfig {
    
    @Bean
    @ConfigurationProperties("app.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    @ConfigurationProperties("app.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    @Primary
    public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource ds) {
        return new JdbcTemplate(ds);
    }
    
    @Bean
    public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource ds) {
        return new JdbcTemplate(ds);
    }
}

Event-Driven Architecture

@Component
public class OrderEventListener {
    
    private final EmailService emailService;
    private final InventoryService inventoryService;
    
    @Autowired
    public OrderEventListener(EmailService emailService, InventoryService inventoryService) {
        this.emailService = emailService;
        this.inventoryService = inventoryService;
    }
    
    @EventListener
    @Async
    public void handleOrderCreated(OrderCreatedEvent event) {
        emailService.sendOrderConfirmation(event.getOrder());
    }
    
    @EventListener
    public void handleOrderConfirmed(OrderConfirmedEvent event) {
        inventoryService.reserveItems(event.getOrder().getItems());
    }
}

@Service
public class OrderService {
    
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    public Order createOrder(CreateOrderRequest request) {
        Order order = new Order(request);
        Order savedOrder = orderRepository.save(order);
        
        // Publish event for async processing
        eventPublisher.publishEvent(new OrderCreatedEvent(savedOrder));
        
        return savedOrder;
    }
}

Performance Optimization and Best Practices

Bean Creation Performance

Monitor bean creation times and optimize accordingly:

@Configuration
@EnableConfigurationProperties(AppProperties.class)
public class PerformanceConfig {
    
    @Bean
    @Lazy // Only create when first accessed
    public ExpensiveResource expensiveResource() {
        return new ExpensiveResource();
    }
    
    @Bean
    @ConditionalOnProperty(name = "feature.advanced.enabled", havingValue = "true")
    public AdvancedFeatureService advancedFeatureService() {
        return new AdvancedFeatureService();
    }
    
    // Use @PostConstruct for expensive initialization
    @Bean
    public CacheService cacheService() {
        return new CacheService();
    }
}

@Component
public class CacheService {
    
    private Map<String, Object> cache;
    
    @PostConstruct
    public void initializeCache() {
        // Expensive initialization moved to PostConstruct
        this.cache = loadCacheFromDatabase();
    }
    
    private Map<String, Object> loadCacheFromDatabase() {
        // Expensive operation
        return new HashMap<>();
    }
}

Memory Management

@Configuration
public class MemoryOptimizedConfig {
    
    @Bean
    @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public BatchProcessor batchProcessor() {
        return new BatchProcessor();
    }
    
    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(8);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

Common Issues and Troubleshooting

Circular Dependencies

One of the most common issues developers face:

// Problem: Circular dependency
@Service
public class UserService {
    @Autowired
    private OrderService orderService; // OrderService depends on UserService
}

@Service  
public class OrderService {
    @Autowired
    private UserService userService; // Creates circular dependency
}

// Solution 1: Constructor injection with @Lazy
@Service
public class UserService {
    private final OrderService orderService;
    
    public UserService(@Lazy OrderService orderService) {
        this.orderService = orderService;
    }
}

// Solution 2: Refactor to remove circular dependency
@Service
public class UserOrderService {
    private final UserService userService;
    private final OrderService orderService;
    
    // Handles operations that need both services
}

Bean Not Found Errors

// Problem: Bean not found
@Service
public class PaymentService {
    @Autowired
    private PaymentGateway paymentGateway; // No bean of this type
}

// Solution 1: Create the missing bean
@Configuration
public class PaymentConfig {
    @Bean
    public PaymentGateway paymentGateway() {
        return new StripePaymentGateway();
    }
}

// Solution 2: Use @ConditionalOnBean for optional dependencies
@Service
@ConditionalOnBean(PaymentGateway.class)
public class PaymentService {
    @Autowired
    private PaymentGateway paymentGateway;
}

Profile-Specific Configuration Issues

// Problem: Wrong profile configuration
@Component
@Profile("prod") // Only active in 'prod' profile
public class ProductionEmailService implements EmailService {
    // Implementation
}

// Solution: Provide default implementation
@Component
@Profile("!prod") // Active when 'prod' is NOT active
public class MockEmailService implements EmailService {
    // Mock implementation for development
}

// Or use @ConditionalOnMissingBean
@Component
@ConditionalOnMissingBean(EmailService.class)
public class DefaultEmailService implements EmailService {
    // Default implementation
}

Bean Validation Errors

@Configuration
@Validated
public class DatabaseConfig {
    
    @Bean
    public DataSource dataSource(
            @Value("${db.url}") @NotBlank String url,
            @Value("${db.username}") @NotBlank String username,
            @Value("${db.password}") @NotBlank String password) {
        
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(url);
        config.setUsername(username);
        config.setPassword(password);
        return new HikariDataSource(config);
    }
}

Configuration Alternatives Comparison

Aspect XML Configuration Annotation-Based Java Configuration
Learning Curve Easy for beginners Medium Requires Java knowledge
Type Safety No compile-time checking Limited Full compile-time checking
Refactoring Support Poor Good Excellent
Configuration Flexibility High Medium Very High
Startup Performance Slower (XML parsing) Fast Fastest
External Tool Support Excellent Good Good

Advanced Bean Configuration Patterns

Factory Beans

@Component
public class ConnectionFactoryBean implements FactoryBean<Connection> {
    
    @Value("${database.url}")
    private String url;
    
    @Override
    public Connection getObject() throws Exception {
        // Complex connection creation logic
        return DriverManager.getConnection(url);
    }
    
    @Override
    public Class<?> getObjectType() {
        return Connection.class;
    }
    
    @Override
    public boolean isSingleton() {
        return false; // New connection each time
    }
}

Conditional Bean Creation

@Configuration
public class ConditionalConfig {
    
    @Bean
    @ConditionalOnProperty(name = "cache.type", havingValue = "redis")
    public CacheManager redisCacheManager() {
        return new RedisCacheManager.Builder(redisConnectionFactory()).build();
    }
    
    @Bean
    @ConditionalOnProperty(name = "cache.type", havingValue = "memory", matchIfMissing = true)
    public CacheManager memoryCacheManager() {
        return new ConcurrentMapCacheManager();
    }
    
    @Bean
    @ConditionalOnClass(name = "com.fasterxml.jackson.databind.ObjectMapper")
    public JsonSerializer jsonSerializer() {
        return new JacksonJsonSerializer();
    }
}

Spring’s IoC container provides powerful dependency management capabilities that scale from simple applications to complex enterprise systems. The key to success lies in choosing the right configuration approach for your team and project requirements, understanding bean lifecycles, and following established patterns for common scenarios. When deploying Spring applications, proper bean configuration becomes even more critical for optimal resource utilization and performance.

For additional information, refer to the official Spring Framework Documentation and the comprehensive Spring Guides for more advanced use cases and patterns.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

Your email address will not be published. Required fields are marked