1. Bean — a container-managed object
After the first launch of the Spring context, almost everyone has the same desire: “Oh, the container can create objects — so let’s entrust it with everything!” The impulse is understandable — about like buying a power drill and then starting to stir soup with it. In this section, we will calmly figure out what a bean is, how it differs from an ordinary object, and why the word “bean” says nothing about a class being “cool”, but rather about the role of the object in assembling the application.
In Spring, a bean is a specific object (instance) that is created by the container and managed by it (ApplicationContext). The container has very specific plans for this object: create it at the right moment, inject dependencies, later call lifecycle callbacks, and generally treat it as part of the application’s “regular structure”.
Let’s immediately lock in an idea that usually makes beginners breathe easier: a bean does not have to be a special class. It can be an ordinary POJO — with no inheritance from Spring classes, no magic, no SomethingAware interfaces, and no “extends FrameworkMonster”. A bean is not a “type of class”, but a status of an object: the container knows about it and manages it.
If we allow ourselves a very everyday analogy, the container is the “HR department”, and beans are “staff employees”. The container knows who reports to whom (dependencies), whom to hire first (creation order), and who is actually needed so the application can work. Domain objects like Order, on the other hand, are not employees but “documents” that employees create and process during work. HR does not create a separate personnel file for every document.
A useful observation that will come in handy today: the container manages beans through BeanDefinition. That is, before an object becomes a bean, its description appears in the container: “this class (or factory method), these dependencies”. And if an object is created via new directly in your code, the container simply does not know about it — and that is normal.
A small example at the level of “feeling it with your hands”:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
// The context is a "factory" and a "catalog" for beans
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// This object is created by the container and lives under its management
OrderPlacementService service = context.getBean(OrderPlacementService.class); // bean
// This object we create ourselves: it is data for a specific scenario
Order order = new Order("o-1", OrderStatus.NEW); // ordinary object
System.out.println(service.getClass().getSimpleName()); // OrderPlacementService
System.out.println(order.status()); // NEW
}
Here, service comes from the container — it is part of the “application skeleton”. And we create order ourselves: it is the specific data of a specific scenario. And Spring does not suffer any inner pain from that — it is not jealous.
2. The container boundary and choosing beans
The practical rule here is simple: not everything should be a bean. But after the first approving nod, hands still reach to put @Bean on everything that moves, and on some things that do not, “just in case”. In this section, we will introduce the concept of the container boundary: it helps separate the “application structure” from the “scenario data” and avoids turning configuration into a pile of random entities.
The container boundary answers the question: which objects are “application infrastructure” and which are “the contents of its work”. The Spring container is ideal for the first type of objects: they are relatively stable, reused by many parts of the code, and often have dependencies. The second type of objects is what everything is done for: domain entities, commands, calculation results, temporary structures. They appear, disappear, multiply by the thousands — and should live calmly outside the container.
Why this matters not only philosophically, but also in practice:
If you hand everything over to the container, the configuration quickly turns into a catalog of random entities. It becomes harder to understand what here is the “application framework” and what is just data. And pretty soon you will run into objects that cannot honestly be made beans without awkward hacks, because they need data known only at runtime in a scenario (for example, customerId, a list of items, the current date in a report, and so on).
If, on the contrary, there are too few beans and you keep creating services manually, you will slide back to the pain of the early days: dependencies will start hiding again, the composition root will spread out, and swapping implementations will become unpleasant.
In our ContextFlow project, we want that golden middle: the container manages services and infrastructure, while domain objects live as normal Java objects. Then the project remains understandable: Spring helps assemble and maintain the object graph, but does not try to be an “operating system for every new”.
To reinforce the idea, here is a small table with the “human” meaning:
| Object type | Example in ContextFlow | How long does it live? | Should it be a bean? | Why |
|---|---|---|---|---|
| Service / use-case | OrderPlacementService | Long time (as long as the application lives) | Yes | It is part of the framework, has dependencies, and is convenient to assemble once |
| Infrastructure | InMemoryOrderStore | Long time | Yes | It is the “outside world” for the business, convenient to swap and configure |
| Scenario entry point | ScenarioRunner | Long time | Yes | It is the scenario orchestrator, and it needs ready-made dependencies |
| Domain entity | Order | As scenarios happen | No | There are many of them; they are data, not infrastructure |
| Command | CreateOrderCommand | Instantly / until the call ends | No | It is a “package of input data” created at the moment of action |
| Intermediate result | BigDecimal totalAmount | Instantly | No | It is a calculation, not an application component |
Note: the word “long” here is not about the object being immortal, but about the fact that the object is not tied to one specific order. It is needed as a mechanism, not as data.
3. Beans in ContextFlow: services and infrastructure
When we say “a bean is a managed object”, the immediate question is: “Okay, so who exactly do we hire?” In this section, we will apply the boundary concept to our ContextFlow and list typical candidates for beans: services, entry points, ports (interfaces), and their implementations. What interests us now is a single logic: what exactly makes sense for the container to assemble by itself.
From an architectural point of view, ContextFlow at this stage is similar to a small console application with a service layer: there is a scenario (create an order), there is a service that orchestrates it, and there are infrastructure dependencies (storage, audit, notifications, ID generator). All these things are “mechanisms” that are convenient to assemble once and then reuse.
Typical list of beans at the current stage of the project looks like this:
ScenarioRunner — because it is the entry point of our application. It should not manually assemble services: its job is simply to start up already ready.
OrderPlacementService (and other services) — because it is a business orchestrator that depends on ports and infrastructure.
OrderStore, AuditWriter, NotificationSender, OrderIdGenerator — because these are points of variability and external dependencies. Today these are simple in-memory and console-based implementations; later in the course they will still change (in different profiles, configurations, etc.), and the Spring container is exactly for that.
It is useful to keep this larger picture in mind going forward: ContextFlowApplication -> ScenarioRunner -> OrderPlacementService -> ports and infrastructure, while commands and domain objects remain ordinary Java code.
Let us show this with a small live fragment. The main method for a Spring Core application is almost perfect in its simplicity: create a context, take one starting bean, and run it.
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ContextFlowApplication {
public static void main(String[] args) {
// At application startup, we create the context: it will assemble beans from the configuration
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Take the scenario "entry point" as a ready-made bean
ScenarioRunner runner = context.getBean(ScenarioRunner.class);
runner.run(); // Start the business scenario
}
}
}
Notice that main does not do business work here. It does not build Order, does not calculate discounts, does not choose notification channels. Its job is to say: “Container, assemble the application. I need a starting point.”
Now, a configuration fragment (we are not trying to make it ideal; right now the important thing is to understand the principle itself):
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration // We tell Spring: this is a configuration class, from it we need to assemble beans
public class AppConfig {
@Bean // Infrastructure bean: a storage "mechanism" that is convenient to swap
public OrderStore orderStore() {
return new InMemoryOrderStore();
}
@Bean // Entry-point bean: the container will pass the needed service here itself
public ScenarioRunner scenarioRunner(OrderPlacementService service) {
return new ScenarioRunner(service);
}
}
Yes, this still lacks @Bean for OrderPlacementService and other dependencies — in a real project they would certainly be there. But even this fragment shows the main thing: beans are the “nodes of the framework”, not the “data of the scenario”.
It is also useful to see that ScenarioRunner itself is an ordinary class. It does not have to know that Spring creates it. Its dependency is visible in the constructor — that is the DI approach we have already learned.
public class ScenarioRunner {
private final OrderPlacementService orderPlacementService;
public ScenarioRunner(OrderPlacementService orderPlacementService) {
// The dependency is passed from outside (by the container) — that is DI
this.orderPlacementService = orderPlacementService;
}
public void run() {
System.out.println("Scenario started"); // Scenario started
// Next will be the creation of ordinary scenario objects (commands, domain entities)
}
}
And that is already an important victory: the starting object is created by the container, and then it launches the scenario with ordinary Java code.
4. What not to make beans: domain entities and commands
The most common beginner mistake is to decide: “if Spring is a container of objects, then any object should live in the container.” In this section, we will calmly look at the other half of the picture: which objects in ContextFlow are logically better left as ordinary Java objects. The point is not to “forbid” something, but not to break the design and not to turn the container into a warehouse of temporary things.
In ContextFlow, the domain model is intentionally “thin”: Order, OrderItem, Customer, order statuses, commands for creation/cancellation. These objects exist because the business scenario operates on them as data. They should not have dependencies on OrderStore or NotificationSender — otherwise the domain model starts leaking infrastructure, and that is almost always painful.
Here is an example of a command. It is a typical “bag of data” at the input of a scenario:
import java.util.List;
// A command is the input data for a scenario (usually created at the moment a use-case is called)
public record CreateOrderCommand(
String customerId,
List<OrderItem> items
) {
}
A command does not ask the container for help. It simply stores the information that the user or scenario passed into the use-case. Therefore, it is natural to create it where we already have concrete data: in ScenarioRunner or later inside a test.
The same goes for the domain entity. It describes an order as a fact:
import java.time.Instant;
// A domain entity is "business data", not an infrastructure component
public record Order(
String id,
String customerId,
OrderStatus status,
Instant createdAt
) {
}
In a real project, an order will have more fields, but the essence does not change: this is data. It is not “configured by the container” — it appears at the moment the scenario is processed.
And here is the important point: there will be many such orders. If we have 100 orders, there will be 100 Order objects. The container should not know about each one and should not try to “hold” them. The container holds the mechanisms that know how to work with orders.
What this looks like in a scenario:
import java.util.List;
public class ScenarioRunner {
private final OrderPlacementService orderPlacementService;
public ScenarioRunner(OrderPlacementService orderPlacementService) {
this.orderPlacementService = orderPlacementService;
}
public void run() {
// The command is created on the spot, because the data is known only at scenario time
CreateOrderCommand cmd = new CreateOrderCommand(
"cust-1",
List.of(new OrderItem("PEN", 2))
);
// The service (bean) processes the command and returns a domain result (an ordinary object)
Order created = orderPlacementService.placeOrder(cmd);
System.out.println(created.status()); // NEW
}
}
CreateOrderCommand and OrderItem are created via new or the record constructor directly in the scenario. That is normal, that is correct, and that is not a “return to manual assembly”. Manual assembly is when you manually assemble the application framework (services and infrastructure). Creating domain objects during work is ordinary life for an application.
5. How beans and ordinary objects are connected in a scenario
Beans and ordinary objects are connected very simply: they meet in the same scenario, but they play different roles. The container creates services, the scenario creates commands and domain objects, the service uses infrastructure beans and returns a domain result. Our goal is to see in this not two accidentally touching worlds, but one normal flow.
First, the overall diagram. It is useful to keep it in mind when you do not understand “why this object is not in the container”.
flowchart LR
subgraph C[Spring ApplicationContext]
SR["ScenarioRunner (bean)"]
OPS["OrderPlacementService (bean)"]
GEN["OrderIdGenerator (bean)"]
STORE["OrderStore (bean)"]
end
SR -->|creates| CMD["CreateOrderCommand (plain)"]
CMD --> OPS
OPS --> GEN
OPS --> STORE
OPS -->|creates| O["Order (plain)"]
STORE -->|stores| O
The main thing is visible here: the container manages the “mechanisms” (left block), while CreateOrderCommand and Order live “outside” as data.
Now a piece of the service. It is a bean because it has dependencies and is part of the framework:
import java.time.Instant;
public class OrderPlacementService {
private final OrderIdGenerator idGenerator;
private final OrderStore orderStore;
public OrderPlacementService(OrderIdGenerator idGenerator, OrderStore orderStore) {
// Dependencies of the service (bean) come from outside — from the container
this.idGenerator = idGenerator;
this.orderStore = orderStore;
}
public Order placeOrder(CreateOrderCommand cmd) {
// The service creates a domain object: this is a "product of work", not a bean
Order order = new Order(idGenerator.nextId(), cmd.customerId(), OrderStatus.NEW, Instant.now());
// Use the infrastructure bean to save it
orderStore.save(order);
return order;
}
}
There is a very telling moment here: the service creates the domain object itself. That is not a reason to make Order a bean. On the contrary: it is exactly what shows that domain objects are the product of the service’s work.
And now, a piece of configuration that shows the container only needs to know about the “mechanisms”:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration // Configuration that describes the application framework
public class AppConfig {
@Bean // Infrastructure bean: ID generator
public OrderIdGenerator orderIdGenerator() {
return new UuidOrderIdGenerator();
}
@Bean // Service bean: the container will wire its dependencies itself (gen, store)
public OrderPlacementService orderPlacementService(OrderIdGenerator gen, OrderStore store) {
return new OrderPlacementService(gen, store);
}
}
This config does not have @Bean public Order () — and that is excellent. We are not trying to force the container to “hold the order”. The container stores what is needed to create and save orders.
6. Rules for choosing beans for a beginner
At the start, it is hard to decide based on “feel”: it seems that OrderStore is important, and Order is important, and CreateOrderCommand is important... so everything important must be a bean. In this section, we will collect a few simple questions that help decide whether to make an object a bean or not, without magic and without dogmatism. They are not perfect, but for Junior level they give very good discipline.
The first question is: “Is this a mechanism or data?” If an object is a mechanism (it does something, interacts with other components, has dependencies), then it is a candidate for a bean. If an object is data (it describes a specific order, a specific customer, a specific command), then it should almost always be an ordinary object.
The second question is: “How many such objects will there be during execution?” If there can potentially be many (orders, order items, commands), the container is not needed here: it should not manage thousands of identical instances that are born every second. If an object is usually one per application (storage, ID generator, service), a bean is a good option.
The third question is: “Does the object need data known only at runtime?” If an object cannot be created without values that appear only at scenario time (for example: customerId, a list of OrderItem, createdAt), then this is almost a direct signal that you are not looking at a bean. The container creates beans at application startup, not at the moment “the user decided to buy a pen”.
To avoid staying in abstraction, let us look at a bad but illustrative example. Do not do this:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration // This looks "like a config", but in fact it configures domain data — do not do this
public class BrokenConfig {
@Bean // Anti-pattern: make a domain entity a bean, and with hardcoded data on top of that
public Order order() {
return new Order("o-1", "cust-1", OrderStatus.NEW, java.time.Instant.now());
}
}
Why this is a bad idea even without Spring subtleties:
Such an Order turns into “application structure”, although it should be “data”. It is created when the context starts — before we have even decided to place an order. There will be only one instance, although we need many orders. And on top of that, you have effectively hardcoded customerId, while Instant.now() at application startup is a whole separate kind of surrealism: the order is “created” at program launch, even if the user has not done anything yet.
A good counterexample is to make not Order a bean, but the service that knows how to create it, and the dependencies that service needs. Then the system remains natural: Spring manages the mechanisms, and the business scenario manages the data.
To make this easier to remember, you can look at container boundary as a simple rule: “inside the container — what helps us work”, “outside — what we are working on”.
7. Typical mistakes when choosing beans
Mistake #1: trying to make every domain entity a bean (“let Spring create my Orders”).
This usually comes from the desire to “use Spring to the maximum”. In practice, you get a strange configuration where domain data becomes part of the application’s startup structure, and then also tries to live as one shared instance. A domain entity is data for a specific scenario, and it is perfectly fine for it to appear through new inside services or scenario classes.
Mistake #2: keeping temporary scenario data inside a bean as a class field.
This looks harmless: “well, I’ll save the last created order in a field, just in case”. But very quickly this turns into hidden state that does not belong to the “mechanism”. A bean should be a “service”, not a “folder with current documents”. Temporary data is better kept in local variables of methods, in returned objects, or in explicitly dedicated storage structures (for example, in OrderStore).
Mistake #3: confusing an “important object” with a “bean”.
Order is more important than any OrderPlacementService in terms of domain meaning, but that does not make it a bean. An object becomes a bean not because it is important, but because it is part of the application framework: it needs to be assembled, wired with dependencies, and kept in working order.
Mistake #4: turning the container into a universal new, and getBean into the everyday way to create objects.
At the start of learning, it is tempting to write: “if I need an object, I’ll take it from the context”. The problem is that the vast majority of objects in an application should be created by ordinary Java code: commands, domain entities, calculation results. The context is great for infrastructure and services, but if you start going to it for every “single-use” object, the code becomes harder to read and maintain.
Mistake #5: thinking that “if an object is not a bean, then Spring is not needed”.
Sometimes students, after splitting objects into beans and ordinary objects, think: “So half my objects are created with new, which means Spring did not help much.” That is a trap. Spring takes over the most unpleasant part: assembling and maintaining the object graph of services and infrastructure. And the fact that domain objects are created manually is not a drawback, but a normal, healthy application model.
GO TO FULL VERSION