Dependency Injection With Cyclic Dependencies

Photo by Matt Seymour on Unsplash

Intro

Dependency injection is a very powerful technique that allows us to write code that is both more testable and decoupled.

When using DI we isolate the construction of dependencies in a single place we call composition root or container and then the code that uses these dependencies remains ignorant of how they are constructed.

The construction of these dependencies consists in arranging them in some topological order and then using this order to construct them, which is just a fancy of saying: "begin by constructing the dependencies that have no dependencies themselves, then you move on to the ones which only depend on the dependencies that were previously constructed, and so on until every single dependency have been constructed".

Suppose we have the following situation:

There’s a function a that depends on another function b, b however, also depends on the function a (and these dependencies are not hardcoded, as we’re using DI).

The question is: which do we construct first, a or b?

That’s the problem with cyclic dependencies, they introduce a paradox, for if we choose to construct a first, they would need to have b already constructed and vice versa.

Causes of Cyclic Dependencies

You might be wondering, " but why would we even have a cyclic dependency in the first place?".

There are mainly two kinds of situations that lead to having cyclic dependencies: recursion and circular references (class A has reference to class B and vice versa).

Even though it is possible to avoid both of these situations by bailing out of dependency injection (and thus augmenting coupling and losing some testability) or by converting recursion to iteration in the case of functions or fusing classes that reference one another circularly, or by using ids instead of real references, sometimes this is not desirable.

Good abstractions are hard to come by, and sometimes they will rely on recursion or circular references, so we should be able to keep them while using dependency injection.

But worry not, here we’ll see how to tame these cyclic dependencies.

Shaving the Barber of Seville

"A man of Seville is shaved by the Barber of Seville if and only if the man does not shave himself. Who shaves the Barber of Seville?"

Let’s put this into code so we can better visualize the situation, shall we?

//a.js
const makeA = (b) => () => {
  // do something with b
}

//b.js
const makeB = (a) => () => {
  // do something with a
};

//container.js
const a = makeA(b);
const b = makeB(a);

// Error: Cannot access 'b' before initialization

First of all, it is important to point out that even though we’re ultimately interested in the two functions a and b, we’re dealing with four functions, which are a, b and their constructors, makeA and makeB.

Notice that the only reason that makeA and makeB take b and a, respectively, as arguments are to forward them for further use by a and b.

These constructors don’t use the dependencies they take as arguments, so they don’t care if what they are receiving as arguments are the dependencies they are expecting.

So, what if we could somehow trick them by passing something that looks like the dependencies they expect, but are not exactly these dependencies, because they haven’t been constructed yet, and by the time these dependencies are invoked they have already been constructed?

I know it might sound a little bit confusing, but let me illustrate this idea with a well-known joke:

"— John to Bill Gates —
John: I want to marry your daughter.
Bill Gates: No way! Do I know you?
John: I’m the CEO of the World Bank.
Bill Gates: OK!

— John to the President of World Bank —
John: Make me the CEO of your bank.
President: No!
John: I’m Bill Gates’ son-in-law.
President: Hmm. Ok!"

So here’s what we’ll do, instead of passing the raw dependencies to the constructors, we’ll pass a wrapper object that will (supposedly) contain these dependencies, and as long as the constructor doesn’t peek inside our wrapper, we’ll be fine.

//a.js
const makeA = (dependencies) => () => {
  const { b } = dependencies;
  // do something with b
};

//b.js
const makeB = (dependencies) => () => {
  const { a } = dependencies;
  // do something with as
};

//container.js
const container = {};
container.a = makeA(container);
container.b = makeB(container);

// Everything alright now!

We made two modifications to our code:

  1. Instead of passing dependencies directly to constructors we’re wrapping them in an object.
  2. We’re building the container “step by step” (immutability here won’t do) and we’re passing the container as the dependencies wrapper to constructors.

Let’s take a closer look at the container assembly to understand precisely what’s happening.

//container.js

// First we create an empty container, 
// which will eventually contain all of our
// dependencies
const container = {};

// Then we create "a" and put it inside 
// the container.
// But how can we construct "a" if it expects
// "b" to already be there?
// The answer is that as "makeA" doesn't
// actually use "b" as it is only 
// used when "a" is invoked and not 
// during its construction, so it suffices
// to pass an object that "makeA" "believes"
// that it contains "b".
container.a = makeA(container);

// We then construct "b" doing the same
// thing as before and even though
// "a" is already constructed, we also
// cannot use it inside "makeB", as 
// trying to use "a" will cause "b" to
// be called, and then we'd be back at square 1.
container.b = makeB(container);

With Classes

And this very same idea can be easily applied to classes as well:

//A.js
class A {
  constructor(dependencies) {
    this.dependencies = dependencies;
  }

  foo() {
    const { b } = this.dependencies;
    // Do something with b
  }
}

//B.js
class B {
  constructor(dependencies) {
    this.dependencies = dependencies;
  }

  foo() {
    const { a } = this.dependencies;
    // Do something with a
  }
}

//container.js
const container = {};

container.a = new A(container);
container.b = new B(container);

Troubleshooting

There are many subtle ways where things can go wrong so I think it’s useful to talk about the most common ones.

Destructuring dependencies in function arguments

// This might fail because when
// constructing "a", "b" might not 
// have been constructed yet, therefore 
// "b" will be undefined in this context
// and will be captured as such by the inner 
// closure (AKA the function "a")
const makeA = ({ b }) => () => {
  // do something with b
}

Destructuring dependencies inside the constructor

// Same thing as the previous example,
// specially because destructuring arguments
// is just a syntax sugar for destructuring 
// them inside the function body
const makeA = (dependencies) => {
  const { b } = dependencies;

 return () => {
    // do something with b
  }
}

Assigning dependencies to attributes inside a class

// This might fail because when
// constructing "a", "b" might not 
// have been constructed yet, therefore 
// "b" will be undefined in this context
// and will be stored as such in the class attribute
class A {
  constructor(dependencies) {
    this.b = dependencies.b;
  }
}

// Also fails for the same reasons
class A {
  constructor({ b }) {
    this.b = b;
  }
}

Also, trying to do anything with these dependencies, like invoking them if they are functions, trying to access attributes, or invoking methods if they are classes/objects will cause problems, given that to do any of these things you need to access them first.

Automat(g)ic Dependency Injection

(This section is optional)

Whether you’re going to do manual dependency injection or use some form of automatic dependency injection is completely up to you, but as your application grows so do your dependencies, and making sure that they’re always being constructed in the right order is no easy task.

That said, if I were to recommend a JS Dependency Injection Container it would be Awilix, especially because it supports cyclic dependencies!

To deal with cyclic dependencies using Awilix you just need to follow the same guidelines we already discussed and make sure you’re using the PROXY injection mode.

We want to work with you. Check out our "What We Do" section!