1. Dependencies and the composition root
If yesterday your service could “sneakily” write new HttpClient() and new ObjectMapper() inside itself, then today we try not to do that. And that is correct: the class starts to look like a normal mature component, not like a self-assembled cabinet from the “Build Yourself” store. But every mature component has a downside — it requires someone to create it and connect it to the rest.
This is where beginners often experience a small cognitive dissonance. On one hand, we say: “Do not create dependencies inside classes; pass them through the constructor.” On the other hand, we still have to write new somewhere. There is no way around it: Java still cannot materialize CatalogService out of thin air by the power of thought (although some IDEs behave as if they can).
So the key idea of today's lecture appears: the application must have one clear place where it is “allowed” to assemble objects and connect them into a working system. This place is called the composition root, or, in more everyday language, the application assembly point.
Composition root and new
Composition root is not a magical pattern from the book “Architecture for Those Who Like Suffering.” It is a very simple discipline: all the “wiring” between objects lives in one place, usually near the entry point (in our case, in the app package). Then you open one file and immediately see which major parts the application consists of, and who calls whom.
It is convenient to think like this: business code (services, repositories, clients) should be like appliances — they do the work. And the composition root is the electrical panel, where you connect the appliances to the power grid and to each other. The appliances should not run wires around the apartment themselves. Otherwise, it will be fun — but not for long.
To make it completely down to earth, let us fix a simple rule in a table. It is not about “beauty”; it is about where you will look for bugs and how quickly you can change the application's behavior.
| Place in the project | What is allowed there | Why |
|---|---|---|
| com.example.readlater.app | Create objects with new, choose implementations (mock/real), and wire everything into a working scheme | This is the “assembly zone”: the whole picture is visible here |
| catalog.service, readinglist.service | Do application work, call dependencies, but do not create them inside | Otherwise the service becomes non-reusable and “sticks” to a specific implementation |
| catalog.client, readinglist.repository | Implement a specific dependency (HTTP, storage) | This is the “hardware,” but not the “electrical panel” |
| dto | Only data shape | DTO should not know who creates it or where |
This rule does not forbid you from doing new ReadingStatus(...) (although enum is not created anyway). The point is specifically about infrastructure and application dependencies: HttpClient, ObjectMapper, CatalogClient, CatalogService, and entry-layer controllers/adapters.
2. Object graph: application connections
When you have assembled objects and connected them, you get not “a pile of instances” but an object graph (object graph). The word sounds like something from mathematics, but in practice it is just the answer to the question: “Which objects live in memory, and who points to whom through fields?” If you understand this graph, you understand the application at the level of mechanics, not hopes.
Why is this useful right now, on a simple project? Because your brain can still keep the whole graph in mind. That is a rare luxury. In real life, the graph becomes so large that, without discipline, it turns into “spaghetti with references.” And we want the application to remain readable even without Spring.
Let us draw a simplified object graph for the client part of ReadLater Starter:
flowchart TD
A["ReadLaterApplication main"] --> B["CommandRouter"]
B --> C["CatalogCommandController"]
C --> D["CatalogService"]
D --> E["CatalogClient"]
E --> F["HttpClient"]
E --> G["ObjectMapper"]
Two ideas matter here. First, the direction of the connections is read from top to bottom: from the entry point to business logic and further to infrastructure. Second, HttpClient and ObjectMapper are low-level “bricks” that are usually worth creating once and reusing, rather than spawning one instance per call.
3. Anti-pattern: assembling everywhere
Most often, code degrades not because someone is evil and wants chaos. It degrades because “this is faster right now.” The most typical scenario: you moved a piece of code from main() into CatalogService, but left dependency creation inside the service. Formally, you “split it into classes.” In practice, you just hid the chaos deeper so it would not bother you with its appearance.
Here is an example of something that looks “convenient,” but in practice breaks substitutability and testability:
import java.net.http.HttpClient;
public final class CatalogService {
// Bad: the service chooses the implementation and the way the dependency is created itself
// (from the outside, this is not visible and it is hard to substitute in tests).
private final CatalogClient client =
new CatalogHttpClient(HttpClient.newHttpClient(), ObjectMapperFactory.create()); // dependencies are hidden
}
From the outside, it is not clear that the service uses HTTP at all. Even worse: you cannot quickly swap CatalogHttpClient for CatalogMockClient, because the service has decided for you which client it likes. It is roughly as if a kettle could choose its own outlet in the wall and, if needed, slightly move the furniture.
The correct variant has already been discussed: the service is “given” a dependency, not “embedded” with it:
public final class CatalogService {
private final CatalogClient client;
public CatalogService(CatalogClient client) {
// Good: the dependency comes from the outside (from the composition root)
this.client = client;
}
}
And now a logical next step appears: if the service itself does not create CatalogClient, then someone must create CatalogClient and pass it into CatalogService. That is exactly the job of the composition root.
4. Option 1: main() as the assembly point
Sometimes students get the feeling that “assembly in main() is bad practice.” In fact, it is neither bad nor good. It is simply very honest: you can see what is being created, in what order, and where it is passed. For a small project, this is even a plus: fewer files, fewer jumps through the code.
The main condition is that main() must not perform business logic, must not make HTTP calls, and must not parse JSON. It should create the necessary objects and pass control onward.
An example of a “tolerable” main() as an assembly point:
import java.net.http.HttpClient;
public final class ReadLaterApplication {
public static void main(String[] args) {
// Low-level infrastructure is usually created once per run
HttpClient httpClient = HttpClient.newHttpClient();
// A shared JSON mapper is also part of the infrastructure; there is no need to spawn it all over the code
var objectMapper = ObjectMapperFactory.create();
// Wiring: build the graph from top to bottom (or bottom to top — whichever is clearer for you)
var catalogClient = new CatalogHttpClient(httpClient, objectMapper);
var catalogService = new CatalogService(catalogClient);
var controller = new CatalogCommandController(catalogService);
// Control handoff point: from here on, the application's entry layer works
new CommandRouter(controller).run(args);
}
}
Notice an important detail: there is no “catalog logic” here, only wiring. If you catch yourself thinking, “let's parse args here and do search,” then main() has started to bloat again and it is time to move routing into CommandRouter (which is exactly what we do).
But there is an honest downside: as soon as new parts are added (readinglist, shared utilities, another command), main() will start to grow. And then you will want the second option.
5. Option 2: the ApplicationBootstrap class
When main() starts to resemble a shopping list for the week (everything important is there, but it is hard to read), we move assembly into a separate class that does only that. It is often called Bootstrap, Assembler, or ApplicationFactory — the name is not so important. What matters is that it lives in the app package and remains the only place where we connect major components.
A thin main() with this approach looks almost like a “power switch”:
public final class ReadLaterApplication {
public static void main(String[] args) {
// Bootstrap is responsible only for assembling and connecting objects
ApplicationBootstrap bootstrap = new ApplicationBootstrap();
// Router is already the application's “entry layer”, which accepts external input
CommandRouter router = bootstrap.createRouter();
// Pass control onward
router.run(args);
}
}
Now all the “kitchen” is hidden not inside the services (which is bad), but in one specially designated place (which is good). And it is hidden not by magic, but by ordinary Java code — you can open ApplicationBootstrap and see everything.
The bootstrap itself usually keeps low-level dependencies as fields so that it does not create them again and again:
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.http.HttpClient;
public final class ApplicationBootstrap {
// Shared low-level dependencies: create once per run
private final HttpClient httpClient = HttpClient.newHttpClient();
// It is also convenient to keep the mapper shared: the same settings for the whole application
private final ObjectMapper objectMapper = ObjectMapperFactory.create();
}
Here it is important not to go to the extreme of making static everything. Bootstrap fields are fine: this is just one object created at startup that contains “shared assembly details.” These are not global singletons yet; this is ordinary explicit ownership of dependencies.
6. Graph: catalog search and catalog details
Now let us assemble the pieces into a more coherent scheme closer to our project. We will follow a “bottom-up” logic: first create low-level things (HttpClient, ObjectMapper), then the catalog client, then the service, then the entry layer (command controller), then the router.
Let us start with a small ObjectMapper factory. It is convenient to keep it in common, because we need JSON both in the catalog and later in other parts of the project.
import com.fasterxml.jackson.databind.ObjectMapper;
public final class ObjectMapperFactory {
private ObjectMapperFactory() { }
public static ObjectMapper create() {
// The single place where ObjectMapper is configured.
// If modules/settings are needed later, add them here.
return new ObjectMapper();
}
}
Now, the wiring for the catalog. We leave the top-level app package for ReadLaterApplication, CommandRouter, and ApplicationBootstrap, and keep the feature's entry code close to it — in catalog.command. What matters to us below is exactly the wiring slice: it does not repeat the entire CLI logic, but shows who gets what through the constructor.
public final class CatalogCommandController {
private final CatalogService catalogService;
public CatalogCommandController(CatalogService catalogService) {
// Important: the controller receives the dependency from the outside, not by creating it
this.catalogService = catalogService;
}
public void search(String query) {
// The controller is thin: it just delegates to the application layer
catalogService.search(query);
}
}
Notice that the controller does not print or format anything yet. That is normal if output in your project is organized differently for now. The point of the example is that the entry layer is thin and does not create dependencies.
Now the bootstrap that can assemble the catalog part:
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.http.HttpClient;
public final class ApplicationBootstrap {
// Reuse low-level dependencies
private final HttpClient httpClient = HttpClient.newHttpClient();
private final ObjectMapper objectMapper = ObjectMapperFactory.create();
public CatalogCommandController createCatalogController() {
// This is the implementation “switch”: you can swap the HTTP client for a mock
CatalogClient client = new CatalogHttpClient(httpClient, objectMapper);
// The service receives the client through the constructor (explicit dependency)
CatalogService service = new CatalogService(client);
// The controller receives the service through the constructor
return new CatalogCommandController(service);
}
}
This already looks like a proper assembly: the order is visible, the connections are visible, and the implementation switch is visible (in this version it is always CatalogHttpClient, but it is exactly the bootstrap that is the place where you can choose CatalogMockClient without touching services).
And here it is easy to see what plain Java makes you pay for manually: we choose the real/mock implementation ourselves, we create shared infrastructure objects like HttpClient and ObjectMapper ourselves, and we pass them through constructors to the needed place ourselves. Later, the Spring container will assemble this object graph for us, but the roles and dependencies do not disappear. If a service needs a client and the client needs a mapper, we still have to think it through — just without manual wiring boilerplate.
All that remains is to connect this with the command router. The router is part of the app package because it is the application's entry level (it is not “about the catalog”; it is “about starting the application”).
import java.util.Arrays;
public final class CommandRouter {
private final CatalogCommandController catalog;
public CommandRouter(CatalogCommandController catalog) {
// Router receives controller(s) — these are its entry-layer dependencies
this.catalog = catalog;
}
public void run(String[] args) {
// Minimal input validation: if there are too few arguments, do nothing
if (args.length < 3) return;
// Routing: choose the command and delegate to the needed controller
if ("catalog".equals(args[0]) && "search".equals(args[1])) {
// Build query from the remaining arguments
String q = String.join(" ", Arrays.copyOfRange(args, 2, args.length));
catalog.search(q);
}
}
}
The logic is intentionally minimal here. The router does not make HTTP calls and does not map JSON — it only accepts external input (args) and sends it onward. Yes, for now it only supports catalog search. You can extend it in the same way for catalog details, but even in this form you can already see that routing lives in one place, not scattered across services.
Now the final piece: the bootstrap assembles the router and passes it the already assembled controller.
public final class ApplicationBootstrap {
public CommandRouter createRouter() {
// Assemble the application's entry layer from already assembled pieces
return new CommandRouter(createCatalogController());
}
}
And all together in your head, it turns into a clear story: ReadLaterApplication creates the bootstrap, the bootstrap creates the router, the router calls the controller, the controller calls the service, the service calls the client. Exactly the object graph we drew as a diagram.
7. Typical mistakes in manual wiring
Mistake No. 1: part of the graph is created in bootstrap, and part is “silently” inside services.
This is the most insidious breakdown of discipline. From the outside it seems: “well, we already made bootstrap.” But if inside CatalogService there is still new CatalogHttpClient(...), you have effectively made two composition roots: one visible and one hidden. As a result, you get unpredictable behavior, duplication of HttpClient, and no way to honestly replace a dependency implementation.
Mistake No. 2: duplicate assembly in several places “because it is faster.”
Today you assembled CatalogService in ReadLaterApplication, tomorrow — in ApplicationBootstrap, the day after tomorrow — in some CatalogUtils.createService(). It will work, but the project will start to behave like a hydra: change the constructor, and suddenly you have to edit three different places, otherwise some launch modes break. One graph — one assembly point.
Mistake No. 3: turning bootstrap into a business-logic place.
Bootstrap is not a “service service.” It should not decide how to search books or how to handle errors. Its job is to assemble objects. If you catch yourself seeing conditions like “if the request is empty, return an error” in bootstrap, then your business logic has gone to the wrong place. Assembly should be short and boring. A boring bootstrap is a sign of a healthy project.
Mistake No. 4: global static fields as “quick DI.”
Sometimes it is tempting to make public static final ObjectMapper MAPPER = ... and call it a day. Yes, it is fast. But then you will want different mapper settings or to substitute it in a test scenario, and suddenly it turns out that everything is nailed to one place. In an educational project, we train the habit of explicit dependencies: if an object is needed, let it come through the constructor, and let it be created in the composition root.
Mistake No. 5: creating absolutely everything “just in case,” even what is not needed in this run.
Manual assembly is good precisely because you can see the cost of each new. If you create a bunch of objects that are not used in the current command, you are either wasting resources or, worse, unknowingly triggering side effects. Good assembly does not have to be lazy and clever, but it must be deliberate: you understand why each object was created.
GO TO FULL VERSION