2

I am trying to understand how should I implement a composition root in a project.

From what I have red, if use the composition root the wrong way (for example, by referencing it in lots of place in your application code), you will end up with the service locator.

Let me show you an example of a projcet with out a compositon root.

I have the following project structure:

  • server.ts
  • domain.ts
  • application.ts
  • api.ts
  • sql-repository

server.ts:

This file imports the API and initializes 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:

This file holds the core logic of the domain.

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
}

application.ts:

This file holds the use cases of the application.

import {IRepository} from './domain';

export const CheckIfEntityExists = (filterParam: string, entityRepository: IRepository): boolean => {
    let entity = entityRepository.GetEntity(filterParam);
    return typeof entity != "undefined";
};

sql-repository.ts:

This file holds the concrete implementation of the IRepository interface

import {Entity, IRepository} from './domain';

export class SqlRepository implements IRepository {
    GetEntity(filterParam: string): Entity {
        //
        // some sort of logic to get entity from an sql database
        //
        return {
            param1: '',
            param2: ''
        };
    }
    GetMultipleEntities(filterParam: string): Entity[] {
        //
        // some sort of logic to get multiple entity from an sql database
        //
        return [
            {
                param1: '',
                param2: ''
            },
            {
                param1: '',
                param2: ''
            }
        ];
    }
    CreateEntity(entity: Entity): void {
        // some logic to enter new data to the sql database that represents an entity
    }
    UpdateEntity(entity: Entity): void {
        // some logic to update the entity
    }
}

api.ts:

This file holds the api that uses the use cases in the application.ts file

import {Router} from 'express'
import {CheckIfEntityExists} from './application';
import {SqlRepository} from './sql-repository';

const router = Router();

router.get("/exists/:filterParam", async (req, res) => {
    CheckIfEntityExists(req.params.filterParam, new SqlRepository);
    res.end()
});

export default router

Ofc this is just an example, but you get the point of how the project looks like.

From what you can see, its all good until we see the api.ts file. It imports the concrete implementation and injects it into the use case. What if there were much more dependencies to import and use, I do not want the api.ts to be responsible to decide which implementations go to which place, its not its responsibility.

But on the other hand, how should I implement a composition root then? I have no idea how should I construct the full object graph and then pass it to the server object so that the right implementation will go to the right objects.

Thanks in advance!

ford04
  • 30,106
  • 4
  • 111
  • 119
Max
  • 451
  • 6
  • 16

1 Answers1

1

Definitions

To give some scope and definitions of the term Composition Root, here are good quotes by Mark Seemann in two related articles:

Where should we compose object graphs?

As close as possible to the application's entry point.

What is a Composition Root?

A Composition Root is a (preferably) unique location in an application where modules are composed together.

The Composition Root is an application infrastructure component.

A Composition Root is application-specific; it's what defines a single application. After having written nice, decoupled code throughout your code base, the Composition Root is where you finally couple everything, from data access to (user) interfaces.

Implications

In other words, your api.ts could be seen as the entry point of your server application, so it is perfectly fine to compose your object graph in it. You could also

  1. choose server.ts or
  2. define a separate DI module like composition-root.ts which does all the composing and is imported by server.ts or api.ts (even more cohesive).

More important here is, that you have a unique location near/in your application entry point of your project which is responsible for creating/composing the dependencies.

Example

Let's take your concrete example and presume we want to do all composing stuff in composition-root.ts imported by api.ts. Your dependency graph looks like this (--> means an import here):

server.ts --> api.ts --> application.ts --> domain.ts 
                                        --> sql-repository.ts

Everything except composition-root.ts is decoupled from its dependencies. Constructor injection could be used like in the article's example, or any another injection method, depending on the language/framework/coding style. Your sample already looks quite fine, let's add some DB abstraction layer for the repository and abstract the composing away from api.ts.

sql-repository.ts:

export class SqlRepository implements IRepository {
  constructor(private db: DB) {}
  ...
}

api.ts:

import {CheckIfEntityExists} from "./composition-root"
...

router.get("/exists/:filterParam", async (req, res) => {
    CheckIfEntityExists(req.params.filterParam);
    res.end()
});

composition-root.ts:

import {CheckIfEntityExists} from './application';
import {SqlRepository} from './sql-repository';

const db = new DB();
const sqlRepository = new SqlRepository(db);
// We do partial application here to free api.ts from 
// knowing the concrete repository.
const _CheckIfEntityExists = (filterParam: string) =>
  CheckIfEntityExists(filterParam, sqlRepository);

export { _CheckIfEntityExists as CheckIfEntityExists };

All in all, you have encapsulated your dependencies nicely in one place composition-root.ts, and other code in the more inner layers of your application architecture does not know anything about their construction.

Hope, that helps.

Community
  • 1
  • 1
ford04
  • 30,106
  • 4
  • 111
  • 119
  • Would it be better instead of making a partial, to create a variable in the composition root named `repository` and make it equal to the concrete repository that we are using, and then make the `api.ts` import both the use case and the repository variable, and pass it to the use case? It still frees api.ts from knowing the concrete implementation but it makes the code in api.ts show that an object of repository is passed to it. What do you think? – Max Aug 24 '19 at 10:26
  • 1
    If you want to treat `api.ts` like a dumb controller/UI binding part, it only should know the next inner usecase/application layer (application.ts) and no modules of the persistence infrastructure. The application layer then knows about repositories. Of course you could also do composing in `api.ts` and shift more responsibilities to it, depends on your architecture/layers you want to have. I think, it is neglectable, as long as you don't distribute dependencies construction and do it somewhere in the inner layers/beyond your entry point! – ford04 Aug 24 '19 at 10:44
  • Puh, that's another question concerning tests that could fill pages. Simply spoken, it depends on the test you want to write. For unit tests you typically mock object dependencies that may also have internal state. In a pure core layer in functional programming, you even wouldn't need to use mocks, have a look at [this link](https://medium.com/javascript-scene/mocking-is-a-code-smell-944a70c90a6a) as hint. E2E tests which include rest api you wouldn't want to mock anything. – ford04 Aug 24 '19 at 11:53
  • 1
    Yeah I looked at few places few minutes after I posted the comment, found out exactly the things you said, so I deleted the comment, found it not really relevant. – Max Aug 24 '19 at 12:42