Appearance
Dependency Injection
Dependency Injection (DI) is a core design principle of the Spring Framework and is fundamental to the architecture of this application. DI allows for the creation of loosely coupled components by inverting control: instead of components creating their own dependencies, the dependencies are "injected" into them by an external container—in this case, the Spring IoC (Inversion of Control) container.
This document details how DI is implemented across the application, the patterns we follow, and how custom components are configured and managed by the Spring container. For a higher-level view of how these components fit together, refer to the Architecture Overview.
Spring Container
The Spring IoC container is responsible for instantiating, configuring, and assembling objects known as beans. It gets its instructions on what objects to manage from configuration metadata, which in our application is primarily provided through annotations.
Component Scanning
Spring Boot automatically scans for components to register as beans in the container. This process is enabled by the @SpringBootApplication annotation on our main application class. By default, it scans the package containing the main class and all its sub-packages.
Classes are marked as candidates for component scanning using stereotype annotations:
@RestController: Marks a class as a web controller, responsible for handling incoming HTTP requests. SeeTicketController.@Service: Marks a class for holding business logic in the service layer. SeeTicketService.@Configuration: Declares that a class contains one or more@Beanmethods and may be processed by the Spring container to generate bean definitions. SeeRabbitMQConfig.@Repository: While not explicitly used on our own classes, Spring Data JPA automatically creates repository bean implementations (likeTicketRepository) and annotates them with@Repository. This also enables JPA exception translation.
For more information on the roles of these components, see the Core Components documentation.
Bean Lifecycle
The Spring container manages the complete lifecycle of every bean it creates, from instantiation to destruction. This includes:
- Instantiation: The container creates an instance of the bean.
- Populating Properties: The container injects all specified dependencies.
- Initialization: If defined, custom initialization methods (e.g., methods annotated with
@PostConstruct) are called. - In Use: The bean is now ready and available for the application to use.
- Destruction: When the container is shut down, it calls any defined destruction callbacks (e.g., methods annotated with
@PreDestroy) to allow the bean to release resources.
Our application relies on this managed lifecycle to ensure that dependencies like the TicketService are fully configured before being used by the TicketController.
Auto-configuration
Spring Boot's auto-configuration feature attempts to automatically configure the application based on the JAR dependencies on the classpath. For example:
spring-boot-starter-web: Auto-configuresDispatcherServlet, an embedded web server (Tomcat by default), and other web-related infrastructure.spring-boot-starter-data-jpa: Auto-configures aDataSource, anEntityManagerFactory, and aTransactionManagerbased on themysql-connector-jdriver and database connection details in ourapplication.properties.spring-boot-starter-amqp: Auto-configures a RabbitMQConnectionFactoryand a defaultRabbitTemplate, which we then customize in ourRabbitMQConfig.
This convention-over-configuration approach significantly reduces boilerplate and allows developers to focus on business logic.
Injection Patterns
The application standardizes on specific DI patterns to maintain consistency and readability.
Constructor Injection (Preferred)
Constructor injection is the primary DI pattern used throughout the application. Dependencies are provided as parameters to a class's constructor. This is the recommended approach for mandatory dependencies.
Benefits:
- Immutability: Dependencies can be declared as
final, ensuring they cannot be changed after the object is constructed. - Explicitness: A class's dependencies are clearly listed in its constructor signature.
- Testability: It is easy to instantiate the class in unit tests by manually passing mock or stub implementations of its dependencies.
We leverage Lombok's @RequiredArgsConstructor to implement this pattern cleanly and without boilerplate code. This annotation automatically generates a constructor that accepts an argument for each final field.
Example: TicketController.java The TicketController requires a TicketService to function. By marking the ticketService field as final, @RequiredArgsConstructor generates the necessary constructor for Spring to use for injection.
java
// src/main/java/com/slalom/demo/ticketing/controller/TicketController.java
@RestController
@RequestMapping("/api/tickets")
@RequiredArgsConstructor // Generates: public TicketController(TicketService ticketService) { ... }
@Slf4j
public class TicketController {
// The dependency is declared as final, making it mandatory.
private final TicketService ticketService;
// ... methods using ticketService
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Similarly, TicketService injects TicketRepository and MessagePublisher:
java
// src/main/java/com/slalom/demo/ticketing/service/TicketService.java
@Service
@RequiredArgsConstructor
@Slf4j
public class TicketService {
private final TicketRepository ticketRepository;
private final MessagePublisher messagePublisher;
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Field Injection with @Autowired
Field injection (using @Autowired directly on a field) is discouraged in this project. While it may seem simpler, it has several drawbacks:
- It obscures the class's dependencies.
- It makes unit testing more difficult, as it requires reflection or a Spring context to set the injected fields.
- It can hide potential design problems, such as a class having too many dependencies.
You will not find this pattern in our primary application components like controllers and services.
Component Annotations (@Service, @Repository, @RestController)
As mentioned under Component Scanning, stereotype annotations are crucial for DI. They not only mark a class for discovery but also make it a candidate for injection into other components. When Spring finds a field of type TicketService in TicketController's constructor, it looks for a bean of that type in its container—which it finds because TicketService is annotated with @Service.
Configuration Beans
For beans that require complex setup logic or need to be configured based on external properties, we use dedicated @Configuration classes.
RabbitMQConfig Bean Definitions
The RabbitMQConfig.java class is a prime example of custom bean configuration. It sets up all the necessary components for interacting with RabbitMQ.
java
// src/main/java/com/slalom/demo/ticketing/config/RabbitMQConfig.java
@Configuration
public class RabbitMQConfig {
@Value("${rabbitmq.exchange.name}")
private String exchangeName;
@Value("${rabbitmq.queue.name}")
private String queueName;
@Value("${rabbitmq.routing.key}")
private String routingKey;
// Defines the queue bean
@Bean
public Queue queue() {
return new Queue(queueName, true); // durable queue
}
// Defines the exchange bean
@Bean
public TopicExchange exchange() {
return new TopicExchange(exchangeName);
}
// Defines the binding between the queue and exchange
// Note how Spring injects the queue and exchange beans as parameters
@Bean
public Binding binding(Queue queue, TopicExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(routingKey);
}
// Defines a custom message converter to use JSON
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
// Defines a customized RabbitTemplate, overriding the auto-configured default
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
// Set our custom JSON message converter
rabbitTemplate.setMessageConverter(messageConverter());
return rabbitTemplate;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Key Takeaways from RabbitMQConfig:
@Bean: Each method annotated with@Beanproduces a bean that is registered in the Spring container.- Inter-bean Dependencies: Spring automatically resolves dependencies between beans. The
bindingbean method takesQueueandTopicExchangeas parameters, and Spring injects the beans produced by thequeue()andexchange()methods. - Customization of Auto-configuration: The
rabbitTemplatebean method accepts aConnectionFactoryparameter. ThisConnectionFactoryis the one created by Spring Boot's auto-configuration. Our method then customizes theRabbitTemplateby setting a JSON message converter, demonstrating a powerful pattern of combining auto-configuration with explicit customization. - Externalized Configuration: The
@Valueannotation injects values fromapplication.properties, making the configuration flexible and environment-agnostic.
Bean Scope and Lifecycle
By default, all beans defined in the application are singletons. This means the Spring container creates only one instance of each bean (e.g., one TicketService instance, one RabbitTemplate instance) and shares it throughout the application.
Implications for Developers:
- Thread Safety: Singleton beans, especially services and controllers, must be designed to be thread-safe. They handle requests from multiple threads concurrently.
- State Management: Avoid storing request-specific or user-specific state in instance fields of singleton beans. Doing so can lead to race conditions and data corruption. All state should be contained within the scope of a method call.
While other scopes like prototype, request, and session are available, our application's stateless service architecture is well-served by the default singleton scope.