Spring Beans: @Component, @Bean and @Configuration

Introduction

This tutorial explains the ways to declare beans in Spring - the objects the container creates and manages. We will start with @Component and its specialized forms @Service, @Repository and @Controller, and see how bean names are chosen. We will then look at component scanning: how Spring finds those classes, how to point it at the right packages, and how to include or exclude classes with filters. Next we will declare beans with @Bean methods in @Configuration classes - the way to make beans out of classes you cannot annotate - and see the difference between a @Configuration class and a plain one. Lastly, we will combine configuration classes with @Import and decide when to use which approach.

(This tutorial assumes you know what the container and a bean are. If not, read the Spring IoC container and ApplicationContextClick to read tutorial: Spring IoC Container and ApplicationContext Explained first.)

Two ways to declare a bean

Spring needs to know which objects to manage. You tell it in one of two ways -
  • Annotate the class with @Component (or one of its specializations). Spring finds the class by component scanning and creates the bean itself, using the class's constructor.
  • Write a factory method annotated with @Bean inside a configuration class. Spring calls the method, and the object it returns becomes the bean.
Both produce the same kind of bean, and an application usually uses both: @Component for its own classes, @Bean for library classes and for beans that need custom construction. (A third way, XML configuration, is still supported but is not covered in this series.)

@Component and the stereotype annotations

@Component is the general-purpose annotation. Spring also has three stereotype annotations, each itself annotated with @Component, that say what role a class plays -
  • @Service: business logic. It behaves exactly like @Component; the name documents intent.
  • @Repository: data access. Besides documenting intent, it lets Spring translate database-specific exceptions into Spring's DataAccessException hierarchy when exception translation is set up (Spring Data and Spring Boot do this for you).
  • @Controller (and @RestController): web request handlers in Spring MVC, which looks for these classes when it maps URLs.
The example below declares one class of each kind, gives one of them a custom bean name, and then prints the names Spring chose -
@Component, @Service, @Repository and bean names
//StereotypesApp.java
package com.javabrahman.spring.beans.stereotypes;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;

@Repository
class EmployeeRepository {
  String findName(int id) {
    return id == 1 ? "Deborah Sprightly" : "unknown";
  }
}

@Service
class EmployeeService {
  private final EmployeeRepository repository;

  EmployeeService(EmployeeRepository repository) {
    this.repository = repository;
  }

  String describe(int id) {
    return "Employee " + id + " is " + repository.findName(id);
  }
}

@Component("clock")   // explicit bean name instead of the default "systemClockAdapter"
class SystemClockAdapter {
  String now() {
    return "09:00";
  }
}

@Configuration
@ComponentScan
class AppConfig {
}

public class StereotypesApp {
  public static void main(String[] args) {
    try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
      System.out.println(context.getBean(EmployeeService.class).describe(1));
      for (String name : context.getBeanDefinitionNames()) {
        if (!name.contains(".")) {
          System.out.println(name + " -> " + context.getType(name).getSimpleName());
        }
      }
    }
  }
}
 OUTPUT of the above code
Employee 1 is Deborah Sprightly
appConfig -> AppConfig
employeeRepository -> EmployeeRepository
employeeService -> EmployeeService
clock -> SystemClockAdapter

Explanation of the code

  • Component scanning found all three annotated classes, because @Service and @Repository are meta-annotated with @Component.
  • The default bean name is the simple class name with its first letter in lower case: EmployeeRepository becomes employeeRepository.
  • @Component("clock") sets the name explicitly. The stereotype annotations accept a name the same way, for example @Service("payroll").
  • The configuration class itself, AppConfig, is a bean too: @Configuration is also meta-annotated with @Component.

Component scanning

@ComponentScan on a configuration class tells Spring where to look for component classes. Without attributes it scans the package of the configuration class and all its sub-packages. You can name other packages, and you can add filters that include or exclude classes. The example below has three packages - the application's root package, a data sub-package, and a legacy sub-package whose beans we want to leave out -
com.javabrahman.spring.beans.scan.data - a repository in a sub-package
//Customers.java
package com.javabrahman.spring.beans.scan.data;

import org.springframework.stereotype.Repository;

@Repository
public class Customers {
  public int count() {
    return 42;
  }
}
com.javabrahman.spring.beans.scan.legacy - a component we want to exclude
//OldMailer.java
package com.javabrahman.spring.beans.scan.legacy;

import org.springframework.stereotype.Component;

@Component
public class OldMailer {
  public OldMailer() {
    System.out.println("OldMailer created - this should not happen");
  }
}
Scanning the root package, with an exclude filter
//ScanApp.java
package com.javabrahman.spring.beans.scan;

import com.javabrahman.spring.beans.scan.data.Customers;
import java.util.Arrays;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Service;

@Service
class Dashboard {
  private final Customers customers;

  Dashboard(Customers customers) {
    this.customers = customers;
  }

  String summary() {
    return "Customers: " + customers.count();
  }
}

@Configuration
@ComponentScan(
    basePackages = "com.javabrahman.spring.beans.scan",
    excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX,
        pattern = "com\\.javabrahman\\.spring\\.beans\\.scan\\.legacy\\..*"))
class AppConfig {
}

public class ScanApp {
  public static void main(String[] args) {
    try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
      System.out.println(context.getBean(Dashboard.class).summary());
      System.out.println("Beans: " + Arrays.stream(context.getBeanDefinitionNames())
          .filter(name -> !name.contains(".")).toList());
    }
  }
}
 OUTPUT of the above code
Customers: 42
Beans: [appConfig, dashboard, customers]

Explanation of the code

  • basePackages names the package to scan; its sub-packages are included, which is how Customers in the data sub-package was found and injected into Dashboard.
  • excludeFilters with FilterType.REGEX removed every class in the legacy package, so OldMailer never became a bean - its constructor message did not print.
  • Other filter types match by annotation (FilterType.ANNOTATION), by assignable type (FilterType.ASSIGNABLE_TYPE), by AspectJ pattern, or by your own TypeFilter class. includeFilters works the other way round and adds classes that have no stereotype annotation.
  • basePackageClasses = SomeClass.class is a type-safe alternative to basePackages: it scans the package of the given class and still compiles if the package is renamed by a refactoring tool.
  • Tip: put the configuration class (in Spring Boot, the @SpringBootApplication class) in the root package of the application, so that the default scan covers everything below it and nothing else. Never scan a top-level package such as com, which would scan every library on the classpath too.

@Bean methods in a @Configuration class

@Component only works for classes you can edit. For a class from a library - here java.time.Clock and java.time.format.DateTimeFormatter from the JDK - you write a @Bean method that creates and configures the object. @Bean methods are also the right choice when creating a bean needs logic, such as choosing an implementation based on a setting -
Declaring beans for JDK classes with @Bean methods
//BeanMethodsApp.java
package com.javabrahman.spring.beans.methods;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

class ReceiptPrinter {
  private final Clock clock;
  private final DateTimeFormatter formatter;

  ReceiptPrinter(Clock clock, DateTimeFormatter formatter) {
    this.clock = clock;
    this.formatter = formatter;
  }

  String print(String item) {
    return item + " - " + formatter.format(clock.instant().atZone(clock.getZone()));
  }
}

@Configuration
class AppConfig {
  // A fixed clock, so the example prints the same time on every run
  @Bean
  Clock clock() {
    return Clock.fixed(Instant.parse("2026-11-06T10:15:30Z"), ZoneId.of("Europe/London"));
  }

  @Bean(name = "receiptDateFormat")
  DateTimeFormatter dateFormat() {
    return DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm");
  }

  @Bean
  ReceiptPrinter receiptPrinter(Clock clock, DateTimeFormatter formatter) {
    return new ReceiptPrinter(clock, formatter);
  }
}

public class BeanMethodsApp {
  public static void main(String[] args) {
    try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
      System.out.println(context.getBean(ReceiptPrinter.class).print("Java 8 in Action"));
      System.out.println("Formatter bean found by its custom name: "
          + context.containsBean("receiptDateFormat"));
    }
  }
}
 OUTPUT of the above code
Java 8 in Action - 06 Nov 2026 10:15
Formatter bean found by its custom name: true

Explanation of the code

  • Each @Bean method returns the object that becomes the bean. Spring calls it once (for a singleton) and keeps the result.
  • @Bean(name = "receiptDateFormat") overrides the default name, which would have been the method name, dateFormat.
  • The parameters of receiptPrinter() are resolved from the container by type: Spring passes in the Clock and DateTimeFormatter beans.
  • ReceiptPrinter has no Spring annotations at all. A class created by a @Bean method does not need to know Spring exists, which keeps it reusable and easy to test.
  • @Bean also accepts initMethod and destroyMethod attributes, covered in the tutorial on the bean lifecycleClick to read tutorial: Spring Bean Lifecycle: @PostConstruct, @PreDestroy and More.

Why @Configuration classes are special

@Bean methods can also be written in a class annotated only with @Component, or in a @Configuration(proxyBeanMethods = false) class - this is called lite mode. The difference shows when one @Bean method calls another. In a full @Configuration class, Spring subclasses the configuration class at runtime (with CGLIB) and intercepts such calls, so the call returns the existing singleton bean. In lite mode, the call is an ordinary Java method call and creates a second object -
Full @Configuration vs lite mode when @Bean methods call each other
//ConfigurationModesApp.java
package com.javabrahman.spring.beans.modes;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

class ConnectionPool {
  private static int created = 0;
  final int number = ++created;
}

class OrderDao {
  final ConnectionPool pool;

  OrderDao(ConnectionPool pool) {
    this.pool = pool;
  }
}

class InvoiceDao {
  final ConnectionPool pool;

  InvoiceDao(ConnectionPool pool) {
    this.pool = pool;
  }
}

@Configuration
class FullConfig {
  @Bean
  ConnectionPool pool() {
    return new ConnectionPool();
  }

  @Bean
  OrderDao orderDao() {
    return new OrderDao(pool());     // intercepted: returns the pool bean
  }

  @Bean
  InvoiceDao invoiceDao() {
    return new InvoiceDao(pool());   // intercepted: returns the same pool bean
  }
}

@Configuration(proxyBeanMethods = false)
class LiteConfig {
  @Bean
  ConnectionPool pool() {
    return new ConnectionPool();
  }

  @Bean
  OrderDao orderDao() {
    return new OrderDao(pool());     // plain Java call: a new ConnectionPool
  }

  @Bean
  InvoiceDao invoiceDao() {
    return new InvoiceDao(pool());   // plain Java call: another new ConnectionPool
  }
}

public class ConfigurationModesApp {
  public static void main(String[] args) {
    show(FullConfig.class);
    show(LiteConfig.class);
  }

  static void show(Class<?> config) {
    try (var context = new AnnotationConfigApplicationContext(config)) {
      System.out.println(config.getSimpleName() + " runs as " + context.getBean(config).getClass().getSimpleName());
      System.out.println("  pool bean #" + context.getBean(ConnectionPool.class).number
          + ", OrderDao uses pool #" + context.getBean(OrderDao.class).pool.number
          + ", InvoiceDao uses pool #" + context.getBean(InvoiceDao.class).pool.number);
    }
  }
}
 OUTPUT of the above code
FullConfig runs as FullConfig$$SpringCGLIB$$0
  pool bean #1, OrderDao uses pool #1, InvoiceDao uses pool #1
LiteConfig runs as LiteConfig
  pool bean #2, OrderDao uses pool #3, InvoiceDao uses pool #4

Explanation of the code

  • FullConfig runs as a generated subclass whose name contains $$SpringCGLIB$$. Its pool() calls were intercepted, so both DAOs share pool #1, the one pool bean.
  • LiteConfig runs as itself, with no subclass. Each pool() call made a new object, so the context ended up with three pools: the bean (#2) and two private copies (#3 and #4) that the container does not manage. That is a real bug if the pool holds database connections.
  • The safe pattern in both modes is to take dependencies as @Bean method parameters instead of calling other @Bean methods, as BeanMethodsApp did. Lite mode is then perfectly safe, and it starts slightly faster because no subclass is generated - which is why Spring Boot's own auto-configuration classes use proxyBeanMethods = false.
  • Because of the generated subclass, a full @Configuration class must not be final, and its @Bean methods must not be private or final.

Combining configuration classes with @Import

Larger applications split their configuration into several classes - one for persistence, one for messaging, and so on. @Import pulls other configuration classes into one, so the context can still start from a single class -
Splitting configuration into classes and combining them with @Import
//ImportApp.java
package com.javabrahman.spring.beans.imports;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

record DataSourceSettings(String url) {
}

record MailSettings(String host) {
}

@Configuration
class PersistenceConfig {
  @Bean
  DataSourceSettings dataSourceSettings() {
    return new DataSourceSettings("jdbc:postgresql://localhost/shop");
  }
}

@Configuration
class MessagingConfig {
  @Bean
  MailSettings mailSettings() {
    return new MailSettings("smtp.example.com");
  }
}

@Configuration
@Import({PersistenceConfig.class, MessagingConfig.class})
class AppConfig {
}

public class ImportApp {
  public static void main(String[] args) {
    try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
      System.out.println(context.getBean(DataSourceSettings.class));
      System.out.println(context.getBean(MailSettings.class));
    }
  }
}
 OUTPUT of the above code
DataSourceSettings[url=jdbc:postgresql://localhost/shop]
MailSettings[host=smtp.example.com]

Explanation of the code

  • The context starts from AppConfig only, but @Import registers the beans of PersistenceConfig and MessagingConfig too.
  • The beans are recordsClick to read: records - any Java object can be a bean.
  • Component scanning would also have found the two configuration classes, since @Configuration is a @Component. @Import is for configuration that is not in a scanned package, such as configuration shipped in a library, and for making dependencies between configuration classes explicit. Spring Boot's auto-configuration is, at heart, a large and conditional version of this idea.

When to use which

  • Your own classes: annotate them with @Service, @Repository, @Controller or @Component, and let component scanning find them. This keeps the bean declaration next to the code.
  • Library classes - clocks, HTTP clients, object mappers, data sources: declare them with @Bean methods, because you cannot annotate code you do not own.
  • Beans that need construction logic - choosing an implementation, reading settings, calling a builder: use @Bean methods, where that logic is plain Java.
  • Configuration classes: group related @Bean methods by area, take dependencies as method parameters, and combine the classes with scanning or @Import.

Summary

In this tutorial we first saw the two ways to declare a bean: annotating the class, or writing a @Bean factory method. We used @Component and the stereotypes @Service and @Repository, and saw how default and custom bean names are chosen. We then used component scanning across packages, with a filter that excluded one package. Next we declared beans for JDK classes with @Bean methods, and saw why full @Configuration classes intercept calls between @Bean methods while lite mode does not. Lastly, we combined configuration classes with @Import and summed up when to use each approach. The next tutorial looks at how the container passes beans to each other: constructor, setter and field injectionClick to read tutorial: Spring Dependency Injection: Constructor vs Setter vs Field.

Spring Beans: @Component, @Bean and @Configuration - JavaBrahman