Spring BootSetter Injection
Spring Boot

Setter Injection

Setter injection provides dependencies through setter methods after the bean is constructed. It is the appropriate choice for optional dependencies — ones that have a reasonable default behavior when absent. Understanding when setter injection is correct, and when constructor injection is better, prevents common design mistakes.

What Is Setter Injection?

Setter injection injects dependencies through setter methods annotated with @Autowired. The bean is first created with its default (no-arg) constructor, then Spring calls each annotated setter to inject the corresponding dependency. The object is in a partially initialized state between construction and the last setter call — all dependencies are only available after all setters have been called. Setter injection was the recommended style in early Spring (before Spring 4), primarily because it allowed re-injection after construction — useful for testing. Modern Spring recommends constructor injection for mandatory dependencies and setter injection specifically for optional dependencies. An optional dependency is one where the class has a meaningful default behavior when the dependency is absent — a service that logs to a metrics system when available but works fine without it, or a notification service that uses SMS if an SMS provider is configured but functions with email alone otherwise.

Basic Setter Injection

Setter injection uses @Autowired on setter methods. By default, @Autowired is required — Spring throws if no matching bean exists. Set required=false for optional dependencies.
Java
// Basic setter injection — required dependency (prefer constructor for this):
@Service
public class UserService {

    private UserRepository userRepository;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // Note: field cannot be final with setter injection
    // The bean is created with default constructor, then setUserRepository() is called
}

// Optional setter injection — use required=false:
@Service
public class NotificationService {

    private EmailService emailService;      // primary delivery method
    private SmsService smsService;          // optional — may not be configured
    private PushNotificationService push;   // optional — may not be available

    // Required — must have at least email:
    @Autowired
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }

    // Optional — SMS works only if SmsService bean exists:
    @Autowired(required = false)
    public void setSmsService(SmsService smsService) {
        this.smsService = smsService;
    }

    // Optional — push notifications if available:
    @Autowired(required = false)
    public void setPushNotificationService(PushNotificationService push) {
        this.push = push;
    }

    public void notify(String userId, String message) {
        // Email always sent — required:
        emailService.send(userId, message);

        // SMS only if configured:
        if (smsService != null) {
            smsService.send(userId, message);
        }

        // Push only if configured:
        if (push != null) {
            push.send(userId, message);
        }
    }
}

Optional Dependencies — The Right Use Case

Setter injection with required=false is the clearest way to express that a dependency is optional. The class has documented behavior with and without the dependency, and null checks make the optionality explicit in the code.
Java
// Well-designed optional dependency pattern:
@Service
public class AuditableOrderService {

    // Required — must have these to function:
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;

    // Optional — metrics are nice to have but orders work without them:
    private MetricsCollector metricsCollector;

    // Optional — external audit log, may not be configured in all environments:
    private ExternalAuditService externalAuditService;

    // Constructor for required dependencies:
    public AuditableOrderService(OrderRepository orderRepository,
                                  PaymentService paymentService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
    }

    // Setters for optional dependencies:
    @Autowired(required = false)
    public void setMetricsCollector(MetricsCollector metricsCollector) {
        this.metricsCollector = metricsCollector;
    }

    @Autowired(required = false)
    public void setExternalAuditService(ExternalAuditService externalAuditService) {
        this.externalAuditService = externalAuditService;
    }

    public Order createOrder(CreateOrderRequest request) {
        PaymentResult payment = paymentService.charge(
            request.customerId(), request.totalAmount()
        );
        Order order = orderRepository.save(new Order(request, payment));

        // Optional behavior — runs only when beans are configured:
        if (metricsCollector != null) {
            metricsCollector.increment("orders.created");
            metricsCollector.record("orders.amount", request.totalAmount().doubleValue());
        }

        if (externalAuditService != null) {
            externalAuditService.logOrderCreation(order);
        }

        return order;
    }
}

// In production (application.properties):
// metrics.enabled=true  → MetricsCollector bean created → injected
// audit.external.enabled=false → ExternalAuditService not created → null

// In development:
// Neither optional bean is configured — service works fine without them

Combining Constructor and Setter Injection

The most common and correct pattern is to use constructor injection for mandatory dependencies and setter injection for optional ones. This clearly documents which dependencies are required and which are optional at the API level.
Java
@Service
public class ReportingService {

    // ── REQUIRED — injected via constructor ──────────────────────────
    private final ReportRepository reportRepository;
    private final UserRepository userRepository;
    private final PdfGenerator pdfGenerator;

    public ReportingService(ReportRepository reportRepository,
                             UserRepository userRepository,
                             PdfGenerator pdfGenerator) {
        this.reportRepository = reportRepository;
        this.userRepository = userRepository;
        this.pdfGenerator = pdfGenerator;
        // Service is usable immediately after constructor completes
    }

    // ── OPTIONAL — injected via setter if bean exists ────────────────
    private EmailDeliveryService emailDeliveryService;
    private S3StorageService s3StorageService;
    private ReportCacheService cacheService;

    @Autowired(required = false)
    public void setEmailDeliveryService(EmailDeliveryService emailDeliveryService) {
        this.emailDeliveryService = emailDeliveryService;
    }

    @Autowired(required = false)
    public void setS3StorageService(S3StorageService s3StorageService) {
        this.s3StorageService = s3StorageService;
    }

    @Autowired(required = false)
    public void setCacheService(ReportCacheService cacheService) {
        this.cacheService = cacheService;
    }

    // ── Business logic ───────────────────────────────────────────────
    public Report generateReport(Long userId, ReportType type) {
        // Check cache first if available:
        if (cacheService != null) {
            Optional<Report> cached = cacheService.get(userId, type);
            if (cached.isPresent()) return cached.get();
        }

        User user = userRepository.findById(userId).orElseThrow();
        byte[] pdf = pdfGenerator.generate(user, type);
        Report report = reportRepository.save(new Report(userId, type, pdf));

        // Store in S3 if configured:
        if (s3StorageService != null) {
            String url = s3StorageService.upload("reports/" + report.getId() + ".pdf", pdf);
            report.setStorageUrl(url);
            reportRepository.save(report);
        }

        // Email if configured:
        if (emailDeliveryService != null) {
            emailDeliveryService.deliver(user.getEmail(), report);
        }

        // Cache for next request:
        if (cacheService != null) {
            cacheService.put(userId, type, report);
        }

        return report;
    }
}

Setter Injection and Circular Dependencies

Spring uses setter injection internally to resolve circular dependencies between singleton beans. When bean A requires bean B and bean B requires bean A, Spring creates both beans with default constructors first, then uses setter injection to wire them together — breaking the circular creation deadlock.
Java
// Circular dependency — Spring resolves with setter injection:
@Service
public class ServiceA {

    private ServiceB serviceB;

    // Setter injection allows circular dependency resolution:
    @Autowired
    public void setServiceB(ServiceB serviceB) {
        this.serviceB = serviceB;
    }

    public void doWork() {
        serviceB.assist();
    }
}

@Service
public class ServiceB {

    private ServiceA serviceA;

    @Autowired
    public void setServiceA(ServiceA serviceA) {
        this.serviceA = serviceA;
    }

    public void assist() {
        System.out.println("ServiceB assisting ServiceA");
    }
}

// How Spring resolves it:
// 1. Creates ServiceA with default constructor (serviceB is null)
// 2. Creates ServiceB with default constructor (serviceA is null)
// 3. Calls serviceA.setServiceB(serviceB) — wires B into A
// 4. Calls serviceB.setServiceA(serviceA) — wires A into B
// 5. Both beans are now fully wired

// With constructor injection — Spring CANNOT resolve circular dependencies:
// @Service
// public class ServiceA {
//     public ServiceA(ServiceB serviceB) { ... }  // needs B to exist
// }
// @Service
// public class ServiceB {
//     public ServiceB(ServiceA serviceA) { ... }  // needs A to exist
// }
// → BeanCurrentlyInCreationException at startup — detected and reported

// IMPORTANT: A circular dependency is a design problem, not a feature:
// Using setter injection to work around it hides the real issue
// The correct solution is to redesign — extract shared logic into a third service,
// use events, or restructure the responsibility boundary
// Spring Boot 2.6+ throws on circular dependencies by default:
// spring.main.allow-circular-references=true  (enables the old workaround behavior)

Setter Injection in Tests

Setter injection is particularly useful in test scenarios — you can inject test doubles after construction, reconfigure dependencies between tests, or override specific dependencies without reconstructing the object.
Java
// Service with setter injection for optional dependencies:
@Service
public class SearchService {

    private final ProductRepository productRepository;   // required
    private SearchIndexClient searchClient;               // optional

    public SearchService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Autowired(required = false)
    public void setSearchClient(SearchIndexClient searchClient) {
        this.searchClient = searchClient;
    }

    public List<Product> search(String query) {
        if (searchClient != null) {
            return searchClient.search(query);   // use Elasticsearch if available
        }
        return productRepository.findByNameContaining(query);  // fallback to DB
    }
}

// Test — verify behavior without search client (fallback path):
@ExtendWith(MockitoExtension.class)
class SearchServiceTest {

    @Mock
    private ProductRepository productRepository;

    private SearchService searchService;

    @BeforeEach
    void setUp() {
        // No SearchIndexClient — tests the fallback DB path:
        searchService = new SearchService(productRepository);
        // searchClient remains null — setter not called
    }

    @Test
    void search_withoutSearchClient_queriesDatabase() {
        when(productRepository.findByNameContaining("laptop"))
            .thenReturn(List.of(new Product(1L, "Gaming Laptop")));

        List<Product> results = searchService.search("laptop");

        assertThat(results).hasSize(1);
        verify(productRepository).findByNameContaining("laptop");
    }

    @Test
    void search_withSearchClient_usesIndex() {
        SearchIndexClient mockClient = mock(SearchIndexClient.class);
        searchService.setSearchClient(mockClient);  // inject via setter in test
        when(mockClient.search("laptop"))
            .thenReturn(List.of(new Product(2L, "Laptop Pro")));

        List<Product> results = searchService.search("laptop");

        assertThat(results).hasSize(1);
        assertThat(results.get(0).getName()).isEqualTo("Laptop Pro");
        verifyNoInteractions(productRepository);  // DB not queried when client present
    }
}

// ReflectionTestUtils — inject via setter without @Autowired (legacy testing):
@SpringBootTest
class LegacyServiceTest {

    @Autowired
    private LegacyService legacyService;

    @Test
    void testWithMockedDependency() {
        EmailService mockEmail = mock(EmailService.class);
        ReflectionTestUtils.setField(legacyService, "emailService", mockEmail);
        // Now legacyService uses the mock for this test
    }
}

Setter Injection vs Constructor Injection — Decision Guide

The choice between constructor and setter injection comes down to whether the dependency is mandatory or optional. This guide covers every scenario.
Java
// ── USE CONSTRUCTOR INJECTION when: ──────────────────────────────

// 1. The dependency is REQUIRED — class cannot function without it:
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;    // cannot save orders without this
    private final PaymentService paymentService;      // cannot charge without this
}

// 2. You want to validate dependencies at startup:
public UserService(UserRepository userRepository) {
    this.userRepository = Objects.requireNonNull(userRepository,
        "UserRepository must not be null");
}

// 3. The class should be immutable (fields final):
@Service
@RequiredArgsConstructor
public class CryptoService {
    private final String secretKey;     // never changes after construction
    private final int iterations;       // never changes after construction
}

// 4. You want clear testability — pass dependencies in constructor:
UserService service = new UserService(mockRepo, mockEncoder, mockEmail);


// ── USE SETTER INJECTION when: ────────────────────────────────────

// 1. The dependency is OPTIONAL — class works without it:
@Autowired(required = false)
public void setMetricsCollector(MetricsCollector collector) {
    this.metricsCollector = collector;
}

// 2. You need to reconfigure a dependency after construction:
// (rare in production, useful in tests)
service.setEmailService(testEmailService);

// 3. Breaking a circular dependency (last resort — prefer redesign):
@Autowired
public void setServiceB(ServiceB serviceB) { this.serviceB = serviceB; }

// 4. Framework integration requiring no-arg constructor:
// Some older frameworks (JPA, Jackson, JAXB) require no-arg constructors
// Combined with setter injection to wire dependencies post-construction


// ── NEVER USE FIELD INJECTION in production code: ─────────────────
// @Autowired
// private UserRepository userRepository;   // hidden dependency, not testable
// Cannot be final, cannot validate, requires reflection to test


// ── QUICK DECISION TABLE: ─────────────────────────────────────────
// Dependency type              Injection style
// ─────────────────────────────────────────────────────────────────
// Mandatory, always needed     Constructor injection
// Optional, may be absent      Setter injection (required=false)
// Configuration value          Constructor with @Value
// Group of config properties   Constructor with @ConfigurationProperties
// Optional config value        @Value with default: @Value("${x:default}")
// Circular (last resort)       Setter injection (prefer redesigning)