Spring BootBeanFactory
Spring Boot

BeanFactory

BeanFactory is the root interface of Spring's IoC container — the most basic form of the container that provides dependency injection and bean management. ApplicationContext extends BeanFactory with enterprise features. Understanding BeanFactory clarifies the foundation that all of Spring is built on and explains the behavior of lazy initialization, bean registration, and the container's core contract.

What Is BeanFactory?

BeanFactory is the root interface of Spring's IoC container hierarchy. It defines the core contract for a Spring container: the ability to hold bean definitions and return configured bean instances on demand. Every Spring container — including ApplicationContext — is a BeanFactory. BeanFactory provides: - Bean retrieval by name, type, or both - Singleton and prototype scope support - Lazy initialization (beans created only when first requested) - Type checking and alias resolution - Dependency injection through factory methods BeanFactory does NOT provide: - Event publication (ApplicationEvent system) - Internationalization (MessageSource) - Resource loading abstraction - Automatic BeanPostProcessor registration - Automatic BeanFactoryPostProcessor registration - AOP integration via auto-proxying ApplicationContext extends BeanFactory and adds all of these missing features. In practice, you always work with ApplicationContext in Spring Boot. BeanFactory appears primarily in framework internals, testing utilities, and legacy code. Understanding it explains why certain Spring behaviors work the way they do.

BeanFactory vs ApplicationContext

The distinction between BeanFactory and ApplicationContext is primarily about what features are available and when beans are initialized. ApplicationContext is recommended for all application code.
Java
// BeanFactory — the minimal container:
// - Lazy initialization (beans created on first getBean() call)
// - No automatic BeanPostProcessor registration
// - No automatic BeanFactoryPostProcessor registration
// - No event publishing
// - No i18n support
// - No resource loading abstraction

// ApplicationContext — the full container (extends BeanFactory):
// - Eager initialization (all singleton beans at startup)
// - Automatic BeanPostProcessor detection and registration
// - Automatic BeanFactoryPostProcessor detection and registration
// - ApplicationEvent publishing
// - MessageSource (i18n)
// - ResourceLoader abstraction
// - EnvironmentCapable (profiles, properties)

// The interface hierarchy:
// BeanFactory
//   └── HierarchicalBeanFactory    (parent-child context support)
//         └── ConfigurableBeanFactory (scope registration, bean aliasing)
//               └── AutowireCapableBeanFactory (autowiring support)
//   └── ListableBeanFactory        (enumerate all beans)
//         └── ApplicationContext   (full container — adds events, i18n, resources)
//               └── ConfigurableApplicationContext (refresh, close, env)
//                     └── AnnotationConfigApplicationContext
//                     └── AnnotationConfigServletWebServerApplicationContext (Spring Boot)

// Key difference — BeanPostProcessor registration:
// With plain BeanFactory: you must register BeanPostProcessors manually
// beanFactory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
// Without this@Autowired doesn't work!

// With ApplicationContext: BeanPostProcessors discovered automatically via component scan
// @Autowired, @PostConstruct, @PreDestroy, AOP proxies — all work automatically

// When to use BeanFactory directly:
// - Writing a lightweight framework or plugin
// - Testing scenarios that need a minimal container
// - Understanding Spring internals
// - Legacy Spring code (pre-2.5)
// For all other cases: use ApplicationContext

BeanFactory API

BeanFactory defines a concise API for retrieving and inspecting beans. ApplicationContext inherits all of these methods and adds more.
Java
// BeanFactory interface methods:
public interface BeanFactory {

    // Retrieve a bean by name:
    Object getBean(String name) throws BeansException;

    // Retrieve a bean by name with type safety:
    <T> T getBean(String name, Class<T> requiredType) throws BeansException;

    // Retrieve a bean by type (fails if multiple candidates exist):
    <T> T getBean(Class<T> requiredType) throws BeansException;

    // Retrieve a bean by type with constructor arguments (for prototype beans):
    <T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

    // Check if a bean with the given name exists:
    boolean containsBean(String name);

    // Check if the named bean is a singleton:
    boolean isSingleton(String name) throws NoSuchBeanDefinitionException;

    // Check if the named bean is a prototype:
    boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

    // Check if the bean matches the given type:
    boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;

    // Get the type of the named bean:
    Class<?> getType(String name) throws NoSuchBeanDefinitionException;

    // Get the aliases for the named bean:
    String[] getAliases(String name);
}

// Using BeanFactory via ConfigurableListableBeanFactory (the implementation):
@Configuration
public class AppConfig {

    // BeanFactory is available as a dependency:
    @Autowired
    private ConfigurableListableBeanFactory beanFactory;

    @Bean
    public CommandLineRunner inspectBeans() {
        return args -> {
            // List all bean definition names:
            String[] names = beanFactory.getBeanDefinitionNames();
            System.out.println("Total beans: " + names.length);

            // Get a specific BeanDefinition (metadata, not the instance):
            BeanDefinition def = beanFactory.getBeanDefinition("userService");
            System.out.println("Scope: " + def.getScope());
            System.out.println("Is singleton: " + def.isSingleton());
            System.out.println("Is lazy: " + def.isLazyInit());
            System.out.println("Bean class: " + def.getBeanClassName());

            // Check parent:
            BeanFactory parent = beanFactory.getParentBeanFactory();
        };
    }
}

ConfigurableListableBeanFactory — The Implementation

In Spring Boot applications, the actual container is a DefaultListableBeanFactory — the primary implementation of BeanFactory. It implements all sub-interfaces and provides the full bean management API.
Java
// Accessing the underlying BeanFactory in a Spring Boot app:
@Component
public class BeanFactoryInspector implements BeanFactoryAware {

    private ConfigurableListableBeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
    }

    public void demonstrateBeanFactory() {

        // ── Bean retrieval ────────────────────────────────────────────

        UserService service = beanFactory.getBean(UserService.class);
        UserService byName = (UserService) beanFactory.getBean("userService");

        // ── Bean definition inspection ────────────────────────────────

        BeanDefinition def = beanFactory.getBeanDefinition("userService");
        String scope = def.getScope();                    // "singleton"
        boolean isLazy = def.isLazyInit();                // false
        boolean isSingleton = def.isSingleton();          // true
        String beanClass = def.getBeanClassName();         // "com.example.UserService"
        String[] deps = def.getDependsOn();               // explicit @DependsOn deps
        ConstructorArgumentValues ctorArgs = def.getConstructorArgumentValues();
        MutablePropertyValues propValues = def.getPropertyValues();

        // ── Scope and type checking ───────────────────────────────────

        boolean isSingletonBean = beanFactory.isSingleton("userService");   // true
        boolean isPrototypeBean = beanFactory.isPrototype("userService");   // false
        boolean matchesType = beanFactory.isTypeMatch("userService", UserService.class);
        Class<?> type = beanFactory.getType("userService");

        // ── BeanPostProcessor registration (manual — only needed with raw BeanFactory):
        beanFactory.addBeanPostProcessor(new CustomBeanPostProcessor());

        // ── Programmatic bean registration ────────────────────────────

        // Register a new BeanDefinition programmatically:
        BeanDefinitionBuilder builder = BeanDefinitionBuilder
            .genericBeanDefinition(SpecialService.class)
            .setScope(BeanDefinition.SCOPE_SINGLETON)
            .addConstructorArgReference("userRepository")
            .addPropertyValue("maxRetries", 3);

        beanFactory.registerBeanDefinition("specialService", builder.getBeanDefinition());

        // Register a singleton instance directly (bypasses normal lifecycle):
        beanFactory.registerSingleton("externalService",
            new ExternalServiceAdapter("https://api.example.com"));

        // ── Dependency resolution ─────────────────────────────────────

        // Autowire an externally created object:
        ExternalObject external = new ExternalObject();
        beanFactory.autowireBean(external);  // @Autowired fields injected into external
    }
}

BeanDefinition — The Blueprint

A BeanDefinition is the metadata object that describes how to create a bean — its class, scope, constructor arguments, property values, lazy flag, and lifecycle methods. Spring reads your annotations and creates BeanDefinitions before instantiating any bean.
Java
// BeanDefinition is Spring's internal metadata for each bean.
// Annotations like @Service, @Scope, @Lazy are all read into BeanDefinition fields.

// Reading BeanDefinition metadata:
@Component
public class BeanDefinitionReader implements BeanFactoryAware {

    private ConfigurableListableBeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory factory) {
        this.beanFactory = (ConfigurableListableBeanFactory) factory;
    }

    @PostConstruct
    public void readDefinitions() {
        for (String beanName : beanFactory.getBeanDefinitionNames()) {
            BeanDefinition def = beanFactory.getBeanDefinition(beanName);

            System.out.printf("Bean: %-40s | Scope: %-10s | Lazy: %s%n",
                beanName,
                def.getScope().isEmpty() ? "singleton" : def.getScope(),
                def.isLazyInit()
            );
        }
    }
}

// Creating BeanDefinitions programmatically — three approaches:

// Approach 1 — BeanDefinitionBuilder (fluent API):
GenericBeanDefinition def1 = (GenericBeanDefinition) BeanDefinitionBuilder
    .genericBeanDefinition(UserService.class)
    .setScope(BeanDefinition.SCOPE_SINGLETON)
    .setLazyInit(false)
    .addConstructorArgReference("userRepository")   // ref to another bean
    .addConstructorArgValue("admin@example.com")    // literal value
    .addPropertyValue("maxRetries", 3)
    .setInitMethodName("initialize")
    .setDestroyMethodName("cleanup")
    .getBeanDefinition();

// Approach 2 — GenericBeanDefinition directly:
GenericBeanDefinition def2 = new GenericBeanDefinition();
def2.setBeanClass(ProductService.class);
def2.setScope(BeanDefinition.SCOPE_PROTOTYPE);
def2.setLazyInit(true);
ConstructorArgumentValues ctorArgs = new ConstructorArgumentValues();
ctorArgs.addIndexedArgumentValue(0, new RuntimeBeanReference("productRepository"));
def2.setConstructorArgumentValues(ctorArgs);

// Approach 3 — RootBeanDefinition (for concrete beans, most efficient):
RootBeanDefinition def3 = new RootBeanDefinition(CacheService.class);
def3.setScope(BeanDefinition.SCOPE_SINGLETON);

// Register and use:
ConfigurableListableBeanFactory factory = ...; // from BeanFactoryAware or context
factory.registerBeanDefinition("userService", def1);
factory.registerBeanDefinition("productService", def2);
factory.registerBeanDefinition("cacheService", def3);

BeanFactoryPostProcessor — Modifying Definitions

BeanFactoryPostProcessor allows you to inspect and modify BeanDefinitions after they are loaded but before any beans are instantiated. This is how @PropertySource resolution and configuration class processing work internally.
Java
// BeanFactoryPostProcessor runs BEFORE any beans are created:
// - Receives the ConfigurableListableBeanFactory
// - Can read and MODIFY BeanDefinitions
// - Cannot create beans (context not yet refreshed for beans)

// Custom BeanFactoryPostProcessor example:
@Component
public class ScopeOverridingBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
            throws BeansException {

        // Change the scope of a specific bean at startup:
        BeanDefinition def = beanFactory.getBeanDefinition("reportGenerator");
        def.setScope(BeanDefinition.SCOPE_PROTOTYPE);
        System.out.println("Changed reportGenerator scope to prototype");

        // Add a property value to an existing bean definition:
        BeanDefinition emailDef = beanFactory.getBeanDefinition("emailService");
        emailDef.getPropertyValues().add("retryCount", 3);

        // Mark a bean as lazy:
        BeanDefinition heavyDef = beanFactory.getBeanDefinition("heavyService");
        heavyDef.setLazyInit(true);
    }
}

// Spring's built-in BeanFactoryPostProcessors:
// PropertySourcesPlaceholderConfigurer — resolves ${...} placeholders
// ConfigurationClassPostProcessor   — processes @Configuration, @Bean, @Import
// EventListenerMethodProcessor      — registers @EventListener methods

// BeanDefinitionRegistryPostProcessor — extends BeanFactoryPostProcessor:
// Can ADD new BeanDefinitions (not just modify existing ones)
@Component
public class DynamicBeanRegistrar implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
            throws BeansException {
        // Register new beans before the context is refreshed:
        for (String tenantId : loadTenantIds()) {
            BeanDefinition def = BeanDefinitionBuilder
                .genericBeanDefinition(TenantDataSource.class)
                .addConstructorArgValue(tenantId)
                .getBeanDefinition();
            registry.registerBeanDefinition("dataSource-" + tenantId, def);
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        // Additional factory-level post-processing if needed
    }

    private List<String> loadTenantIds() {
        return List.of("tenant1", "tenant2", "tenant3");
    }
}

Using BeanFactory in Tests

BeanFactory is useful in integration tests for verifying bean registration, checking scopes, and programmatically manipulating the container.
Java
// Inject BeanFactory in Spring Boot tests:
@SpringBootTest
class BeanFactoryIntegrationTest {

    @Autowired
    private ApplicationContext context;    // ApplicationContext IS a BeanFactory

    @Autowired
    private ConfigurableListableBeanFactory beanFactory;

    @Test
    void allExpectedBeansAreRegistered() {
        assertThat(context.containsBean("userService")).isTrue();
        assertThat(context.containsBean("orderService")).isTrue();
        assertThat(context.containsBean("userRepository")).isTrue();
    }

    @Test
    void userServiceIsSingleton() {
        assertThat(beanFactory.isSingleton("userService")).isTrue();
        // Same instance returned every time:
        UserService a = context.getBean(UserService.class);
        UserService b = context.getBean(UserService.class);
        assertThat(a).isSameAs(b);
    }

    @Test
    void reportGeneratorIsPrototype() {
        assertThat(beanFactory.isPrototype("reportGenerator")).isTrue();
        // Different instance every time:
        ReportGenerator a = context.getBean(ReportGenerator.class);
        ReportGenerator b = context.getBean(ReportGenerator.class);
        assertThat(a).isNotSameAs(b);
    }

    @Test
    void beanDefinitionHasCorrectScope() {
        BeanDefinition def = beanFactory.getBeanDefinition("shoppingCart");
        assertThat(def.getScope()).isEqualTo("session");
    }

    @Test
    void canRegisterAndUseBeanProgrammatically() {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder
            .genericBeanDefinition(TestHelper.class);
        beanFactory.registerBeanDefinition("testHelper", builder.getBeanDefinition());

        TestHelper helper = context.getBean("testHelper", TestHelper.class);
        assertThat(helper).isNotNull();
    }
}