Constructor Injection

Receive dependencies through the constructor into final fields, so beans are complete, immutable and easy to test without Spring.

Syntaxprivate final Dep dep; Service(Dep dep) { this.dep = dep; } // single constructor: no @Autowired needed

Spring can inject dependencies three ways: through a constructor, through setter methods, or straight into fields marked @Autowired. Use the constructor. When a class has exactly one constructor, Spring uses it automatically and no annotation is needed.

@Service
class CheckoutService {
    private final OrderRepository orders;
    private final PaymentClient payments;

    CheckoutService(OrderRepository orders, PaymentClient payments) {
        this.orders = orders;
        this.payments = payments;
    }
}

Why constructors win

  • The object is never half-built. Every dependency arrives before any method can run, and final fields cannot be swapped later.
  • Tests need no container. A unit test calls the constructor with fakes or mocks. With field injection you need reflection or a Spring context just to fill a private field.
  • Dependencies are visible. The constructor is the list of what the class needs. When it grows to seven or eight parameters, that is a design warning you would not notice with seven @Autowired fields.
  • Cycles are caught. If A needs B and B needs A through constructors, Spring cannot build either and fails at startup with a description of the cycle.

Circular dependencies

Spring Boot refuses circular references by default. Setting spring.main.allow-circular-references=true lets field and setter injection paper over a cycle, but the design problem remains. The usual fixes are to move the shared logic into a third bean that both depend on, or to have one side publish an application event instead of calling the other directly.

Optional and multiple dependencies

If a dependency may not exist, inject ObjectProvider<T> and call getIfAvailable(); it also defers lookup until you need the bean. To receive every bean of a type, inject List<T>; to receive them keyed by bean name, inject Map<String, T>.

A class with more than one constructor needs @Autowired on the one Spring should use; otherwise Spring looks for a no-argument constructor and fails if there is none.

Common mistakes

Calling methods on dependencies inside the constructor works, because they are already injected, but do not start threads or make network calls there: the rest of the context is not ready yet. Do not inject the ApplicationContext just to look beans up; inject the beans themselves. And do not put @Autowired on a field of a class you also create with new; the field stays null.

Example

Example · java

import java.math.BigDecimal;
import java.time.Clock;
import java.time.Instant;

import org.springframework.stereotype.Service;

record Payment(String orderId, BigDecimal amount, Instant at) {}

interface PaymentClient {
    String charge(String orderId, BigDecimal amount);
}

interface PaymentLog {
    void append(Payment payment);
}

@Service
public class CheckoutService {
    private final PaymentClient payments;
    private final PaymentLog log;
    private final Clock clock;

    // One constructor: Spring calls it and passes in the three beans.
    public CheckoutService(PaymentClient payments, PaymentLog log, Clock clock) {
        this.payments = payments;
        this.log = log;
        this.clock = clock;
    }

    public String pay(String orderId, BigDecimal amount) {
        if (amount.signum() <= 0) {
            throw new IllegalArgumentException("amount must be positive");
        }
        String reference = payments.charge(orderId, amount);
        log.append(new Payment(orderId, amount, Instant.now(clock)));
        return reference;
    }
}

When to use it

  • Unit test a checkout service with a fake payment client and no Spring context.
  • Break a startup failure caused by two services depending on each other through their constructors.
  • Run a fraud check only in environments where a fraud-check bean is configured.

More examples

Testing a service without Spring

Because the dependency is a constructor parameter, the test passes a lambda and checks the result in plain JUnit. No context starts, so tests like this run in milliseconds. That speed is the main practical reason to prefer constructors.

Example · java
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Test;

class ReminderServiceTest {

    @Test
    void sendsOneMessagePerOverdueInvoice() {
        List<String> sent = new ArrayList<>();
        ReminderService service = new ReminderService(sent::add);

        int count = service.remind(List.of("inv-1", "inv-2"));

        assertThat(count).isEqualTo(2);
        assertThat(sent).containsExactly("Invoice inv-1 is overdue", "Invoice inv-2 is overdue");
    }

    @Test
    void rejectsNullList() {
        ReminderService service = new ReminderService(message -> { });
        assertThatThrownBy(() -> service.remind(null)).isInstanceOf(NullPointerException.class);
    }
}

interface Messenger {
    void send(String message);
}

class ReminderService {
    private final Messenger messenger;

    ReminderService(Messenger messenger) {
        this.messenger = messenger;
    }

    int remind(List<String> invoiceIds) {
        invoiceIds.forEach(id -> messenger.send("Invoice " + id + " is overdue"));
        return invoiceIds.size();
    }
}

An optional collaborator with ObjectProvider

Injecting FraudCheck directly would fail startup wherever no implementation exists. ObjectProvider keeps the constructor style and makes the absence explicit in code. Use it for genuinely optional features, not to dodge a dependency that should always be there.

Example · java
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

interface FraudCheck {
    boolean looksFraudulent(String customerId, long amountPence);
}

@Service
public class OrderApproval {
    private final ObjectProvider<FraudCheck> fraudCheck;

    public OrderApproval(ObjectProvider<FraudCheck> fraudCheck) {
        this.fraudCheck = fraudCheck;
    }

    public boolean approve(String customerId, long amountPence) {
        FraudCheck check = fraudCheck.getIfAvailable();
        // No FraudCheck bean in this environment: approve on amount alone.
        if (check == null) {
            return amountPence < 500_00;
        }
        return !check.looksFraudulent(customerId, amountPence);
    }
}

Choosing between two constructors

With two constructors Spring cannot guess, so mark the one it should call. Without @Autowired here, Spring would look for a no-argument constructor, not find one and fail with a "No default constructor found" error. The constructor taking a Clock assumes a Clock bean, which you would declare in a @Configuration class.

Example · java
import java.time.Clock;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class TokenExpiry {
    private final Clock clock;
    private final long ttlSeconds;

    // Spring uses this one because it carries @Autowired.
    @Autowired
    public TokenExpiry(Clock clock) {
        this(clock, 900);
    }

    // Kept for tests and for callers who want a different lifetime.
    TokenExpiry(Clock clock, long ttlSeconds) {
        this.clock = clock;
        this.ttlSeconds = ttlSeconds;
    }

    public long expiresAt() {
        return clock.instant().getEpochSecond() + ttlSeconds;
    }
}

Discussion

  • Be the first to comment on this lesson.