1. From DI to the container
When you first do DI properly, through a constructor, you get a pleasant sense of order: dependencies become explicit, the code is easier to read, tests are easier to write. And then comes the question that sometimes makes beginners’ left eye twitch: “Okay... so who creates these objects now and passes them into the constructor?” In a small example, your main() did that, but in a real application, “main as the assembler of the whole world” quickly turns into a huge monster.
Spring solves this problem with a special container object that becomes the application’s “assembly shop.” It knows which objects need to be created, in what order, which ones depend on others, and how to give you a ready-made, interconnected set of components. In Spring, this container is usually represented by the ApplicationContext interface.
There is one important idea to keep in mind: DI by itself does not “create” dependencies. DI is a style, while the container is the mechanism that makes this style work at the scale of the whole application.
What is a bean
The word bean sounds as if Spring were trying to sell us beans as an architectural pattern. In practice, it is much simpler: a bean is an object whose life is managed by Spring. Yes, technically it is just an ordinary object in JVM memory. But conceptually it differs because it is created, stored, and wired not by your new, but by the container.
Think of the difference between “I brewed my own coffee in the kitchen” and “I went to a coffee shop, and they made coffee according to the house rules.” The coffee is the same object, but the processes around it are different: who prepares it, who is responsible for quality, where the information is stored, what to do if something is missing. A Spring bean is “coffee from a coffee shop”: you ask, the container gives it to you, and it is also responsible for how that object came into existence.
To avoid confusion, it helps to keep this little table in mind:
| Term | In simple words | In our course context |
|---|---|---|
| Java object | The result of new (or a factory) | Can exist anywhere |
| Bean | A Java object created and controlled by Spring | Used for services, repositories, configuration, infrastructure |
| Container | A “smart storage” + object factory + dependency assembler | In Spring, this is ApplicationContext |
And one more important detail: not every class in a project is a bean. Only what the container knows how to create and why to keep becomes a bean. For your own classes, that knowledge appears either through component annotations or through explicit configuration. For now, we only need to understand the idea of a “managed object.”
2. ApplicationContext as the center of Spring
If you had to choose one anchor word for understanding Spring, honestly, it is not “annotation” but context. ApplicationContext is Spring’s main container interface, and it is what turns a set of classes into a real working application. It stores a “bean catalog,” knows how to create them, wire them with dependencies, and hand them back on request.
If we simplify it into a practical picture, ApplicationContext is a mission control center. It does not do the business logic for you — that is your job — but it organizes the space: who exists, who depends on whom, who is created when, who is ready to work when. When a Boot application starts, the key event is not “main() finished,” but “the ApplicationContext came up.”
The ApplicationContext has many capabilities — in Spring Core it really is a multitasker — but for this lecture we care about four:
1) it contains beans (managed objects),
2) it builds the dependency graph (connects beans to each other),
3) it manages the life cycle of these objects at a basic level (create → prepare → make available),
4) it serves as an access point to the Spring world (but it is important not to turn this into a “global variable”; we will talk about that later).
To see this concretely, let’s look at where the context appears in a Boot application.
3. Where does ApplicationContext come from?
At the start of a Boot application, there is a ritual line that many people type automatically without really understanding its meaning. We are going to make it a little less magical now: SpringApplication.run(...) returns the context. In other words, you literally get an object that represents the container of the whole application.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class CatalogServiceApplication {
public static void main(String[] args) {
// Start Spring Boot and get the "container" with all application beans
ApplicationContext context =
SpringApplication.run(CatalogServiceApplication.class, args);
// Just a demo: which specific context class was started
System.out.println(context.getClass().getSimpleName()); // ...ApplicationContext (example)
}
}
There is a lot happening inside run(...), but for this point we only need one fact: Boot does not start “a set of annotations”; it starts a container.
And here is a very telling trick. In plain Java, if you write new twice, you get two different objects. In Spring, if you ask for the same bean twice, you usually get the same object, because the container stores it internally.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class CatalogServiceApplication {
public static void main(String[] args) {
// Start the context — after this, beans can be requested from the container
ApplicationContext context =
SpringApplication.run(CatalogServiceApplication.class, args);
// Get the same bean twice (by type)
CatalogServiceApplication a = context.getBean(CatalogServiceApplication.class);
CatalogServiceApplication b = context.getBean(CatalogServiceApplication.class);
// For a singleton bean (by default), this will be the same object
System.out.println(a == b); // true
}
}
It is important not to draw the wrong conclusion here. CatalogServiceApplication is available from the context as the Boot application’s configuration entry point. That does not mean that any project class automatically becomes a bean: for ordinary components, the container must either discover them as components or receive explicit registration.
This example matters not because of the specific application class, but because of the feeling: the context is where an object lives as “a single entity of the application,” not as a random new somewhere in the code.
4. The dependency graph
The word “graph” sounds like something from mathematics, after which many people feel like quietly leaving the lecture hall and pretending they were never here. But here, a graph is just a useful metaphor. A dependency graph is a diagram of “who depends on whom,” where the nodes are beans and the arrows are dependencies, usually what comes into a constructor.
Let’s take the same tiny catalog-service graph. The service has a repository contract, and a controller is easy to build on top of the service — that already shows the whole dependency chain:
import java.util.List;
interface CourseCatalogRepository {
List<String> findAllSlugs();
}
class InMemoryCourseCatalogRepository implements CourseCatalogRepository {
@Override
public List<String> findAllSlugs() {
// Repository: data source (for now, a hypothetical one, without a real DB)
return List.of("spring-boot", "spring-core");
}
}
class CourseCatalogService {
// Service: business logic, depends on the repository
private final CourseCatalogRepository repository;
// DI through constructor: dependency is explicit
CourseCatalogService(CourseCatalogRepository repository) {
this.repository = repository;
}
}
class CourseCatalogController {
// Controller: entry point (for example, HTTP), depends on the service
private final CourseCatalogService service;
// DI through constructor: dependency is explicit
CourseCatalogController(CourseCatalogService service) {
this.service = service;
}
}
If you describe the dependencies in words, it looks like this: controller depends on service, service depends on repository. That is the whole “scary graph.” It can be drawn like this:
flowchart TD
Controller[CourseCatalogController] --> Service[CourseCatalogService]
Service --> Repo[CourseCatalogRepository]
What does the Spring container do? It takes this picture and acts as a “smart assembler”: it understands that to create CourseCatalogController, it must first create CourseCatalogService, and to create CourseCatalogService, it must first create CourseCatalogRepository.
Manually, it would look the same, just without the container:
// Manual assembly of the dependency graph without a container
CourseCatalogRepository repository = new InMemoryCourseCatalogRepository();
CourseCatalogService service = new CourseCatalogService(repository);
CourseCatalogController controller = new CourseCatalogController(service);
Spring does the same thing, but at the level of the whole application and automatically, once you register the beans correctly — how exactly that works is the next lecture about component scanning. And that is why ApplicationContext is so important: it is not just a “storage,” it is also a mechanism for building and holding this graph.
5. ApplicationContext as a Map
For beginners, it is often easier to start with a very down-to-earth model: “The context is like a Map, where the key is a name or a type, and the value is an object.” This model is not complete, but it is useful as a first step to stop fearing the abstraction.
Imagine this pseudo-code — not Spring code, just the idea:
// Pseudo-example: not Spring code, but intuition
Map<String, Object> container = new HashMap<>();
// "Register" objects in the container under names
container.put("repository", new InMemoryCourseCatalogRepository());
// Pull out a dependency and pass it into the constructor manually
container.put(
"service",
new CourseCatalogService((CourseCatalogRepository) container.get("repository"))
);
This is already somewhat like a container, but Spring has several important things on top of this idea. It can calculate the creation order by itself, can perform injection by type, can create objects lazily or eagerly, can track the life cycle, and can store bean metadata. In other words, ApplicationContext is a “smart container,” not just a warehouse.
But the good news is that for basic understanding, right now you only need this: beans live in the context, and that is where Spring takes dependencies for DI. We will add everything else gradually, otherwise we would swap demystification for a new kind of mysticism.
6. getBean(...): a tool and an anti-pattern
When you learn that ApplicationContext has a getBean(...) method, a temptation appears: “Oh! Then I can grab the context anywhere and pull out whatever I want.” And that is exactly where it is important to stop in time, because that is how the anti-pattern called Service Locator is born — the container becomes a “global directory” — and the code becomes implicit and harder to test again.
It is better to think like this: in application code, dependencies should come through the constructor, not be fetched from the context. The context is infrastructure, not a “librarian in every class.”
Still, getBean(...) can be useful, especially when you are just starting to learn. It is a good tool for:
1) quickly checking that a bean really exists,
2) demonstrations and experiments in main(),
3) some test scenarios where you explicitly verify the wiring.
Here is an example of “normal” use in main() as a teaching microscope, not as the basis of the architecture:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import java.util.Arrays;
@SpringBootApplication
public class CatalogServiceApplication {
public static void main(String[] args) {
// Start the context to see which beans are registered in it
ApplicationContext context =
SpringApplication.run(CatalogServiceApplication.class, args);
// Get the names of all bean definitions (definition, not necessarily already created objects)
String[] names = context.getBeanDefinitionNames();
Arrays.sort(names);
// Just a demo: print the first bean after sorting
System.out.println("First bean = " + names[0]); // First bean = ... (example)
}
}
And here is an example of an approach that looks like it “works,” but from an architectural point of view is a step backward:
import org.springframework.context.ApplicationContext;
class BadStyleService {
private final ApplicationContext context;
BadStyleService(ApplicationContext context) {
// The context arrived as a dependency, but then the "magic" begins
this.context = context;
}
void doWork() {
// The dependency is hidden: from the class code it is unclear what it really needs
Object something = context.getBean("something");
System.out.println(something);
}
}
The problem here is the same as with new inside a class: dependencies become hidden. Only now, instead of new, we have a “magic chest” called context.getBean(...). So the rule for the next few lectures is simple: do not pass the context into application classes; make dependencies explicit.
7. Dependency graph build errors
Spring is quite kind — by framework standards, of course. If it cannot assemble the application, it fails immediately and tells you fairly honestly what it did not like. It is like a strict teacher: unpleasant, but useful — better now than in production two hours after release.
The most common beginner mistake sounds like this: “I wrote a class, so Spring knows about it.” No, it does not. If a class is not registered as a bean, the container cannot return it and cannot inject it as a dependency.
In that case, you will see an error like “no such bean” — in essence NoSuchBeanDefinitionException. For example, this code, if CourseCatalogService did not become a bean, will fail at startup:
// Try to get a bean by type.
// If the container does not know about this type, an exception will be thrown here.
CourseCatalogService service = context.getBean(CourseCatalogService.class);
// throws exception if the bean is not registered
Another common situation: “I have a type, but there are several matching beans.” Then the container does not guess “by eye” — and rightly so — but reports ambiguity, in essence NoUniqueBeanDefinitionException. We are not covering how this is solved yet: there will be @Primary, @Qualifier, and other mechanisms later. What matters now is the idea: the container is not a magician and not a psychic. It assembles the application according to the rules and clearly says when the rules are not enough.
And one more important psychological point. A startup failure in a Boot application does not mean “everything is broken forever”; it is just a signal that “the container could not build the graph.” Often that is even more useful than if everything had “started somehow” and then quietly worked incorrectly.
8. Mini context diagnostics
Before we get to the project’s “own” beans, it is useful to be able to look at the context as a living object. That removes fear: you see that the container is not a black hole, but a perfectly observable structure.
The simplest trick is to look at the number of beans and make sure that there is indeed a lot going on, even if you have written almost nothing yet:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class CatalogServiceApplication {
public static void main(String[] args) {
// Start the application and get the context
ApplicationContext context =
SpringApplication.run(CatalogServiceApplication.class, args);
// How many bean definitions the container knows about (including Spring/Boot infrastructure)
System.out.println("Beans = " + context.getBeanDefinitionCount()); // Beans = 100+ (example)
}
}
Do not be alarmed by the number. Most of those beans are Spring/Boot infrastructure, and that is normal: the framework brings a lot of ready-made parts so you do not have to write them manually. Later, when we talk about starters and auto-configuration, you will start recognizing “who all these people are.”
Another neat trick is to check whether a bean exists at all without fetching it directly:
// Check whether a bean exists by name (the name depends on Spring's naming rules)
boolean exists = context.containsBean("catalogServiceApplication");
// Demo: the container can answer "yes/no" without creating and returning the object itself
System.out.println(exists); // true (example for the standard name)
There are some nuances with bean naming — that is a separate topic — so treat this example as a demonstration of capabilities, not as a “hard contract.” The main thing right now is that you understood: the context can be “prodded” and asked questions.
9. Typical mistakes when working with ApplicationContext
Mistake #1: thinking that any class in the project automatically becomes a bean.
It is very easy to confuse “I wrote a class” with “the container now knows how to create it.” Spring works only with those objects that are registered as beans. If the container does not know about your class, it cannot inject it as a dependency and cannot return it through getBean(...).
Mistake #2: turning ApplicationContext into a global variable and carrying it through the whole codebase.
Sometimes it seems convenient to pass the context into services and fetch dependencies where needed: “oh, I need one more object here.” But this kills the transparency of dependencies and takes us back to the problems from the first lecture: the code becomes hard to read and even harder to test. In a proper style, dependencies are visible in the constructor.
Mistake #3: actively using getBean(...) in business logic.
getBean(...) is a useful tool for diagnostics, prototypes, and rare infrastructure cases. But if your service calls context.getBean(...) at every step, you are effectively writing an application in the service locator style and hiding the architecture in runtime calls. As a result, it is unclear what exactly the class needs to work, and errors appear later.
Mistake #4: confusing “the context came up” with “the application is actually ready to work.”
At this stage of the course, we simplify the picture, but it is important to understand the basic idea: bringing up ApplicationContext is about creating and wiring beans. Real applications often have additional readiness steps — data loading, configuration checks, warm-up, etc. We will get to those topics later, but even now it is useful not to think that “it started, so everything is perfect.”
Mistake #5: fearing startup errors and trying to “silence” them instead of understanding them.
When Spring fails at startup, it usually means one of two things: a bean is missing or there is ambiguity — or some other problem in the dependency assembly. The worst path is trying to “work around” it with random edits. It is much more useful to read the error message as a hint: the container is directly telling you which piece of the dependency graph it could not build.
GO TO FULL VERSION