DEV Community

Ankit Verma
Ankit Verma

Posted on

Component scanning & stereotypes

The last article left a thread dangling. It showed that the annotation style marks a class as a bean by putting @Component — or one of its cousins — right on the class, and then said the container finds those marked classes by component scanning. It promised that scanning was "its own topic, the very next article." This is that article.

Component scanning is the step where Spring walks through your code, finds the classes you marked as beans, and registers each one with the container. It is the bridge between "I annotated a class" and "the container knows that class exists." Without it, an @Service is just an annotation sitting on a class Spring never bothers to look at.

You meet scanning the first time a class you wrote refuses to show up. You added @Service, you asked the container for it, and Spring answers that there is no such bean. Almost always the class is sitting in a package the scan never visited. To fix that with intent instead of guesswork, you need to know what scanning actually does and where it actually looks.

This article walks the whole mechanism: how the scan finds your classes, the stereotype annotations that mark them, the one stereotype that quietly does more than mark, where the search begins, and the traps that come from searching too narrowly or too widely.

The mark and the search are two halves of one thing

Recall the single job of @Component: it labels a class as something the container should build and manage — a bean. But a label does nothing on its own. Something has to go looking for it.

That something is the component scanner. At startup, before a single bean is built, the scanner walks a set of packages, inspects every class it finds, and for each class wearing @Component it writes down a bean definition — the recipe card from the earlier articles. When the scan finishes, the container holds a definition for every annotated class, exactly as if you had spelled them all out in Java config by hand.

@Service
class OrderService {
    OrderService(PaymentGateway gateway) { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

On its own, that class is inert. The annotation says "I am a bean"; the scan is what hears it. Miss either half — forget the annotation, or never scan the package it lives in — and the bean simply does not exist.

Turning the scan on

You rarely write the line that starts scanning, but it is worth seeing once, plainly. The annotation that switches it on is @ComponentScan:

@Configuration
@ComponentScan("com.shop")
class AppConfig { }
Enter fullscreen mode Exit fullscreen mode

That says: walk the com.shop package and everything beneath it, and register every @Component you find. The string is a base package — the root where the search begins. Scanning is recursive, so com.shop, com.shop.orders, and com.shop.billing.tax are all swept in by that one line.

In a Spring Boot app you never actually write @ComponentScan, because it is hidden inside the annotation on your main class. But it is running all the same, and knowing it is there is what makes the next trap avoidable.

Where the search starts — and the classic trap

The annotation on your main class, @SpringBootApplication, is a bundle: it folds together @Configuration, the auto-configuration switch, and @ComponentScan — the last one with no package argument.

When you give @ComponentScan no package, it does not scan nothing. It defaults to the package of the class it sits on, and everything below it.

package com.shop;          // the root package

@SpringBootApplication      // scans com.shop and downward
class ShopApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShopApplication.class, args);
    }
}
Enter fullscreen mode Exit fullscreen mode

This is the reason every Boot project puts its main class in the root package, above all the others. From there, the default scan naturally covers the whole application — com.shop.web, com.shop.billing, all of it.

Now the trap, which catches nearly everyone once. Move that main class down into a sub-package by mistake:

package com.shop.web;       // oops — not the root

@SpringBootApplication       // scans com.shop.web and downward ONLY
class ShopApplication { }
Enter fullscreen mode Exit fullscreen mode

Scanning now starts at com.shop.web. Your com.shop.billing package is above the start point, so it is never visited, and none of its beans get registered. The symptom is a NoSuchBeanDefinitionException at startup — Spring asking for a bean it was never told about. The fix is to move the main class back up to the root, or to name the packages explicitly with @SpringBootApplication(scanBasePackages = "com.shop"). Either way, the cause is the same: the scan only ever looks down from where it starts.

The stereotypes: same scan, different meaning

@Component is the generic mark. On top of it, Spring ships three specialized labels — @Service, @Repository, and @Controller — known together as the stereotype annotations, because each names the role a class plays in the usual layered design: business logic, data access, web entry point.

Here is the detail that ties them to everything above. Each stereotype is itself meta-annotated with @Component — that is, @Component is stamped on the annotation itself:

@Component          // <-- right here, on the annotation
public @interface Service {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Because the scanner treats "annotated with @Component, directly or through another annotation" as the trigger, a @Service is picked up by exactly the same scan as a bare @Component. There is no separate machinery for them. A stereotype is a @Component with a name on it.

So why bother with the specialized ones? Two reasons, both real but modest. They document the layer at a glance — @Repository tells a reader "this talks to the database" in a way @Component never could. And they give Spring and your tools a hook to treat a layer specially. For most classes the stereotypes are interchangeable with @Component; the habit is simply to pick the one that names what the class actually does.

@Repository earns more than its name

One stereotype does more than label, and it is worth knowing exactly what. When the scan registers a @Repository bean, Spring also wraps it in a proxy — the same stand-in trick from the first article — to perform exception translation.

Here is the problem it solves. Every persistence technology throws its own exceptions: raw JDBC throws SQLException, Hibernate throws its own HibernateException, and so on. If your service layer caught those directly, it would be welded to whichever library sits underneath.

@Repository
class JpaOrderRepository {
    Order findById(long id) { /* ... */ }   // may hit a vendor-specific failure
}
Enter fullscreen mode Exit fullscreen mode

Because this is a @Repository, the proxy around it catches those vendor-specific exceptions on the way out and rethrows them as Spring's own DataAccessException family — one consistent hierarchy, the same no matter what runs below. Swap JPA for plain JDBC tomorrow, and the exception types your service code catches do not change. That single benefit is a concrete reason to put @Repository on data-access classes rather than a generic @Component.

The @Controller stereotype has its own special handling too — the web layer recognizes it and routes HTTP requests to its methods — but that belongs to the web module later. For now it is enough that the scan treats it like any other @Component, and something downstream gives it extra meaning.

Every scanned bean gets a name

When the scanner registers a bean, the container needs a name for it. By default, scanning takes the simple class name and decapitalizes the first letter: OrderService becomes the bean named orderService.

@Service
class OrderService { }      // registered under the name "orderService"
Enter fullscreen mode Exit fullscreen mode

Most of the time you never touch the name, because injection resolves by type, as the dependency-injection article showed. The name starts to matter only when two beans share a type and you must point at one specifically — that tie-breaking is the next article's subject — or when two names collide.

The collision is worth flagging because it fails loudly. Put two classes with the same simple name in different packages, and both want to be orderService:

// com.shop.billing.OrderService   -> wants "orderService"
// com.shop.legacy.OrderService    -> also wants "orderService"
Enter fullscreen mode Exit fullscreen mode

Scanning refuses the ambiguity and throws ConflictingBeanDefinitionException at startup. That is the fail-fast philosophy again — a name clash is caught at boot, not silently resolved into the wrong bean. When you genuinely need two such classes, give one an explicit name with @Service("legacyOrderService").

Scanning too wide

By default the scan registers everything annotated that it finds below the base package. Usually that is what you want. But a base package set too broad will quietly pull in classes you never meant to activate — a stray @Configuration in some sub-module that switches a feature on, or test-only beans that have no business in production.

For that, @ComponentScan accepts filters that include or exclude classes by annotation, type, or pattern:

@ComponentScan(
    basePackages = "com.shop",
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.ANNOTATION,
        classes = Configuration.class)   // skip stray @Configuration classes
)
class AppConfig { }
Enter fullscreen mode Exit fullscreen mode

Filters are the precise tool when the default sweep grabs too much. The blunter fix is the better default: keep your base packages tight. A narrow scan registers only what you intend, and as a bonus it costs less at startup — walking a smaller slice of the classpath is faster than walking all of it. Over-broad scanning is one of the quiet reasons a large application boots slowly.

Putting it together

Component scanning is how the annotation style turns marked classes into registered beans. The scanner walks a base package and everything under it, and for every class carrying @Component — directly or through a stereotype — it writes a bean definition into the container. The annotation declares the intent; the scan is what acts on it, and both halves are required.

The base package is where the search starts and only ever goes downward — which is why the main class lives in the root package, and why a class above the scan's start point silently never registers. The stereotypes@Service, @Repository, @Controller — are @Component with a role attached; they are scanned identically, but @Repository also earns exception translation, and @Controller is later picked up by the web layer. Each scanned bean gets a decapitalized class name by default, and a name clash fails fast at boot.

So the scan ends with the container holding a complete set of bean definitions, names and all. Which raises the question the next article answers: when the container goes to build OrderService and sees it needs a PaymentGateway — and two beans happen to fit that type — how does it decide which one to inject? That is the job of @Autowired, @Qualifier, and @Primary, and it is where we go next.

Top comments (0)