home / skills / giuseppe-trisciuoglio / developer-kit / spring-boot-dependency-injection

This skill streamlines Spring Boot dependency injection by enforcing constructor-first patterns, optional collaborators, and explicit bean configuration to

npx playbooks add skill giuseppe-trisciuoglio/developer-kit --skill spring-boot-dependency-injection

Review the files below or copy the command above to add this skill to your agents.

Files (4)
SKILL.md
8.7 KB
---
name: spring-boot-dependency-injection
description: Provides dependency injection patterns for Spring Boot projects covering constructor-first patterns, optional collaborator handling, bean selection, and validation practices. Use when configuring beans, wiring dependencies, or troubleshooting injection issues.
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
category: backend
tags: [spring-boot, dependency-injection, constructor-injection, bean-configuration, autowiring, testing, java]
version: 1.1.0
---

# Spring Boot Dependency Injection

This skill captures the dependency injection approach promoted in this repository: constructor-first design, explicit optional collaborators, and deterministic configuration that keeps services testable and framework-agnostic.

## Overview

- Prioritize constructor injection to keep dependencies explicit, immutable, and mockable.
- Treat optional collaborators through guarded setters or providers while documenting defaults.
- Resolve bean ambiguity intentionally through qualifiers, primary beans, and profiles.
- Validate wiring with focused unit tests before relying on Spring's TestContext framework.

## When to Use

- Implement constructor injection for new `@Service`, `@Component`, or `@Repository` classes.
- Replace legacy field injection while modernizing Spring modules.
- Configure optional or pluggable collaborators (feature flags, multi-tenant adapters).
- Audit bean definitions before adding integration tests or migrating Spring Boot versions.

## Instructions

Follow these steps to implement proper dependency injection in Spring Boot:

### 1. Identify Dependencies

List all collaborators required by each class. Distinguish between mandatory dependencies (required for operation) and optional dependencies (feature-specific).

### 2. Apply Constructor Injection

Create constructors that accept all mandatory dependencies. Mark fields as final. Use Lombok @RequiredArgsConstructor to reduce boilerplate.

### 3. Handle Optional Collaborators

For optional dependencies, provide setter methods with @Autowired(required = false) or use ObjectProvider<T> for lazy resolution. Include default no-op implementations.

### 4. Configure Beans

Declare @Bean methods in @Configuration classes. Use conditional annotations (@ConditionalOnProperty, @ConditionalOnMissingBean) for environment-specific beans.

### 5. Resolve Ambiguity

Apply @Primary to default implementations. Use @Qualifier with specific names when multiple beans of the same type exist.

### 6. Validate Wiring

Write unit tests that instantiate classes directly with mock dependencies. Add slice tests (@WebMvcTest, @DataJpaTest) to verify Spring context loads without errors.

### 7. Review Configuration

Ensure no field injection (@Autowired on fields) remains. Verify circular dependencies are resolved through domain events or extracted services.

## Prerequisites

- Align project with Java 17+ and Spring Boot 3.5.x (or later) to leverage records and `@ServiceConnection`.
- Keep build tooling ready to run `./gradlew test` or `mvn test` for validation.
- Load supporting material from `./references/` when deeper patterns or samples are required.

## Workflow

### 1. Map Collaborators
- Inventory constructors, `@Autowired` members, and configuration classes.
- Classify dependencies as mandatory (must exist) or optional (feature-flagged, environment-specific).

### 2. Apply Constructor Injection
- Introduce constructors (or Lombok `@RequiredArgsConstructor`) that accept every mandatory collaborator.
- Mark injected fields `final` and protect invariants with `Objects.requireNonNull` if Lombok is not used.
- Update `@Configuration` or `@Bean` factories to pass dependencies explicitly; consult `./references/reference.md` for canonical bean wiring.

### 3. Handle Optional Collaborators
- Supply setters annotated with `@Autowired(required = false)` or inject `ObjectProvider<T>` for lazy access.
- Provide deterministic defaults (for example, no-op implementations) and document them inside configuration modules.
- Follow `./references/examples.md#example-2-setter-injection-for-optional-dependencies` for a full workflow.

### 4. Resolve Bean Selection
- Choose `@Primary` for dominant implementations and `@Qualifier` for niche variants.
- Use profiles, conditional annotations, or factory methods to isolate environment-specific wiring.
- Reference `./references/reference.md#conditional-bean-registration` for conditional and profile-based samples.

### 5. Validate Wiring
- Write unit tests that instantiate classes manually with mocks to prove Spring-free testability.
- Add slice or integration tests (`@WebMvcTest`, `@DataJpaTest`, `@SpringBootTest`) only after constructor contracts are validated.
- Reuse patterns in `./references/reference.md#testing-with-dependency-injection` to select the proper test style.

## Examples

### Basic Constructor Injection
```java
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;

    public User register(UserRegistrationRequest request) {
        User user = User.create(request.email(), request.name());
        userRepository.save(user);
        emailService.sendWelcome(user);
        return user;
    }
}
```
- Instantiate directly in tests: `new UserService(mockRepo, mockEmailService);` with no Spring context required.

### Intermediate: Optional Dependency with Guarded Setter
```java
@Service
public class ReportService {
    private final ReportRepository reportRepository;
    private CacheService cacheService = CacheService.noOp();

    public ReportService(ReportRepository reportRepository) {
        this.reportRepository = reportRepository;
    }

    @Autowired(required = false)
    public void setCacheService(CacheService cacheService) {
        this.cacheService = cacheService;
    }
}
```
- Provide fallbacks such as `CacheService.noOp()` to ensure deterministic behavior when the optional bean is absent.

### Advanced: Conditional Configuration Across Modules
```java
@Configuration
@Import(DatabaseConfig.class)
public class MessagingConfig {

    @Bean
    @ConditionalOnProperty(name = "feature.notifications.enabled", havingValue = "true")
    public NotificationService emailNotificationService(JavaMailSender sender) {
        return new EmailNotificationService(sender);
    }

    @Bean
    @ConditionalOnMissingBean(NotificationService.class)
    public NotificationService noopNotificationService() {
        return NotificationService.noOp();
    }
}
```
- Combine `@Import`, profiles, and conditional annotations to orchestrate cross-cutting modules.

Additional worked examples (including tests and configuration wiring) are available in `./references/examples.md`.

## Best Practices

- Prefer constructor injection for mandatory dependencies; allow Spring 4.3+ to infer `@Autowired` on single constructors.
- Encapsulate optional behavior inside dedicated adapters or providers instead of accepting `null` pointers.
- Keep service constructors lightweight; extract orchestrators when dependency counts exceed four.
- Favor domain interfaces in the domain layer and defer framework imports to infrastructure adapters.
- Document bean names and qualifiers in shared constants to avoid typo-driven mismatches.

## Constraints and Warnings

- Avoid field injection and service locator patterns because they obscure dependencies and impede unit testing.
- Prevent circular dependencies by publishing domain events or extracting shared abstractions.
- Limit `@Lazy` usage to performance-sensitive paths and record the deferred initialization risk.
- Do not add profile-specific beans without matching integration tests that activate the profile.
- Ensure each optional collaborator has a deterministic default or feature-flag handling path.
- Never use `@Autowired` on fields when constructor injection is possible.
- Be aware that optional dependencies can lead to `NullPointerException` if not properly handled.
- Profile-specific beans can cause runtime errors if profiles are not correctly activated.

## Reference Materials

- [extended documentation covering annotations, bean scopes, testing, and anti-pattern mitigations](references/reference.md)
- [progressive examples from constructor injection basics to multi-module configurations](references/examples.md)
- [curated excerpts from the official Spring Framework documentation (constructor vs setter guidance, conditional wiring)](references/spring-official-dependency-injection.md)

## Related Skills

- `spring-boot-crud-patterns` – service-layer orchestration patterns that rely on constructor injection.
- `spring-boot-rest-api-standards` – controller-layer practices that assume explicit dependency wiring.
- `unit-test-service-layer` – Mockito-based testing patterns for constructor-injected services.

Overview

This skill provides a practical dependency injection guide for Spring Boot projects, promoting constructor-first design, explicit optional collaborator handling, and deterministic bean configuration. It focuses on keeping services framework-agnostic, testable, and easy to reason about during development and refactors.

How this skill works

The skill inspects dependency wiring patterns and prescribes concrete implementation steps: identify mandatory vs optional collaborators, prefer constructor injection, supply guarded setters or ObjectProvider for optional beans, and declare deterministic @Bean factories. It also covers resolving bean ambiguity with @Primary, @Qualifier, profiles, and conditional annotations, and validates wiring with focused unit and slice tests.

When to use it

  • Creating new @Service, @Component, or @Repository classes to ensure explicit wiring
  • Replacing legacy field injection and modernizing modules for better testability
  • Configuring optional or pluggable collaborators behind feature flags or multi-tenant adapters
  • Auditing bean definitions before adding integration tests or upgrading Spring Boot
  • Resolving bean ambiguity when multiple implementations exist

Best practices

  • Prefer constructor injection for mandatory dependencies and mark injected fields final
  • Handle optional collaborators via guarded setters (@Autowired(required = false)), ObjectProvider, or no-op defaults
  • Use @Primary, @Qualifier, profiles, and @ConditionalOn... annotations to make bean selection intentional
  • Write unit tests that instantiate classes directly with mocks before running Spring slice or integration tests
  • Avoid field injection, service locators, and excessive @Lazy; extract services if constructors grow beyond a few collaborators

Example use cases

  • Implement UserService with @RequiredArgsConstructor and inject repositories and notification services via constructor
  • Add a CacheService optional collaborator using a guarded setter and a CacheService.noOp() fallback
  • Register NotificationService conditionally using @ConditionalOnProperty and provide a @ConditionalOnMissingBean noop implementation
  • Migrate a legacy module by replacing @Autowired fields with constructor parameters and adding focused unit tests to validate wiring
  • Resolve ambiguous implementations using @Qualifier and document qualifier names in shared constants

FAQ

How should I test constructor-injected services?

Instantiate the service directly with mock dependencies (Mockito or similar) to validate behavior without starting the Spring context; add slice tests only after constructor contracts are verified.

What's the recommended pattern for optional beans?

Prefer ObjectProvider<T> or @Autowired(required = false) setter injection with a documented no-op/default implementation so behavior is deterministic when the bean is absent.