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

Inversion of Control

IoC, Container responsibility, Lifecycle

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. Deep Dive — Where Control Actually Changes Hands
  5. 4. · Java / Jakarta EE Code Examples
  6. 5. · Configuration, Defaults & Diagnostics
  7. 6. · Production Usage Patterns
  8. 7. · Common Pitfalls & Misconceptions
  9. 8. · Best Practices
  10. 9. · Architect's Perspective & Trade-offs
  11. 10. · Interview Q&A

1· Concept & Definition

Inversion of Control (IoC) is the principle that a framework, not application code, controls the flow of execution: the framework creates objects, calls their methods and manages their lifetime, while the application supplies components that plug into it. Dependency injection is one form of IoC (for obtaining dependencies); others include event callbacks, template methods and servlet containers calling service().

Traditional control flow versus inverted control
Figure 1.3.1 — Traditional control flow versus inverted control

2· Why It Matters

  • Explains why enterprise code consists of annotated components rather than a main() that wires everything.
  • Defines the division of responsibility between developer and container.
  • Lifecycle management by the container is the basis for scopes, pooling and resource cleanup.

IoC also changes what a developer is responsible for. In a plain Java program the author decides when objects are built, in what order and when they are discarded; in a Jakarta EE application those decisions belong to the container, and the developer's job is to describe components accurately enough for it to make them. That shift is why annotations such as @ApplicationScoped, @Inject and @Observes carry so much meaning, and why defects that look like coding mistakes, a null field, a transaction that never started or an observer that never fires, are so often caused by an object the container did not create. Knowing where control sits is the quickest way to explain them.

3· How It Works

IoC

IoC formWhat is invertedJakarta EE example
Dependency injectionObtaining collaborators@Inject
Callbacks / eventsWho calls whom on events@Observes, servlet listeners
Lifecycle managementCreation and destruction@PostConstruct, @PreDestroy
Framework dispatchRequest handling flowFacesServlet, JAX-RS resource methods
Template methodAlgorithm skeletonHttpServlet.service() → doGet()
AlternativeHow dependencies are obtainedDrawback
Direct constructionnew Impl()Tight coupling
Service locatorLocator.lookup(Service.class) / JNDI lookupHidden dependencies, runtime failures
Dependency injectionSupplied by containerRequires container or manual composition root

Container responsibility

Responsibilities an IoC container takes over
Figure 1.3.2 — Responsibilities an IoC container takes over
ResponsibilityIn Jakarta CDI
DiscoveryScans archives according to beans.xml discovery mode
ValidationChecks all injection points at deployment
Instantiation & injectionCreates instances lazily in their context, injects dependencies
LifecycleCalls callbacks, destroys instances when contexts end
EnrichmentClient proxies, interceptors, decorators
EventsDelivers events to observer methods

Lifecycle

Because the container creates objects, it also controls their lifetime: which instance is shared with whom (scope), when initialisation happens (@PostConstruct) and when resources are released (@PreDestroy). Application code declares the desired lifecycle with annotations instead of managing it manually. Chapter 3 covers the CDI lifecycle in depth.

Deep Dive — Where Control Actually Changes Hands

A practical way to reason about IoC is to ask, for every object, who called new. If the container created it, the object is managed: it receives injection, its lifecycle callbacks run, and calls reach it through a proxy that can apply interceptors, transactions and security. If application code created it, it is an ordinary object and none of those services exist for it, even when its class carries exactly the same annotations.

The second hand-over point is the call itself. A call from one bean to another passes through the container's proxy, but a call from one method to another on the same instance does not, so an interceptor binding or @Transactional on the inner method is silently ignored. The third is time: the container decides when a bean is created, so normal-scoped beans are instantiated lazily on the first call through their proxy, and a bean whose @PostConstruct must run at start-up has to observe the Startup event or be touched explicitly.

Managed versus unmanaged instances
@ApplicationScoped
public class InvoiceService {
    @Inject AuditLog audit;               // injected: managed

    @Transactional
    public void issue(Invoice i) {
        audit.record(i);                  // via proxy: interceptors apply
        this.notifyCustomer(i);           // self-call: no interception
    }

    @Transactional(Transactional.TxType.REQUIRES_NEW)
    public void notifyCustomer(Invoice i) {
        // NOT a new transaction when called from issue()
    }
}

// Created with new: no injection, no callbacks, no transactions
InvoiceService plain = new InvoiceService();   // plain.audit == null

4· Java / Jakarta EE Code Examples

Several forms of IoC in one bean
// lifecycle: one instance per application
@ApplicationScoped
public class ExchangeRateCache {
    @Inject RateClient client;                  // DI: supplied by container
    private final Map<String, BigDecimal> rates = new ConcurrentHashMap<>();
 
    @PostConstruct void warmUp() {              // callback: container decides when
        rates.putAll(client.fetchAll());
    }
    // event IoC
    void onRatesChanged(@Observes RatesChanged e) { rates.putAll(e.rates()); }
 
    @PreDestroy void shutdown() { rates.clear(); }
}
Service locator: control not inverted
// Service locator (for contrast) — the component pulls its dependency
public class LegacyService {
    public void run() throws NamingException {
        DataSource ds = (DataSource) new
                InitialContext().lookup("java:comp/env/jdbc/app");
        // hidden dependency, failure only at runtime, hard to test
    }
}
The container as composition root in Java SE
// Bootstrapping a CDI container in Java SE (CDI Full / Weld SE)
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
    ReportService svc = container.select(ReportService.class).get();
    svc.sendDailyReport();
}

5· Configuration, Defaults & Diagnostics

ItemNote
Container implementationsWeld (reference), Apache OpenWebBeans, Quarkus ArC (CDI Lite, build time)
Startup validationAll injection points resolved at deployment
Startup/shutdown events@Observes Startup / Shutdown (CDI 4.0+)
Java SE bootstrapSeContainerInitializer (CDI Full)

Because a CDI container validates the whole graph at deployment, wiring mistakes surface as start-up failures with precise messages, and reading them is the main diagnostic skill. An unsatisfied dependency means no bean matches the injection point's type and qualifiers, usually because the implementation sits in an archive without bean discovery, lacks a bean-defining annotation in annotated mode, or was vetoed. An ambiguous dependency means two or more beans match, and a qualifier or a prioritised alternative is needed. Weld Probe in development mode and the Quarkus Dev UI list every discovered bean, which quickly shows whether the expected implementation was found at all.

Reading the two classic wiring errors
WELD-001408: Unsatisfied dependencies for type RateClient
  with qualifiers @Default
  at injection point [BackedAnnotatedField] @Inject ExchangeRateCache.client
# fix: give the implementation a bean-defining annotation
# (e.g. @ApplicationScoped) or use bean-discovery-mode="all"

WELD-001409: Ambiguous dependencies for type RateClient
  Possible dependencies: [EcbRateClient, CachedRateClient]
# fix: add a qualifier, or mark one @Alternative @Priority(1)

6· Production Usage Patterns

  • All wiring delegated to the container; no main()-style composition in Jakarta EE apps.
  • Build-time containers (Quarkus ArC) resolving the graph at build time for fast startup.
  • Startup observers initialising resources instead of static initialisers.

7· Common Pitfalls & Misconceptions

  • Mixing JNDI lookups and injection inconsistently.
  • Static state that bypasses the container — invisible to lifecycle and tests.
  • Heavy work in constructors instead of @PostConstruct.

8· Best Practices

  • Let the container create and wire all managed components.
  • Declare lifecycle with scopes and callbacks, not manual singletons.
  • Use startup events for initialisation logic.

9· Architect's Perspective & Trade-offs

Container styleResolution timeStrengthTrade-off
Runtime reflection (Weld, Spring)Deployment/startupFull dynamism, portable extensionsStartup time, reflection
Build-time (Quarkus ArC, Micronaut, Dagger)BuildFast startup, native-image friendlyLess runtime dynamism (CDI Lite)

10· Interview Q&A

Q1BasicConcept

What is Inversion of Control?

Inversion of Control means the framework, not your code, drives execution: it creates components, decides when their methods are called and ends their lifetime, while the application supplies pieces that plug into it. The usual summary is the Hollywood principle, don't call us, we'll call you. A servlet never runs its own loop; the container calls service() when a request arrives. A CDI bean never constructs its collaborators; the container injects them and then invokes @PostConstruct. Because the framework owns creation and invocation, it can wrap every call with transactions, security and observability, which is why IoC underpins every enterprise Java platform, from Jakarta EE to Spring.

Q2IntermediateConcept

How does dependency injection relate to IoC?

Dependency injection is one specific form of IoC, the one that inverts how a component obtains its collaborators. Without it, a class creates them with new or looks them up; with it, the class declares what it needs, through a constructor, field or initializer method marked @Inject, and the container supplies a suitable instance. IoC is broader: event observers, lifecycle callbacks, template methods such as HttpServlet.service() calling doGet(), and framework dispatch of Jakarta REST resource methods all invert control without injecting anything. So every DI container is an IoC container, but not every IoC mechanism is dependency injection, and treating the terms as synonyms is a common sign of shallow understanding.

Q3IntermediateTrade-off

What is the difference between DI and a service locator?

Both decouple a component from concrete implementations, but they differ in who acts. With a service locator, such as a JNDI lookup or a static registry, the component pulls its dependencies, so they are hidden inside method bodies, a missing binding fails only when that code path runs, and tests must prepare the global registry. With DI the dependencies are pushed in from outside and visible in the constructor or fields, the container validates them at deployment, and tests simply pass mocks. A locator still has a place for genuinely dynamic choices, such as selecting a plugin by name at run time, and in CDI the idiomatic equivalent is Instance<T> with qualifiers.

Q4Senior EngineerInternals

What responsibilities does a CDI container take over?

A CDI container takes over the whole life of a managed bean. At deployment it discovers beans according to the bean discovery mode, builds the bean graph and validates every injection point, failing with an unsatisfied or ambiguous dependency error before any request arrives. At run time it creates instances lazily within their scope, injects dependencies, calls @PostConstruct, and destroys instances with @PreDestroy when the context ends. For normal-scoped beans it injects client proxies, so a request-scoped bean can safely be used from an application-scoped one. It also applies interceptors and decorators, delivers events to observer methods, and lets portable or build compatible extensions modify the bean model.

Q5Technical ArchitectTrade-off

What is the advantage of build-time dependency resolution?

The container's analysis moves from start-up to build. Quarkus ArC, Micronaut and Dagger scan annotations, resolve injection points and generate plain wiring code during compilation, so the running application performs no classpath scanning and little reflection. Start-up drops from seconds to tens of milliseconds, memory falls because no metadata model is retained, and wiring errors fail the build rather than the deployment. It also makes GraalVM native images practical, since there is little runtime reflection to register. The trade-off is dynamism: CDI Lite has no portable extensions, beans cannot be added at run time, and reflection-heavy libraries need extra configuration. For containerised and serverless workloads the trade is usually worth it.

Q6Solution ArchitectConcept

Why is IoC essential for enterprise platforms?

Because a platform can provide services uniformly only to code whose creation and invocation it controls. When the container instantiates a bean and hands out a proxy, it can begin and commit transactions, check roles, propagate security and trace context, apply retries or metrics, and release resources when a scope ends, all without the business code calling anything. Teams get consistent behaviour across hundreds of services, and platform teams can upgrade those concerns centrally. Code that bypasses the container through static singletons or new silently loses the guarantees: a method on an object created with new runs without a transaction. IoC is therefore the contract between application code and the platform.

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.