I am trying to grasp the concept of implementing a composition root in a project.
Based on my research, improper usage of the composition root (such as referencing it in multiple places within your application code) can lead to the service locator antipattern.
Allow me to present an example of a project without a composition root.
Here is the current project structure:
- server.ts
- domain.ts
- application.ts
- api.ts
- sql-repository
server.ts:
This file imports the API and sets up the server.
import express from 'express';
import API from './api'
const app = express();
const port = 3000;
app.use(express.json());
app.use(API);
// Start server
app.listen(port, () => {
console.log('listening on port: ' + port);
});
domain.ts:
Within this file, the core logic of the domain is defined.
export type Entity = {
param1: string,
param2: string,
};
export type IRepository = {
GetMultipleEntities(filterParam: string): Entity[] | undefined
GetEntity(filterParam: string): Entity | undefined
CreateEntity(entity: Entity): void
UpdateEntity(entity: Entity): void
}
Of course, this is just one illustrative example to give you an idea of the project's structure.
Upon closer inspection, everything seems fine until we reach the api.ts file. It directly imports the concrete implementation and injects it into the use case. What if there are numerous dependencies to import and utilize? I don't want api.ts taking on the responsibility of deciding which implementations should be used where.
On the flip side, how should I go about implementing a composition root then? I'm uncertain about how to construct the complete object graph and then pass it to the server object so that the appropriate implementations end up in the right places.
Thank you in advance for any guidance!