The Annotations Behind the Magic

The two annotations that let Spring wire up dozens of controllers automatically, why one shared service object is safe across many threads, and why field injection is worth avoiding.

September 1, 20263 min read33 / 43

Wiring dozens of controllers, services, and repositories by hand, in the right order, is exactly the tedious work a framework exists to remove. Here's how it actually removes it.

Marking a Class as Something the Framework Should Manage

A plain class needs to be told the framework should own creating and managing its object, the same way a plain class needed to be told it should be persisted at all, back when this series first covered entities. Marking a class as a controller, a service, or a repository tells the framework exactly that: create an object of this on my behalf, and keep track of it.

Only one object of a controller, a service, or a repository is ever needed across a whole running application, unlike a Ticket, where many objects genuinely have to exist. The difference is that a controller or service holds behavior, not data — there's nothing per-instance worth duplicating, so one shared object is enough.

Why One Shared Object Is Safe Across Many Requests at Once

One object doesn't mean one thing happens at a time. The same method, called from two different threads at once, runs against two entirely separate sets of local variables, each thread tracking its own. This is the identical mechanism already covered earlier in this series when finding a critical section: the code is shared, the execution state around it isn't. A shared service object works exactly the same way under concurrent requests.

Telling the Framework to Fill In a Parameter Automatically

Marking a class doesn't automatically connect it to whatever depends on it. A second signal tells the framework to fill in a specific parameter automatically, finding an existing object of the right type, or creating one if none exists yet, and passing it in without anyone writing that wiring code by hand.

Constructor Injection Over Field Injection

This second signal can go directly on a field, or on a constructor's parameters instead, and the constructor is the one actually worth using. Putting it directly on a field hides exactly where that object came from. Remove the framework later, or trace through several parameters whose order actually matters, and a field quietly filled in by magic makes both harder than they need to be.

A constructor keeps the wiring visible: what's needed, and in what order, sits right there in the method signature. Less magic means anyone reading the code later can actually reason about where an object came from, instead of having to trust that the framework did something invisible correctly.

A Controller, a Service, and a Repository Are Functionally Identical Labels

One detail worth knowing directly: marking a class as a controller, a service, or a repository doesn't actually do anything different behind each of those three words. All three tell the framework the exact same thing — manage an object of this class — and nothing more. Put a service label on what's conceptually a repository, and it still works identically.

The three names exist purely so a human reading the codebase can tell what a class is for, not because the framework treats them differently. The one real exception is a more specific label built for handling web requests directly, which layers in actual extra behavior, like automatically converting a returned object into JSON. That one earns its distinction. The other three are just naming convention.