Why I Stopped Using Spring’s @Autowired and Adopted Explicit Dependency Management

The article explains how automatic @Autowired injection can tightly couple domain models to the Spring container, hide dependencies, hinder testing, create circular‑dependency risks, and violate clean architecture, then demonstrates an explicit‑dependency approach with a custom SpringContext utility, compares both methods, and offers practical guidelines for layered design and testing.

Architect's Journey
Architect's Journey
Architect's Journey
Why I Stopped Using Spring’s @Autowired and Adopted Explicit Dependency Management

Spring's @Autowired annotation enables automatic bean injection but can introduce hidden dependencies, tight coupling between domain models and the framework, testing difficulties, circular‑dependency risks, and violations of clean‑architecture principles.

Four sins of automatic injection

Dependency black‑boxing : Dependencies become implicit and invisible, contradicting the “explicit over implicit” design rule.

Unit‑test dilemma : Testing domain objects requires starting the full Spring container, turning unit tests into slower integration tests.

Circular‑dependency breeding ground : Mutual @Autowired injections trigger Spring's three‑level cache, masking circular dependencies and leading to tangled architecture.

Breaking rich domain models : In DDD, domain objects should remain pure POJOs; auto‑injection forces them to be aware of the Spring container.

Explicit dependency management

A SpringContext utility implements ApplicationContextAware and provides static methods to retrieve beans explicitly.

@Primary
public class SpringContext implements ApplicationContextAware, PriorityOrdered, ApplicationRunner {
    private static ApplicationContext applicationContext;
    private static final CountDownLatch INITIALIZATION_LATCH = new CountDownLatch(1);

    @Override
    public void run(ApplicationArguments args) throws Exception {
        INITIALIZATION_LATCH.countDown();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringContext.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    public static <T> T getBeanSync(Class<T> clazz) {
        try {
            if (!INITIALIZATION_LATCH.await(1, TimeUnit.MINUTES)) {
                throw new IllegalStateException("Application initialization timeout");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Bean retrieval interrupted", e);
        }
        return applicationContext.getBean(clazz);
    }

    @Override
    public int getOrder() {
        return PriorityOrdered.HIGHEST_PRECEDENCE;
    }
}

Domain code can now obtain required services explicitly, keeping the model a pure POJO:

public class Order {
    private Long orderId;
    private BigDecimal amount;
    private OrderStatus status;

    public void pay() {
        PaymentService paymentService = SpringContext.getBean(PaymentService.class);
        PaymentResult result = paymentService.execute(this);
        if (result.isSuccess()) {
            this.status = OrderStatus.PAID;
            DomainEventPublisher.publish(new OrderPaidEvent(this));
        }
    }
}

Comparison of the two approaches

Domain model purity : Auto‑injection relies on the container; explicit retrieval yields a pure POJO.

Testability : Auto‑injection needs a Spring environment; explicit retrieval works with ordinary mocks.

Dependency visibility : Implicit with auto‑injection, explicit with the utility.

Circular‑dependency risk : High with auto‑injection, none with explicit retrieval.

Code readability : Requires IDE assistance for auto‑injection; explicit calls are immediately clear.

Best‑practice guide

Layered management strategy

Infrastructure layer: Allow @Autowired for technical components such as JPA repositories.

Domain layer: Prohibit container dependencies; use SpringContext to fetch required services.

Application layer: Prefer constructor injection with limited scope.

Async environment adaptation

public class AsyncBeanAccessor {
    public static <T> Mono<T> getBeanReactive(Class<T> beanClass) {
        return Mono.fromCallable(() -> SpringContext.getBean(beanClass))
                   .subscribeOn(Schedulers.boundedElastic());
    }
}

Testing support

@Test
public void testOrderPayment() {
    SpringContextMock.registerMock(PaymentService.class, mockService);
    Order order = new Order(/*...*/);
    order.completePayment();
    assertThat(order.getStatus()).isEqualTo(PAID);
}

Architectural considerations

Control of technical boundaries : Frameworks should stay in the infrastructure layer and not leak into core business logic.

Complexity transfer : Explicit dependencies move complexity from runtime to compile‑time, aligning with the “Fail Fast” principle.

Evolutionary design : Keeping domain models framework‑agnostic eases future migrations.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaTestingspringDDDDependency Injection@AutowiredExplicit Dependencies
Architect's Journey
Written by

Architect's Journey

E‑commerce, SaaS, AI architect; DDD enthusiast; SKILL enthusiast

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.