How to Break a Cyclic Dependency between ES6 modules

Gara Mohamed
Angular In Depth
Published in
5 min readApr 10, 2019

Based on Typescript examples

Photo by Tine Ivanič on Unsplash

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

Introduction

One of the rare books fully dedicated to modularity patterns is Java Application Architecture: Modularity Patterns with Examples Using OSGi written by Kirk Knoernschild. Among the important sections in this book is the chapter Acyclic Relationships which explains three techniques to break cyclic dependencies to make the dependency structure a Directed Acyclic Graph (DAG) as required by the Acyclic Dependencies Principle (ADP).

In this article, we apply these three techniques to ES6 modules. So, let’s start by introducing the example on which we will carry out the techniques.

Cyclic Dependency Example

In this section, we present the starting code that will be refactored in the following sections. There are two cycle types between modules: direct and indirect. Our example shows just the first type. The source code of the different versions is available here.

In our original codebase, we have two modules:

  • An order module containing an Order class
  • A customer module containing a Customer class

In the two modules, we have two root concepts:

  • Customer
  • Order

and we have two behaviors associated with them:

  • calculate customer discount based on customer orders.
  • calculate order discount based on customer discount.

The two modules customer.ts and order.ts depend on each other. So, the dependency graph is a cyclic one.

Cyclic dependency

Let’s break this cycle.

Breaking Cyclic Dependency Techniques

The most important techniques to break dependency cycles are:

  • Dependency Inversion (aka. Callback)
  • Escalation
  • Demotion

In this section, we cover the three techniques and also a more basic one: Merging. After that, we will try to draw a classification tree of all the strategies.

Dependency Inversion (aka Callback)

To remove the cycle using DI, we remove one direction of the two dependencies and harden the other one. To do that we should use an abstraction: in the order module, we add the DiscountCalculator interface. Thus, the Order class depends on this new type and the order module no longer depends on the customer module.

The customer class is still responsible for calculating the discount, but this time it implements this behavior as a DiscountCalculator class.

The dependency graph becomes now a DAG:

The dependency is now unidirectional and the cycle is removed.

Escalation

With the first technique, to remove the cycle we just edited the dependencies of a single module. In the next two techniques (escalation and demotion), we will delegate the discount calculation to a new module and we will introduce two more dependencies to/from the new module.

The customer calculates the discount based on its orders.

And the Order just stores the raw amount. It’s no longer responsible for calculating its discounted amount.

It’s now the role of the OrderDiscounter to calculate the discounted amount.

The new dependency graph becomes the following:

Escalation

The dependency graph becomes also a DAG, but with a new module and two new arrows.

Demotion

As the escalation, to break the cycle demotion add a new module but with two ingoing dependencies. The new module contains the new class OrderDiscount:

The Order class uses the OrderDiscount to calculate its discountedAmount:

The customer is responsible for creating the OrderDiscount instance that will be passed to the Order.

The new dependency graph becomes the following:

Demotion

Like the escalation graph, the demotion’s dependency graph becomes also a DAG with a new module and two new arrows. But, this time the direction of the arrow is inverted.

Merge modules

The other way to break the dependency cycle in our example is to merge the two modules in a single one. This is a simple strategy but it is not the best one when the cohesion between the two modules is low.

We should also note that by merging our two modules we remove the cycle at the modules level but it stays present at the classes level. If we want to break the cycle at the classes level we can use the same techniques.

Taxonomy

After showing the cyclic dependency breaking strategies, let’s draw a taxonomy diagram as a summary:

Expect the remove the two modules strategy, we already discussed the other strategies. So, we don’t repeat our self and we go straight to the conclusion.

Conclusion

In his book, Kirk Knoernschild carried the patterns from the C++ to the Java world. In this post, we’ve tried to do the same thing by promoting the terminology to the Javascript and Typescript world. The Java world is more mature and rich than the front-end ecosystem. So, it should be more and more inspired by the backend experience.

Before we finish this article, I would like to note two things:

  • We presented each technique in the context of breaking a direct cycle but they can also be used to break indirect cycles.
  • Dependency cycle isn’t absolute evil. For example, in some cases, the cycle is allowed if it is local to a layer. But, the cycle shouldn’t exist between release units.

Finally, you can find Kirk’s code that inspired this post on his GitHub here.

--

--