ArchitectMind
  1. Home
  2. Tutorials
  3. Book 3 · Jakarta EE & Enterprise Java
  4. Volume II
  5. Dependency Injection
Book 3 · Volume II — CDI, Dependency Injection & Interceptors · Topic 1.2

Dependency Injection

Constructor, Field, Method, Resolution

7 min readStage 1 · Foundations6 interview questions
On this page
  1. 1. · Concept & Definition
  2. 2. · Why It Matters
  3. 3. · How It Works
  4. 4. · Java / Jakarta EE Code Examples
  5. 5. · Configuration, Defaults & Diagnostics
  6. 6. · Production Usage Patterns
  7. 7. · Common Pitfalls & Misconceptions
  8. 8. · Best Practices
  9. 9. · Architect's Perspective & Trade-offs
  10. 10. · Interview Q&A

1· Concept & Definition

Dependency injection (DI) is a technique in which an object receives its dependencies from an external party instead of creating or looking them up itself. The dependencies can be passed through the constructor, set into fields, or supplied through methods. A DI container performs resolution: it determines which implementation satisfies each dependency and injects it.

2· Why It Matters

  • It is the mechanism that turns loose coupling into working code.
  • Injection style affects immutability, testability and clarity of dependencies.
  • Resolution rules determine whether an application starts at all.

Dependency injection is where the abstract goal of depending on interfaces becomes concrete: something has to create the implementation and hand it over, and in Jakarta EE that is the CDI container. The injection style then decides how honest a class is about its needs. A constructor with four parameters shows a reviewer the collaborators at a glance; four private annotated fields hide them. Resolution matters because CDI checks every injection point when the application deploys, so a missing or ambiguous bean stops start-up instead of failing on the first request that happens to need it. That fail-fast behaviour is one of the main advantages of container-based injection over manual lookups.

3· How It Works

Constructor, field and method injection compared
Figure 1.2.1 — Constructor, field and method injection compared

Constructor

Dependencies are parameters of the constructor, annotated with @Inject in Jakarta EE (only one constructor may carry it). Fields can be final, the object is fully initialised after construction, and dependencies are visible in the signature. It is the recommended default.

Field

@Inject on a (non-final) field: the container sets it via reflection after construction. It is concise but hides dependencies, prevents immutability and makes unit tests depend on reflection or a container. It remains common in Jakarta EE code and is acceptable in framework-managed classes such as Faces backing beans.

Method

@Inject on a method (an initializer method in CDI terms, often a setter): the container calls it after field injection with resolved arguments. It suits optional dependencies, cyclic breaking in legacy code, or cases where several dependencies must be processed together.

CriterionConstructorFieldMethod
Immutability (final)YesNoNo
Dependencies visible in APIYesNoPartly
Unit testing without containerEasyNeeds reflectionPossible
Order of injection (CDI)1st2nd3rd (after fields)
Proxyability requirementStill needs non-private no-arg ctor for normal-scoped beans——

Resolution

Resolution selects the bean for each injection point by comparing the required type and qualifiers with the available beans, then applying alternative selection. In Jakarta CDI this happens at deployment: unsatisfied or ambiguous dependencies are detected before the application serves requests (details in Chapter 5).

Resolution steps and the two failure outcomes
Figure 1.2.2 — Resolution steps and the two failure outcomes

Resolution rests on bean discovery. With the default annotated discovery mode the container scans each archive for classes carrying a bean-defining annotation, such as @ApplicationScoped, @RequestScoped or @Dependent; with a beans.xml in all mode it treats every eligible class as a bean. For each bean it records its bean types, meaning the classes and interfaces it can be injected as, and its qualifiers, adding @Default when none is declared. For each injection point it then keeps only the enabled beans whose type is assignable and whose qualifiers match, and if several remain, the selected alternative with the highest priority wins. Exactly one bean must be left.

Implementations of CDI Lite, such as Quarkus ArC, perform the same analysis at build time, so wiring errors appear before the application is even packaged, and no reflection or classpath scanning is needed at start-up.

4· Java / Jakarta EE Code Examples

Constructor injection in CDI (with the no-arg constructor required for client proxies)
@ApplicationScoped
public class OrderService {
    private final OrderRepository repo;
    private final PaymentGateway payments;
 
    // constructor injection
    @Inject
    public OrderService(OrderRepository repo, PaymentGateway payments) {
        this.repo = repo;
        this.payments = payments;
    }
 
    // for proxying (normal scope)
    protected OrderService() { this(null, null); }
}
Field and method injection
@Named @ViewScoped
public class CheckoutView implements Serializable {
    @Inject CartService cart;                                  // field injection
 
    private AuditLog audit;
    // method (initializer) injection
    @Inject void setAudit(AuditLog audit) { this.audit = audit; }
}
Resolving zero or many beans with Instance<T>
// Optional or multiple dependencies: programmatic lookup
@Inject Instance<DiscountRule> rules;          // zero..n implementations
public BigDecimal discount(Order o) {
    return rules.stream().map(r -> r.apply(o)).reduce(BigDecimal.ZERO, BigDecimal::add);
}

When two beans implement the same type, a qualifier states which one an injection point wants. A bean that declares a qualifier no longer has @Default, so unqualified injection points still resolve to the standard implementation.

Qualifiers to disambiguate two implementations
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, PARAMETER, METHOD, TYPE})
public @interface Express {}

@ApplicationScoped
public class StandardShipping implements ShippingQuote { /* ... */ }

@Express @ApplicationScoped
public class ExpressShipping implements ShippingQuote { /* ... */ }

@ApplicationScoped
public class QuoteService {
    private final ShippingQuote standard;
    private final ShippingQuote express;

    @Inject
    public QuoteService(ShippingQuote standard,          // @Default bean
                        @Express ShippingQuote express) {
        this.standard = standard;
        this.express = express;
    }

    protected QuoteService() { this(null, null); }       // client proxy
}

Constructor injection also keeps unit tests free of the container: the test simply calls the constructor with in-memory fakes or mocks.

Unit test without a container
// No container, no reflection: the constructor is the wiring
@Test
void placesOrderAndChargesCard() {
    var repo = new InMemoryOrderRepository();
    var payments = mock(PaymentGateway.class);
    var service = new OrderService(repo, payments);

    service.place(new NewOrder("A-1", new BigDecimal("20.00")));

    verify(payments).charge(new BigDecimal("20.00"));
    assertEquals(1, repo.count());
}

5· Configuration, Defaults & Diagnostics

ErrorCauseFix
UnsatisfiedResolutionExceptionNo bean matches type and qualifiersAdd implementation, fix discovery mode or qualifier
AmbiguousResolutionExceptionSeveral beans matchQualifier, alternative with priority, or @Typed
UnproxyableResolutionExceptionNormal-scoped bean cannot be proxiedRemove final, add non-private no-arg constructor
Circular dependency errorCycle among @Dependent beans or constructorsRedesign; break cycle

6· Production Usage Patterns

  • Constructor injection for services; field injection tolerated in UI/backing beans.
  • Instance<T> for plugin-style collections of strategies.
  • Startup failures on resolution errors treated as build failures (container tests in CI).

In larger code bases teams usually add two safeguards. First, a container start-up test in CI, using Weld JUnit, Arquillian or the framework's own test support, boots the real bean graph so that resolution errors fail the build rather than a deployment. Second, an ArchUnit or static-analysis rule flags field injection in service packages and constructors that grow beyond five or six parameters, which usually means a class has too many responsibilities. Plugin-style designs, such as a set of pricing or validation rules, use Instance<T> so that new behaviour is added by deploying a new bean rather than by editing a switch statement.

7· Common Pitfalls & Misconceptions

  • Mixing styles arbitrarily in one code base.
  • Using field injection then calling new in tests — NullPointerExceptions.
  • Constructors with many parameters hidden by switching to field injection.
  • Using Instance.get() for @Dependent beans without destroy() — leaks.

8· Best Practices

  • Default to constructor injection; keep constructors small.
  • Inject interfaces, not implementations.
  • Use qualifiers rather than names for disambiguation.
  • Verify wiring with container-based tests.

9· Architect's Perspective & Trade-offs

ApproachWhen it fits
Constructor injectionMandatory dependencies, immutable services
Field injectionFramework-managed UI beans, legacy code
Method injectionOptional dependencies, setup needing several dependencies
Programmatic lookup (Instance)Dynamic or optional selection at runtime

10· Interview Q&A

Q1BasicConcept

What is dependency injection?

Dependency injection is a technique in which an object receives the collaborators it needs from outside instead of creating or looking them up itself. An OrderService that needs an OrderRepository declares it as a constructor parameter, and the container, or a test, passes in an implementation. The object no longer decides which implementation it uses, so it depends only on an interface and can be wired differently for production, tests or another environment. In Jakarta EE the CDI container performs the injection, resolving each injection point by type and qualifiers at deployment. Injection is the mechanism; inversion of control and loose coupling are the design goals it serves.

Q2IntermediateTrade-off

Why is constructor injection usually preferred?

Because it makes a class honest and safe. Dependencies appear in the constructor signature, so anyone reading or reviewing the class sees what it needs, and a growing parameter list is an early warning of too many responsibilities. Fields can be final, so the object is fully initialised once constructed and can never be observed half-wired. Unit tests simply call the constructor with fakes or mocks, with no container or reflection. Field injection is shorter, but it hides dependencies and lets new OrderService() create an object that later fails with a NullPointerException. In CDI, normal-scoped beans still need a non-private no-arg constructor for their client proxy.

Q3IntermediateInternals

What happens if CDI finds no bean for an injection point?

Deployment fails. CDI validates every injection point while the application starts, and if no enabled bean matches the required type and qualifiers it reports an unsatisfied dependency, typically as an UnsatisfiedResolutionException inside a DeploymentException that names the injection point. The application never begins serving requests. Common causes are a missing implementation, a class without a bean-defining annotation under annotated discovery, a qualifier mismatch, or an alternative that has not been enabled. This fail-fast behaviour is deliberate: wiring errors surface in the build or the first deployment rather than on a rarely used code path in production. Genuinely optional dependencies should be expressed with Instance<T> instead.

Q4Senior EngineerInternals

Why do normal-scoped CDI beans with constructor injection still need a no-arg constructor?

Normal-scoped beans, such as @ApplicationScoped or @RequestScoped ones, are never injected directly. Clients receive a client proxy, a container-generated subclass that finds the correct contextual instance on every call, for example the bean belonging to the current request. To instantiate that subclass the container must call a superclass constructor without any dependencies to pass, so the bean class needs a non-private no-arg constructor, and the class and its public methods must not be final. The real instance is still created through the @Inject constructor. A protected constructor that delegates with nulls is the usual pattern. Quarkus ArC can generate it automatically, but portable code should declare it.

Q5Technical ArchitectTrade-off

When would you use Instance<T> instead of direct injection?

I use Instance<T> when direct injection cannot express the dependency. That covers three cases. The dependency is optional and the application must start without it, checked with isResolvable(). There are several implementations that should all be applied, such as a set of discount rules iterated with stream(). Or the implementation is chosen at run time, using select() with a qualifier derived from a tenant or request attribute. For everything else, direct constructor injection is clearer and is validated at deployment. One caution: @Dependent beans obtained through Instance.get() live until they are destroyed, so call destroy() or use getHandle() to avoid leaks in long-lived beans.

Q6Solution ArchitectDesign

How would you set DI conventions for multiple teams?

I would write a short, enforced convention rather than a long guideline. Services use constructor injection with final fields; field injection is allowed only in UI backing beans and test classes. Injection points at module or architectural boundaries use interfaces, and disambiguation uses typed qualifiers, never string names with @Named. Optional or plugin-style dependencies use Instance<T>. Every service has a container start-up test in CI, so resolution errors fail the build. I would enforce the rules with ArchUnit or static analysis, publish a reference project, and record deliberate exceptions in an ADR. The aim is that any engineer can open any team's code and understand its wiring immediately.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.