
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.