---
title: 'Automatic Configuration'
source: 'https://academia.sh/en/courses/enterprise-application-framework/automatic-configuration'
course: 'The Enterprise Application Framework'
language: en
updated: '2026-10-21T05:03:16+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures, by comparing two setups, that the container puts its own default in place when no component is declared for an interface, and steps back when one is declared'
    - 'Runs the same container in two different setups and counts how many behaviors are born with no annotation at all'
    - 'Proves by showing that a component is never constructed that adding one component can silently disable another'
---

# Automatic Configuration

If no component is declared for an interface, the IoC container puts its own default implementation in place; when the user declares their own component, the container silently steps back and never installs its own default at all. What is automatic is conditional, and that condition appears nowhere in the source with any word at all.

The previous lesson measured that a setting falls to the embedded default when it is found in no
source at all — but what fell was a **value**, the content of a field that already existed. This
lesson carries the same idea one level up: sometimes what falls is not a value, it is **the
component itself**. When no one at all declares an implementation for an interface, the container
fills that interface on its own — by bringing in a class of its own choosing. This behavior's
name is **automatic configuration**, and this lesson's question is: how conditional is the thing
that is actually "automatic"?

The fourth lesson's precedence chain queried three sources **in order** — external source, file,
embedded default — and for every setting, only one of the three won. This lesson's question is
shaped differently: there is no ordered chain here, only a **binary** decision — did the user
declare something or not. This binary is what separates automatic configuration from the
previous lesson: what determines the winner is not a precedence order, it is an **existence
test**. If a class satisfying the requested interface exists in the set the user declared, the
container never steps in at all; if not, it brings in the only candidate it has.

## An Undeclared Interface, the Container's Own Decision

In the measurement below there are three interfaces — `Notifier`, `Cache`, `Metric` — and for
each one the container has an **automatic default** class ready on its own: `AutoNotifier`,
`AutoCache`, `AutoMetric`. This measurement does not use the previous lessons' `@Container.
Component` marker — here, "declaring" only means a class being added to a small list given by
the user, but its role is the same: this list carries the candidates that will take the place of
the managed components the container would otherwise construct on its own. Before making a
decision for an interface, the container looks at whether the user has declared a component
implementing that interface. Did they declare one? Then the container **steps back** — it never
installs its own automatic default at all. Did they not? The container puts in its own default,
and the program never explicitly asked for this decision anywhere.

This "looking" happens in the `resolve` method: the method walks through the list of components
the user declared, and if there is a class in the list implementing the requested interface, it
returns it; if not, it increments its own counter and constructs the automatic default. In this
measurement, "the list of components the user declared" is not a real class path scan, it is a
small list given by hand — but the mechanism itself is the same: a real application framework
asks the same question too, just over a far larger set of candidates.

The check is done with `iface.isAssignableFrom(candidate)`, not `instanceof` — because there is
no **object** in hand yet, only a `Class<?>` reference. `isAssignableFrom` asks whether one type
can be assigned to another, without constructing any instance at all, only by comparing two
`Class` objects. This distinction matters: to decide which class implements which interface, the
container does not have to **construct** that class first — it answers the question entirely at
the type level, and only constructs the actual object with
`getDeclaredConstructor().newInstance()` once the winning candidate is known. The automatic
default candidates are constructed the same way; the only difference between them is that the
automatic default is called only when **no candidate is found at all**.

- **CC22.** "The components the user declared" here is a small list given by hand; there is no
  real class path scan or package discovery in this measurement. All three interfaces have
  exactly one automatic default; choosing among more than one automatic candidate is not within
  this measurement's scope.
- **CC23.** The container runs two setups one after another within the same run; each setup
  carries its own component list, there is no shared state between them.
- **CC24.** The "behavior born with no annotation" counter only counts the cases where the
  automatic default was constructed; a component the user declared themselves never enters this
  counter at all.
- **CC25.** How many times a class was constructed is tracked with a counter added to the
  class's own constructor; no real timestamp or memory measurement is used.

This measurement is deliberately narrower than the previous three lessons' `Container`: the
`resolve` method calls `getDeclaredConstructor().newInstance()`, looking for a parameterless
constructor — unlike the second lesson's `get` method, it does not recursively resolve
constructor parameters. None of the four classes in this measurement (`AutoNotifier`,
`AutoCache`, `AutoMetric`, `CustomNotifier`) has a dependency, so this simplification does not
change the result; but in a real application framework, an automatically configured component can
also have its own dependencies, and then the construction process would need a combination of
this lesson's `resolve` and the second lesson's `get`.

```java
// AutomaticConfiguration.java — if a component is never declared the container puts its own default in place, if declared it steps back
import java.util.*;

public class AutomaticConfiguration {

    static int noAnnotationCount = 0;

    static Object resolve(Class<?> iface, List<Class<?>> userComponents, Class<?> autoDefault) throws Exception {
        for (Class<?> candidate : userComponents) {
            if (iface.isAssignableFrom(candidate)) {
                System.out.printf("%-12s user declared %-18s container stepped back%n", iface.getSimpleName(), candidate.getSimpleName());
                return candidate.getDeclaredConstructor().newInstance();
            }
        }
        noAnnotationCount++;
        System.out.printf("%-12s no declaration at all, container put its default: %s%n", iface.getSimpleName(), autoDefault.getSimpleName());
        return autoDefault.getDeclaredConstructor().newInstance();
    }

    static void setup(String name, List<Class<?>> userComponents) throws Exception {
        System.out.println("-- setup: " + name + " --");
        noAnnotationCount = 0;
        Notifier n = (Notifier) resolve(Notifier.class, userComponents, AutoNotifier.class);
        Cache c = (Cache) resolve(Cache.class, userComponents, AutoCache.class);
        Metric m = (Metric) resolve(Metric.class, userComponents, AutoMetric.class);
        System.out.println("notifier result   : " + n.send());
        System.out.println("cache result      : " + c.get("x"));
        System.out.println("metric result     : " + m.count());
        System.out.println("behavior count born with no annotation at all: " + noAnnotationCount);
        System.out.println();
    }

    public static void main(String[] args) throws Exception {
        setup("no user component at all", List.of());
        setup("user wrote their own notifier", List.of(CustomNotifier.class));

        System.out.println("how many times was AutoNotifier constructed across two setups: " + AutoNotifier.constructionCount);
    }
}

interface Notifier { String send(); }
interface Cache { String get(String key); }
interface Metric { int count(); }

class AutoNotifier implements Notifier {
    static int constructionCount = 0;
    AutoNotifier() { constructionCount++; }
    public String send() { return "auto-notification"; }
}
class AutoCache implements Cache {
    public String get(String key) { return "auto-cache:" + key; }
}
class AutoMetric implements Metric {
    public int count() { return 0; }
}

class CustomNotifier implements Notifier {
    public String send() { return "custom-notification"; }
}
```

```
-- setup: no user component at all --
Notifier     no declaration at all, container put its default: AutoNotifier
Cache        no declaration at all, container put its default: AutoCache
Metric       no declaration at all, container put its default: AutoMetric
notifier result   : auto-notification
cache result      : auto-cache:x
metric result     : 0
behavior count born with no annotation at all: 3

-- setup: user wrote their own notifier --
Notifier     user declared CustomNotifier     container stepped back
Cache        no declaration at all, container put its default: AutoCache
Metric       no declaration at all, container put its default: AutoMetric
notifier result   : custom-notification
cache result      : auto-cache:x
metric result     : 0
behavior count born with no annotation at all: 2

how many times was AutoNotifier constructed across two setups: 1
```

## The Same Container, Two Setups, Two Separate Numbers

In the first setup, the user declares no component at all —
`setup("no user component at all", List.of())` gives an empty list. All three interfaces receive
the container's automatic defaults: `AutoNotifier`, `AutoCache`, `AutoMetric`. The counter is
`3` — all three behaviors were born with no annotation at all. There is not a single line in the
source naming `AutoNotifier`; the container made this decision entirely on its own, because the
question it asked ("did the user declare something") got the answer `no`.

In the second setup, the user only declares their own component for `Notifier` —
`CustomNotifier`. Now one of the three results changes: the `Notifier` request is now met with
`CustomNotifier`, the output becomes `"custom-notification"`, and the container explicitly says
that it stepped back ("container stepped back"). But `Cache` and `Metric` still received no
declaration at all, so they still get their automatic defaults. The counter this time is `2` —
two of three behaviors are still born with no annotation at all, one now comes from a declaration
that really stands in the source.

These two numbers — `3` and `2` — are two separate results the same `resolve` method produces,
with the same three interfaces, with only one input difference. The container itself never
changed; the only thing that changed is the list given by the user. The word "automatic" does
not mean "always the same" here — it means **conditional**: if the condition is not met (no one
declared anything), the automatic default kicks in; if the condition is met (someone declared
their own component), the automatic default is never installed at all.

The setup itself also carries an observation: `noAnnotationCount` is reset to zero before the
`setup` method is called, because the counter is `static` — the two setups share the same static
field, and without the reset, the second setup's count would pile up on top of the first's. This
is the measurement's own internal detail, but it carries a familiar warning: shared state can mix
up two scenarios run one after another; here this mixing is deliberately prevented with the line
`noAnnotationCount = 0;`, but the same precaution is not taken for
`AutoNotifier.constructionCount` — that counter deliberately accumulates across the two setups,
because what is meant to be measured is exactly this: how many times it was constructed in total.

This conditionality carries automatic configuration's real promise and its real risk at the same
time. The promise: getting a reasonable implementation for a commonly used interface without
writing anything at all — three working components come out of a three-line setup, and no one
has to know the name `AutoNotifier`. The risk comes from the same place: the container decides on
its own under which condition it steps back, and this condition (whether the
`isAssignableFrom` check returns true or false) appears explicitly in no line of the code at all.
Adding a component is not just "installing something new," it can also mean "taking the previous
one's place," and this second effect does not show up in the source.

## Limiting Measurement: Silent Deactivation

The last line shows how complete this stepping-back is: the `AutoNotifier` class's constructor
keeps its own construction count, and across two setups that number is only `1` — `AutoNotifier`
was constructed only in the first setup, in the second setup it was **never constructed at
all**. The container did not even attempt to build it; the `resolve` method left the loop the
moment it found `CustomNotifier`, and never touched the automatic default at all.

This is the lesson's limiting claim: **adding a component can silently disable another
component that is never mentioned in the source at all.** The person adding `CustomNotifier`
probably only wanted their own notification logic; they do not need to know that
`AutoNotifier` exists, or that it is no longer constructed, because both stand behind the same
`Notifier` interface and the calling side can call `send()` without knowing which one is
running. Deactivation produces no warning, no log line, no exception at all — only a class that
is never constructed. `Cache` and `Metric` are still automatic in this second setup too, but
this too is written nowhere in the source explicitly; it is only the natural result of the
silence of "no one declared anything."

The difference between this defect and the fourth lesson's spelling difference is that nothing
here needs to be **misspelled**. `CustomNotifier` was written correctly, implemented the correct
interface, worked as expected — the defect is not in its own code, it is in its **existence**
causing another class to never be constructed at all. The defects seen in the second and third
lessons (an unbound interface, an unrecognized scope name) were both a shortage; this "defect" is
the exact opposite — something being **added**. None of the three sources measured throughout the
course (call, annotation, default) is "good" or "bad" on its own; this lesson shows that the
comfort of an automatic default carries an invisibility cost at the same time.

## Three Independent Decisions, One Method

The `resolve` method is called separately for each interface, and each call makes its own
decision entirely on its own — the decision made for `Notifier` has no effect at all on the
decision made for `Cache` or `Metric`. This independence is deliberate: in a real application
framework too, dozens of automatic configuration candidates are evaluated at the same time, and
each one answers its own "did the user declare it" question independently for its own interface.
This measurement's three interfaces were chosen to make this independence visible: only
`Notifier` changed in the second setup, the others stayed the same — if these three were tied to
each other, changing `Notifier` could have affected whether `Cache` stayed automatic too. In this
measurement it does not, because the `resolve` calls never see each other at all.

This independence also draws a limit: the `resolve` method never asks what to do if it finds
**more than one** candidate in the user's declared list — the loop returns the moment it finds
the first matching candidate, it never looks at the rest of the list at all. In this measurement
there is at most one user candidate per interface, so this situation never comes up; but in a
real system, two separate components can implement the same interface and both can be declared,
and then the winner would no longer be determined by "did the user declare it" but by the list's
**order** — a defect very similar to the fourth lesson's ordering dependency in the chain, inside
a different mechanism.

## Summary

- When no component is declared for an interface, the container installs its own automatic
  default; this decision appears in no line of the source at all.
- When the user declares their own component, the container steps back and never installs the
  automatic default at all — two behaviors come from the same `resolve` method, only with an
  input difference.
- When the same container is run in two setups, the count of behavior born with no annotation
  drops from `3` to `2`; this difference is explained only by the one component the user
  declared.
- What is "automatic" is actually conditional: the condition (no one having declared anything)
  appears with no word in the source at all.
- Adding a component can silently disable another component never mentioned in the source at
  all — the disabled class is never constructed, and it produces no warning at all.

This topic's five lessons brought up the same three sources over and over: a behavior was born
either from a call standing in the source, or from an annotation in the source, or from the
container's own rule — a default. This lesson showed that this third source has its own grade
within itself: sometimes the default is a fixed value (the fourth lesson), sometimes a
conditional component choice (this lesson). Both share the same point: someone reading the
source from top to bottom, without knowing the container's rule in advance, cannot predict which
result will be reached.

## Next Step

In both setups, `Cache` and `Metric` worked through the same interface, without the call site
ever being touched — the calling side never had to know which implementation stood behind it at
all. This topic's last lesson carries this same observation one step further: is it possible to
add something from outside to a component's behavior, without ever touching the call site at
all? The next lesson measures this — how a cross-cutting concern (a behavior belonging to no
single class, like logging, counting, or auditing) is added without changing the call site.
