0

I'm developing a microservice application divided into 4 layers (API, Application, Domain and Infrastructure).

The composition root of my application is an assembly in the API layer. From what I understand all the binding and registering of dependencies in an application using a DI framework is typically done in the composition root (aka the API layer).

But doesn't this assumption about the composition root indicate it knows too much about the application? Dependencies to the Infrastructure layer will be set at the API layer. Where is the encapsulation there?

However if each layer could register its own internal dependencies, then wouldn't it couple a specific DI framework to the whole application? I would also need to pass the container between the layers so each layer can register its stuff.

How can this situation be managed?

Kit
  • 20,354
  • 4
  • 60
  • 103
underthevoid
  • 513
  • 1
  • 6
  • 17

2 Answers2

2

But doesn't this assumption about the composition root indicate it knows too much about the application? Dependencies to the Infrastructure layer will be set at the API layer. Where is the encapsulation there?

There will be no encapsulation in your Composition Root. The Composition Root is strongly coupled to all assemblies, modules, and components in your application. But this isn't particular to the use of Loose Coupling or Dependency Injection. Due to the way how library dependencies work—they are transitive—means the startup path of your application will always (directly or indirectly) depend on all other assemblies in your application. Because of this, the start-up path should be the most volatile part of the application (and when you follow the Composition Root pattern, it will be). It should follow the Stable-Dependency Principle; one of Principles of Component Design (see chapter 28 of Agile Principles, Patterns, and Practices for a detailed discussion).

it knows too much about the application?

I would say it knows just enough. It's job is object composition, which means it must know about the structure of all components in the system. It shouldn't know all implementation details, but just enough to be able to correctly compose the system. You can try to move composition back to individual modules, but that leads to the situation where knowledge of composition gets scattered out through the application, while your entry point still depends on everything, because of the transitive nature of assembly dependencies. You'll end up in a worse place. Centralization of object composition is the whole idea of the Composition Root.

In section 4.1 of DIPP&P, Mark Seemann and I talk more extensively about the Composition Root and the transitive nature of library dependencies.

Steven
  • 166,672
  • 24
  • 332
  • 435
1

What you are getting at is the fundamental difference between a library, which is not prescriptive and a framework, which is prescriptive.

Using a library has less initial cognitive load, but using a framework usually has a higher initial cognitive load, and once you know the framework, it does a lot for you.

Ok, so all that aside, what I do with a layered architecture is provide nothing prescriptive in the library in terms of IoC (so no registrations, no nothing). A library should, then, be very reusable without thinking too much about dependencies.

For a framework, I will provide sane defaults for the registrations, and make them overridable. Pragmatically, that comes down to

  • framework registers the things it knows about in each of the layers using concrete implementations for the interfaces. These concrete implementations are what's built into the framework.
  • framework provides "convenience methods", abstract registration classes or profiles that wire up dependencies at the right level of abstraction to allow the caller to override these defaults.

API Scenario

For example, a framework that supports APIs needs to build URIs to related resources knows that it needs a context-aware IUriBuilder, and an INavigationModel to navigate to related resources; together these collaborate to allow an API to provide links along with resources.

That's the level of abstraction that will be imposed on the calling layer. The framework does not impose a lower level of abstraction on the caller because then the caller knows too much, and the caller may break what the framework needs.

Let's say the framework offers a rudimentary API-behind-a-reverse-proxy default. It registers all that for you. If that is not sufficient, your caller calls, let's say, a method called RegisterLinkBuildingService(...). This method replaces arbitrary, individual registrations through ConfigureServices with a prescriptive method that forces the caller to provide specific concrete services the framework needs.

So, you say, OK, I need link building that is aware of all the proxies, API gateways, and such between my cloud and the rest of the world. You wouldn't register all of that at the API layer, because much of it is needed in whole forms at the framework layer*. Instead, you call that Register.. method above with your concrete services.

This integrates the two (or more) layers through contractual registration interfaces, encapsulating what is needed, but allowing upper layers some level of freedom.

Kit
  • 20,354
  • 4
  • 60
  • 103
  • You are talking about a case where each layer (ie. the whole application) depends on a specific DI framework and use it to resolve the dependencies, right? However the actual composition of the application should be done in the API layer as I said, right? That way you could have a framework that injects dependencies using object metadata for instance and because of that each layer should be coupled with the framework to determine its dependencies. – underthevoid Aug 04 '21 at 22:34
  • Yes this does mean a dependency on a specific DI implementation, but it's not likely you'd really change DI implementations. I once thought that, but anecdotally it's never really happened. The layer is still coupled to the framework, and composition does occur at the comp. root, but my point is the composition root is not coupled to individual resolutions but to abstractions and their requirements. – Kit Aug 04 '21 at 23:32