JAVA + SPRING AI — INTERMEDIATE
Spring Beans & Constructor Injection: The Real Mechanics
The Introduction level covered whyobjects shouldn’t build their own dependencies. Here’s how Spring actually does the building and handing-over for you — beans, the container that holds them, and the exact, current idiomatic way to write a constructor-injected class.
The Quick Answer
Annotate a class with @Component (or one of its specializations, @Service and @Repository), and Spring registers it as a bean— an object whose lifecycle and dependencies Spring itself manages, rather than your code managing them. All of Spring’s beans live inside the ApplicationContext, the container that creates them, resolves what each one depends on, and hands out fully-wired instances. The idiomatic way to receive a dependency is through the class’s constructor — and in current Spring Framework 7.x / Spring Boot 4.x, if that class has only one constructor, you don’t even need to write @Autowired on it.
From Annotation to Bean
@Componentis Spring’s generic “manage this class as a bean” marker. @Service and @Repositorydo exactly the same underlying registration — they’re specialized versions of @Component that also communicate intent (a service-layer class, a data-access class) and, in @Repository’s case, add automatic translation of persistence-specific exceptions into Spring’s own exception hierarchy (Spring Boot wires up the machinery for this automatically via autoconfiguration, so it’s a “just works” detail in virtually every standard setup). When Spring scans your codebase at startup and finds one of these annotations, it creates an instance of that class and keeps it in the container — that managed instance is what “bean” means.
The ApplicationContext: Where Beans Live
The ApplicationContext is Spring’s IoC container — the “kitchen” from the Introduction level’s analogy, made concrete. At startup, it scans for annotated classes, creates a bean for each one, and — this is the key part — figures out what each bean needs and supplies it automatically. If OrderService needs a Notifier, the context looks for a bean that satisfies that requirement and passes it in. You never call new OrderService(...) yourself; the container does, once, and reuses that instance everywhere it’s needed (more on exactly how many instances get created, and when that’s not safe, in the Advanced level).
Three Ways to Receive a Dependency
Spring supports three styles for handing a bean its dependencies:
- 1
Constructor injection — the dependency is a constructor parameter. This is the modern default, covered in detail below.
- 2
Field injection —
@Autowiredis placed directly on an instance field, and Spring sets it via reflection after constructing the object. It’s the shortest to type, which is exactly why it shows up in a lot of quick tutorials — but it means the field can silently benulluntil Spring gets around to injecting it, and it makes the class harder to instantiate outside a Spring container (for example, directly in a plain unit test) since there’s no constructor forcing the dependency to be supplied. - 3
Setter injection — a public setter method, annotated
@Autowired, that Spring calls after construction. Spring’s own reference documentation specifically recommends this style for optionaldependencies that can reasonably default to something else, not for dependencies the class can’t function without. The more modern, idiomatic way to express that same optionality is ajava.util.Optional<T>or@Nullableparameter on the setter (or any injected method) rather than relying on setter injection alone.
Why Constructor Injection Is the Default
This isn’t just convention — it’s Spring’s own stated position. Spring’s reference documentation says the team “generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state.” Three concrete benefits fall out of that:
- Immutability. A dependency assigned in the constructor can be stored in a
finalfield — it can’t be reassigned later, accidentally or otherwise. - No half-built objects.An object either has everything its constructor demands, or it doesn’t exist yet. There is no in-between state where the object exists but a required field hasn’t been filled in.
- Testability.Since the dependency is just a constructor parameter, plain unit tests can construct the class directly with a hand-built test double — no Spring container needed at all to test the class’s own logic.
A Real Constructor-Injected Service
Here’s idiomatic current Spring — Framework 7.x, Boot 4.x. Notice there’s no @Autowired anywhere:
public interface Notifier {
void send(String message);
}
@Component
public class EmailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("Emailing: " + message);
}
}
@Service
public class OrderService {
private final Notifier notifier;
// No @Autowired needed here — this class has exactly one
// constructor, so Spring always uses it for injection.
public OrderService(Notifier notifier) {
this.notifier = notifier;
}
public void placeOrder() {
notifier.send("Your order has shipped!");
}
}That omission isn’t a simplification for the article — it’s Spring’s actual documented behavior. The Spring Framework reference states it directly: “If a class only declares a single constructor to begin with, it will always be used, even if not annotated.” @Autowired only becomes necessary once a class declares more than one constructor and you need to tell Spring which one to use for dependency injection.
One Interface, Swappable Implementation
The diagram below shows the shape of what that code just did: “OrderService” only ever talks to the “Notifier” interface. The container is the thing that decides which concrete class actually answers when send() gets called.
Try it yourself below — flip between a tightly coupled version and an injected one, and swap which implementation the container hands over.
Fun Fact
Making @Autowired optional on a single-constructor class wasn’t always the default — it was a deliberate ergonomics improvement the Spring team added specifically so simple, single-dependency classes wouldn’t need to carry an annotation whose job (“use this constructor”) had only one possible answer anyway.
Test Yourself
What does an @Service (or @Component, @Repository)-annotated class become inside Spring?
What is the ApplicationContext?
According to Spring's own documentation, do you need to annotate a constructor with @Autowired if the class has only one constructor?
Why does the Spring team generally recommend constructor injection over field injection?
Ready for what happens after a bean is created — scopes, lifecycle callbacks, and what goes wrong when two beans depend on each other? Continue to the Advanced level →