The Dependency Injection Dilemma: Why I’m Finally Ghosting @Autowired on Fields

Published: (December 19, 2025 at 06:41 AM EST)
6 min read
Source: Dev.to

Source: Dev.to

The Aesthetic Trap: Why We Fell in Love with Field Injection

Before we tear it down, we have to acknowledge why we used it in the first place. Take a look at the following code snippet:

@Service
public class OrderService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PaymentService paymentService;

    @Autowired
    private InventoryClient inventoryClient;

    // Business Logic...
}

It’s undeniably sleek. There are no bulky constructors taking up half the screen. It feels like the “Spring Way.” For years, this was the standard in tutorials and Stack Overflow answers. It allowed us to add a dependency with a single line of code.

However, this “cleanliness” is a visual illusion. It’s like hiding a messy room by shoving everything into a closet. The room looks clean, but you’ve actually made the system harder to manage.


The Case for Immutability

As engineers, we should strive for immutability. An object that cannot change after it is created is inherently safer, more predictable, and easier to reason about in a multi‑threaded environment.

When you use field injection, you cannot declare your dependencies as final. Spring needs to be able to reach into your object after the constructor has run to inject those fields via reflection. This means your dependencies are technically mutable.

By switching to constructor injection, you regain the ability to use the final keyword:

@Service
public class OrderService {
    private final UserRepository userRepository;
    private final PaymentService paymentService;
    private final InventoryClient inventoryClient;

    public OrderService(
            UserRepository userRepository,
            PaymentService paymentService,
            InventoryClient inventoryClient) {
        this.userRepository = userRepository;
        this.paymentService = paymentService;
        this.inventoryClient = inventoryClient;
    }
}

Now, your class is “Born Ready.” Once the OrderService exists, you have a 100 % guarantee that the userRepository is there and will never be changed or set to null by some rogue process. This is the foundation of thread safety and defensive programming.


The Case for Unit Testing

If you want to know how good your architecture is, look at your unit tests. If your test setup looks like a ritualistic sacrifice, your architecture is broken.

Field injection makes unit testing unnecessarily difficult. Because the fields are private and Spring is doing the heavy lifting behind the scenes, you can’t simply instantiate the class in a test. You have two bad options:

  1. Use Spring in your tests – e.g., @SpringBootTest or @MockBean.
    Your “unit” test is now starting a miniaturized Spring context. It’s slow, it’s heavy, and it’s no longer a unit test (it’s an integration test).

  2. Use reflection – e.g., ReflectionTestUtils to manually “shove” a mock into a private field.
    This is brittle. If you rename the field, your test breaks, but the compiler won’t tell you why.

With constructor injection, testing is a breeze. Since the constructor is the only way to create the object, you just pass the mocks in directly:

@Test
void shouldProcessOrder() {
    UserRepository mockUserRepo = mock(UserRepository.class);
    PaymentService mockPaymentService = mock(PaymentService.class);
    InventoryClient mockInventoryClient = mock(InventoryClient.class);

    // Standard Java. No magic. No Spring. Fast.
    OrderService service = new OrderService(mockUserRepo, mockPaymentService, mockInventoryClient);

    service.process(new Order());
}

Failing Fast: The 2:00 AM Production Bug

We’ve all been there. You deploy a change, the app starts up fine, and everything looks green. Then, at 2:00 AM, a specific user hits an edge‑case API endpoint, and the logs explode with a NullPointerException.

Why? Because with field injection, Spring allows the application to start even if a dependency is missing or circular. The field just remains null. You don’t find out until the code actually tries to use that field.

Constructor injection is your early warning system. Because Spring must call the constructor to create the bean, it must satisfy all dependencies immediately. If a bean is missing, the ApplicationContext will fail to load, and the app won’t even start on your machine, let alone in production.

I’d much rather spend five minutes fixing a startup error locally than five hours explaining to a stakeholder why the payment service crashed in the middle of the night.


The Single Responsibility Principle

The Single Responsibility Principle (SRP) states that a class should have one, and only one, reason to change.

Field injection makes it too easy to violate this. Because each dependency is just one line of @Autowired, developers tend to sprinkle additional collaborators into a class without thinking about cohesion. Constructor injection forces you to be explicit about what the class truly needs, making it harder to accumulate unrelated responsibilities.


TL;DR

AspectField InjectionConstructor Injection
VisibilityPrivate fields, hidden dependenciesExplicit constructor parameters
ImmutabilityDependencies cannot be finalDependencies can be final
TestabilityRequires Spring context or reflection hacksPlain Java instantiation with mocks
Fail‑fastNulls appear at runtimeMissing beans cause startup failure
SRP enforcementEasy to add hidden collaboratorsExplicit contract, encourages cohesion

Switch to constructor injection today. Your code will be cleaner, safer, more testable, and your team will thank you when the next production bug tries to sneak in at 2 AM.

Constructor Injection vs. Field Injection


The “Constructor of Doom”

When you rely on field injection (@Autowired), it’s easy to miss when a class starts doing too much. I’ve seen services with 15 @Autowired fields that looked “neat” on the screen.

When you switch to constructor injection, a class with 15 dependencies looks like a monster. The constructor becomes massive, hard to read, and ugly.

And that is exactly the point.
That “Constructor of Doom” is a signal – the code is telling you:

“Hey, I’m doing too much. Please refactor me into smaller, more focused services.”

Field injection is like a layer of makeup that hides a skin infection; constructor injection forces you to see the problem and treat it.

Circular Dependencies: The Infinite Loop

Circular dependencies (Service A needs B, and B needs A) usually indicate poor design. Field injection allows them to happen almost unnoticed. Spring will try to resolve them using proxies, often leading to confusing behavior.

Constructor injection doesn’t allow circular dependencies by default. If you try it, Spring throws a BeanCurrentlyInCreationException.

While this may seem like a nuisance, it’s actually a guardrail. It forces you to rethink service boundaries. Typically, a circular dependency means you need a third service (Service C) to hold the shared logic, or you should move to an event‑driven approach.

The Lombok Cheat Code

The most common pushback I hear is:

“But I don’t want to write and maintain constructors for 200 services!”

I agree. I’m a programmer; if I can automate a task, I will. This is where Project Lombok becomes your best friend.

By using the @RequiredArgsConstructor annotation, you get the best of both worlds: declare your fields as private final, and Lombok generates the constructor at compile time.

@Service
@RequiredArgsConstructor
public class OrderService {
    private final UserRepository userRepository;
    private final PaymentService paymentService;
    private final InventoryClient inventoryClient;

    // No manual constructor needed!
}

Professionalism Is in the Details

At the end of the day, using constructor injection is about intentionality. It’s about making a conscious choice to write code that is:

  • Framework‑independent
  • Easy to test
  • Architecturally sound

It moves you away from “Spring Magic” toward “Java Excellence.”

If you’re working on a legacy codebase filled with @Autowired fields, don’t panic. You don’t have to refactor everything tonight. But for every new service you write, try the constructor approach. Notice how your tests become simpler and your classes become smaller.

Your code reflects your craftsmanship. Don’t let a shortcut like field injection undermine it.

What’s your take?

Are you a die‑hard @Autowired fan, or have you embraced the constructor? Let’s discuss in the comments. If you found this helpful, consider sharing it with a junior dev who is still caught in the “Field Injection Trap.”

Back to Blog

Related posts

Read more »