DEV Community

Ankit Verma
Ankit Verma

Posted on

ApplicationContext vs BeanFactory

Every article so far has talked about "the container" — the factory that builds and wires your beans. In Spring's actual code, that factory has two names. BeanFactory and ApplicationContext are both interfaces that mean "the container," and sooner or later you meet both. SpringApplication.run(...) hands you an ApplicationContext. Older tutorials, framework internals, and the bottom of stack traces talk about BeanFactory. The Javadoc of each keeps pointing at the other.

So which one is the container? Both — at different levels. BeanFactory is the minimal core: just the machinery that stores recipes and builds beans. ApplicationContext is built on top of it: the same factory, plus everything a real application needs around it. This article walks through what each one actually is, and the one difference that fails silently — the reason almost nobody should ever touch the bare factory.

The container's contract is an interface

Strip the factory from the first article down to its essence and ask: what is the smallest thing it must be able to do? Hold the recipes, build beans from them, and hand them out when asked. Spring writes that contract down as an interface, and it is startlingly small:

public interface BeanFactory {
    Object getBean(String name);
    <T> T getBean(Class<T> requiredType);
    boolean containsBean(String name);
    boolean isSingleton(String name);
    // ... a few more lookup variants, nothing else
}
Enter fullscreen mode Exit fullscreen mode

Read what is there: lookups. Nothing about annotations, nothing about configuration files, nothing about proxies. That is the point. BeanFactory is the minimal contract for a bean container — store bean definitions, build each bean on demand, resolve its dependencies, hand it out.

One behavioral detail is worth flagging now, because it becomes a real difference later: a plain BeanFactory is lazy. It does not build anything up front. A singleton gets built the first time somebody asks for it, and not a moment before.

Driving the bare factory by hand

The contract has a real, concrete implementation inside Spring called DefaultListableBeanFactory. You can use it directly, and doing so once is the fastest way to understand what it is — and what it isn't:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();

factory.registerBeanDefinition("gateway",
        new RootBeanDefinition(PaymentGateway.class));
factory.registerBeanDefinition("orders",
        new RootBeanDefinition(OrderService.class));

OrderService orders = factory.getBean(OrderService.class);
Enter fullscreen mode Exit fullscreen mode

This is the factory from the first article, operated manually. You hand it recipe cards — the bean definitions from before — and when you call getBean, it looks at OrderService's constructor, sees it needs a PaymentGateway, builds that first, then builds and caches the service. Dependency resolution, bottom-up construction, singleton caching: all there, in a dozen lines.

Which raises the obvious question. If this little object already builds and wires beans, why does every real Spring application use something bigger?

What the bare factory quietly doesn't do

Try feeding the bare factory a class written the way the previous articles taught:

@Component
class OrderService {
    @Autowired
    private PaymentGateway gateway;

    @PostConstruct
    void ready() { System.out.println("wired and ready"); }
}
Enter fullscreen mode Exit fullscreen mode

Register it, fetch it. No error. No warning. But gateway is null, and "wired and ready" never prints. Every annotation on that class was silently ignored.

Here is why, and it connects straight back to the lifecycle article. Features like @Autowired, @PostConstruct, and the proxy that powers @Transactional are not built into the factory core at all. Each one is implemented by a BeanPostProcessor — the lifecycle hook that gets a crack at every bean as it is built. Field injection is done by one post-processor. Init annotations by another. Proxy wrapping by a third. They are plugins around the factory, not parts of it.

And the bare factory ships with none of them registered. If you insist on using it raw, you have to install each plugin yourself:

factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
// ...and one for @PostConstruct, one for @Transactional proxies, one for...
Enter fullscreen mode Exit fullscreen mode

Forget one, and that feature simply doesn't exist in your application. Nothing fails loudly — fields just stay null, transactions just don't open. A container that ignores your annotations without telling you is a container you don't want to operate by hand. What you want is the version that comes fully staffed.

ApplicationContext: the factory, fully staffed

ApplicationContext is an interface that extends BeanFactory. It is not a rival container — it is a superset, and the inheritance is written right in its declaration:

public interface ApplicationContext extends ListableBeanFactory,
        EnvironmentCapable, MessageSource,
        ApplicationEventPublisher, ResourcePatternResolver {
    // every BeanFactory method, plus all of the above
}
Enter fullscreen mode Exit fullscreen mode

Each parent interface in that list is a service a real application was going to need anyway:

  • ListableBeanFactory — enumerate beans, not just look them up one at a time. This is the machinery behind injecting a List<ShippingRule> of every implementation, the plugin trick from the DI article.
  • EnvironmentCapable — access to properties and profiles, which is what makes @Value("${...}") resolve.
  • ApplicationEventPublisher — publish events to other beans (a concrete example in a moment).
  • MessageSource — translated text for internationalization.
  • ResourcePatternResolver — load files from the classpath or disk with one call.

But the bigger difference is behavioral. An ApplicationContext registers all the standard post-processors automatically. Create one and every annotation just works — injection, init callbacks, proxies, the lot:

ApplicationContext ctx =
        new AnnotationConfigApplicationContext("com.shop");

OrderService orders = ctx.getBean(OrderService.class);
// gateway injected, @PostConstruct ran, proxies applied
Enter fullscreen mode Exit fullscreen mode

The string is the package to scan: the context finds every @Component under com.shop and registers it, no manual registerBeanDefinition needed. And it is the same getBean call as before — it is a BeanFactory, after all. The difference is everything that already happened before you asked.

Eager startup — the difference you can feel

Remember that the bare factory is lazy: nothing is built until first asked for. The ApplicationContext makes the opposite choice. During startup it walks every singleton definition and builds them all eagerly, before the application serves a single request.

That sounds like a detail. It is actually a guarantee about when you find out something is broken.

With a lazy container, a bean with a missing dependency or a broken constructor sits undetected until the first time someone asks for it — which might be a rarely-used endpoint, three days after deploy, in production, on a real user. With eager startup, that same mistake kills the application at boot, on your machine or in CI, with a stack trace pointing at the bad bean.

This is the same fail-fast philosophy the DI article praised when constructor injection turned circular dependencies into boot-time errors. Failing at the safest possible moment is the feature. (When a bean is genuinely too expensive to build up front, @Lazy opts it out one bean at a time — but the eager default is the right one.)

One of the extras, in action: events

The added interfaces aren't decoration; they change how you can structure code. Take ApplicationEventPublisher. Suppose that after an order is placed, an email should go out — but OrderService shouldn't know or care about email. Publish a plain object as an event:

@Service
class OrderService {
    private final ApplicationEventPublisher events;

    OrderService(ApplicationEventPublisher events) {
        this.events = events;
    }

    void placeOrder(Order order) {
        // ...persist the order...
        events.publishEvent(new OrderPlaced(order.id()));
    }
}
Enter fullscreen mode Exit fullscreen mode

Any other bean can listen for it by annotating a method:

@Component
class EmailNotifier {
    @EventListener
    void on(OrderPlaced event) {
        // send the confirmation email
    }
}
Enter fullscreen mode Exit fullscreen mode

The container delivers the event from publisher to every listener. OrderService gained an email feature without ever learning that email exists — and adding an SMS notifier later means adding one new bean, touching nothing. A bare BeanFactory has no idea how to do any of this. The container as event bus only exists at the ApplicationContext level.

So which one do you use?

In application code: always ApplicationContext. In practice you never even construct it yourself: SpringApplication.run(...) builds one and hands it back. Every Spring Boot app you have ever run was an ApplicationContext the whole time.

Under the hood, the relationship is composition, not reimplementation. A running ApplicationContext holds a DefaultListableBeanFactory inside it and delegates every getBean call down to it. The context is a shell of services wrapped around the same little factory you drove by hand earlier. That is why BeanFactory still exists as a separate interface: it is the internal engine and the extension point framework code is written against. You will see the name in Spring's own internals and in stack traces — but app code has no reason to reach for it.

One habit falls out of the layering. When a bean needs one of the context's services, inject the narrow interface, not the whole context. The OrderService above asked for ApplicationEventPublisher, not ApplicationContext — so its signature admits exactly what it uses, and a test can hand it a one-line fake instead of a whole container.

Putting it together

BeanFactory and ApplicationContext are the same container at two levels of dress. BeanFactory is the minimal contract — store definitions, build beans lazily on request — and DefaultListableBeanFactory is its real implementation, the engine at the bottom of every Spring app. ApplicationContext extends it and staffs it: all the standard post-processors registered automatically so annotations actually work, singletons built eagerly so broken wiring fails at boot instead of in production, plus events, properties, messages, and resources.

The bare factory ignores your annotations silently; the context honors them automatically. That single difference decides the question for you: use the ApplicationContext, let Boot create it, and remember that somewhere inside it, the little factory is still doing all the building.

Top comments (0)