Circular Dependencies in Dependency Injection

Brian Mearns
Software Ascending
Published in
11 min readApr 10, 2018

In which we discuss a circular dependency issue we got into, how we got out of it, and some general software principles that we applied along the way.

Image credit left to right: public domain by the author; CC BY-SA 3.0 by Frank Vincentz; CC BY-SA 3.0 by “Mobilos”

Our team was recently working on a code refactor that involved adding some new injectables in Guice. If you’re not familiar with dependency injection or Guice, fear not, here’s all you need to know for this article:

Dependency Injection

Dependency Injection is a way of implementing the dependency inversion principle (DIP), which essentially just means that instead of having your class instantiate objects that it depends on (e.g., HTTP clients, loggers, etc), these objects should be provided to your class. This is important for decoupling your code from its dependencies, makes modification easier, and makes testing easier (e.g., you can just pass in a stub/mock/spy HTTP client, instead of having to set up a test HTTP server, you can read a bit more about this in Testing, Mocking, Contracts, and DIP).

Dependency Injection (DI) means you’re using some kind of tool (in this case, a java package from Google called Guice) that lets you register things like “when I need a dependency of type Foo, use this instance” or “when I need a dependency of type Bar, tagged with this label, then create an instance like this”. You do all this configuration and registration in one place (typically), a class called a module (in Guice terms), and then in the rest of your application, you can ask your DI system for the dependency.

Circular Dependencies

The situation we got into was a circular dependency in our Guice module, our DI configuration. A circular dependency isn’t hard to understand, it basically just means we told Guice how to construct an object to satisfy a certain dependency, and in order to do so, it ends up needing that same dependency. A simplified example might be:

  1. When a dependency of type Chicken is needed, get an Egg and call Egg::hatch() on it to get the Chicken.
  2. When a dependency of type Egg is needed, get a Chicken and call Chicken::layEgg() on it to get the Egg.

Guice now needs an Egg in order to get a Chicken (by rule 1), but it needs a Chicken in order to get an Egg (by rule 2). Rewriting these to explicitly call out the dependencies might look something like:

  1. A Chicken depends on an Egg.
  2. An Egg depends on a Chicken.

In our case, there were several additional steps involved to complete the circle; the dependencies were more or less:

  1. A HeartbeatActor needs a Shutdownable
  2. Shutdownable is implemented by the BatchManager
  3. BatchManager needs a WorkerSupervisor
  4. WorkerSupervisor needs a MessageRouter.
  5. MessageRouter needs a HeartbeatActor
  6. back to square-one, ad infinitum.

Identifying the Problem

First things first, we didn’t know what the problem was. We had no idea there was a circular dependency. The changes that we made were to insert rules 1 and 2 (specifically, so that the HeartbeatActor could shut down the BatchManager if heartbeats stop coming in); there were so many other rules involved in completing this circle (several of which have actually been elided from the above list for simplicity) that none of us had them all in our head, and we had no idea that we had created a circle.

All we knew initially was that the application was failing on startup. After combing through the logs for a little while, we finally noticed something we had been ignoring, which was a warning from the logging library that no log appender had been found for Guice. It finally dawned on someone that if there were any errors in the injectable dependencies we had created, this meant we wouldn’t be seeing them.

After a few false starts and a few trips to Google, StackOverflow, and our company chat, we were able to wire Guice into the logging so we could see the errors. Lo and behold, Guice was telling us very explicitly (and fairly clearly) not only that we had a circular dependency, but exactly what chain of dependencies was involved.

Developing a Solution

I don’t know why, but circular dependencies have always felt somewhat intractable to me. Maybe it’s because of the chicken and egg example, which (ignoring evolution) really has no solution: if you truly need a chicken to get an egg, and you truly need an egg to get a chicken, then you just can’t ever have either. It’s simply unsolvable.

But circular dependencies in software are solvable because the dependencies are always self-imposed by the developers. To break the circle, all you have to do is break one of the links.

One option might simply be to come up with another way to produce one of the dependencies, in order to bootstrap the process. For instance, we might introduce some kind of PrimordialChicken type which doesn’t require an egg, it can be instantiated spontaneously out of the Ether.

All plausible conceptualizations for the Primordial Chicken. (Image credits are shown at top of article).

Another option is not to eliminate, but to reverse one of the dependencies. This is the option we ultimately used to break my circle.

But in order to get there, the first step was to identify the entire circle of dependencies. From there, we started to look at which of these dependencies were most important, which made the most sense. Very often, we end up with dependencies that aren’t actually necessary and aren’t even conceptually appropriate to our system. Any such tenuous links in the dependency chain should be in your first set of candidates for breaking.

The links in the chain that jumped out as most suspect to us were:

  • Rule 5 — MessageRouter needs a HeartbeatActor
  • Rule 2 — Shutdownable is implemented by BatchManager
  • Rule 1 — HeartbeatActor needs a Shutdownable

Rule 5 jumped out because in the actor system the MessageRouter was sending messages directly to the HeartbeatActor, that's why it had the dependency on it. It seemed plausible that the MessageRouter didn’t actually need the HeartbeatActor directly, it might be able to use an indirect reference to the actor, which the actor system could then resolve just-in-time. In this case, the HeartbeatActor wouldn’t need to exist in order to create the MessageRouter, it would just need to exist by the time the MessageRouter was ready to send it messages. In this way, we wouldn’t actually be breaking the dependency between the two classes, but we would be delaying it to the point that Guice no longer cares about it.

Rule 2 jumped out for two reasons: first of all, because it was one of the ones we had added, and second of all because it was the only link in the chain where the dependency was defined by an “is-a” relationship rather than a “has-a”. If we could change that is-a into a has-a, we could reverse the direction of the dependency and therefore break the circle.

Rule 1 jumped out because the HeartbeatActor is an actor, so it seemed like maybe it should be controlling shutdown by sending a message, instead of calling an interface method. In all likelihood, this would not have solved the problem, it would have just added more links in the cycle. Most likely we would simply have added something like a ShutdownActor which would have become dependent on the Shutdownable, and the HeartbeatActor would in turn just become dependent on the ShutdownActor.

However, this one jumped out even more because we knew about Rule 2: we knew that the Shutdownable that would be passed into the HeartbeatActor actually was the BatchManager, and it seemed like it was giving the HeartbeatActor too much power to have that kind of access to what is really the application class. But that really was more a problem with Rule 2 again, not so much Rule 1.

With the dependency between BatchManager and Shutdownable reversed as shown here, the cycle is broken.

So we decided to go with breaking Rule 2. Instead of having the BatchManager implement Shutdownable (so that in order to satisfy a dependency on Shutdownable, you would ultimately be dependent on the BatchManager), we would implement the Shutdownable interface in a dedicated object that could then be passed into the BatchManager’s constructor. This makes BatchManager dependent on the Shutdownable, instead of the other way around.

By reversing this arrow, creating the Shutdownable can now be the first thing Guice does. Once it has that, it can create the HeartbeatActor, then the MessageRouter that depends on it, and so on, until it finally has everything it needs (including the Shutdownable) to create the BatchManager.

The Solution

To implement the solution, we created a ShutdownManager class, a simple stand-alone class with no dependencies. All it really does is keep a shutting down flag which is initially false and can be set to true at most once.

In order to satisfy the dependency, it needs to supply a Shutdownable interface, which has a single method: void requestShutdown(). However, a separate but related dependency that Guice needed to manage was a ShutdownFlag (it wasn't part of the circular dependency), which also has a single method: boolean isShuttingdown(); this is used to query the shutdown state. Clearly, if we were going to be moving the shutdown management out of the BatchManager into this new class, it would need to provide for this interface as well.

Lastly, we would need a way for the BatchManager to actually control the shutdown, specifically to say “shutdown is starting” and “shutdown is complete”, so the new ShutdownManager class would need to supply this interface as well, and the BatchManager would be dependent on it.

However, as mentioned above, we didn’t want to just be passing around this BatchManager instance everywhere, because it was too powerful. It provided an interface to actually shut down the system when all that was ever really needed outside of the BatchManager was the ability to request a shutdown.

The dependencies that this ShutdownManager was going to be satisfying weren’t typed as ShutdownManager, they were typed as Shutdownable and ShutdownFlag. But we still didn’t feel good about passing the actual ShutdownManager instance around to all of these other modules; they could always do an instanceof check and cast it to a ShutdownManager and leverage those more powerful interfaces.

So instead of having the ShutdownManager actually implement the Shutdownable and ShutdownFlag interfaces, we applied the supplier pattern by giving the ShutdownManager methods that would return objects of these types (favoring composition over inheritance).

The ShutdownManager uses the supplier pattern to provide the required interfaces.

These objects were instantiated from anonymous inner classes and method references to delegate all the work to the actual ShutdownManager.

The ShutdownManager implements the supplied interfaces using anonymous inner classes to delegate back to private methods of itself.

Guice could then satisfy Shutdownable and ShutdownFlag dependencies by getting the ShutdownManager instance, and invoking the appropriate supplier methods on it to get one of those anonymous implementations, which don’t expose any of the more sensitive parts of the ShutdownManager.

Updated dependencies: HeartbeatActor and BatchManager now both depend on the ShutdownManager, breaking the circular dependency that bound HeartbeatActor and BatchManager to one another.

Then, to instantiate the BatchManager, Guice would need to provide the actual ShutdownManager, because the BatchManager needs those more powerful methods.

Going further, we actually pulled out those methods that the BatchManager required into a new ShutdownController interface as well, and played the same trick with it, supplying the interface from the ShutdownManager. Now nothing but the Guice module has any direct access to the ShutdownManager instance at all.

It’s worth noting that the ShutdownManager is now a root dependency of several things; the BatchManager for one, but also anything that depends on either a Shutdownable or a ShutdownFlag. However, even though the ShutdownManager is a dependency of these things, there was no reason to make it injectable, so we didn’t. Making it injectable would mean that any class could ask Guice for the ShutdownManager at any time, which defeats the purpose of trying to protect its more important methods. Instead, we created the ShutdownManager instance in the Guice module’s initialization method and simply stored it as an instance variable in the module itself. The supplier methods of the module that supply the BatchManager, Shutdownable, and ShutdownFlag dependencies can use it directly, and it never makes its way into the injector.

Take-Aways

Interface Segregation

The ShutdownManager honors interface segregation, which is a basic principle that your interfaces should be limited in scope. We could have rolled Shutdownable and ShutdownFlag into a single interface and made all the things dependent on that, then simply satisfy that dependency with the ShutdownManager object. By splitting them apart into two interfaces, we can decouple all of those modules and make future changes easier. It also reduces the risk that a developer will come along one day and think it’s a good idea to start using more of the interface than was really intended in a particular module; after all, the interface is there, so it must be ok to use!

Breaking the shutdown code out of the BatchManager was itself an exercise in interface segregation as well, because the requestShutdown() method was previously part of its interface, and things that only ever needed to call that one method were now dependent on the entire BatchManager, which is actually the whole reason the circular dependency happened. Once Shutdownable was pulled out of the BatchManager, all of those modules that had been dependent on BatchManager could now be dependent on just Shutdownable, which gave us the freedom needed to pull out the implementation of the interface and break the circle.

Access Restriction is More than Just Keywords

We didn’t want the BatchManager instance, or the ShutdownManager instance, getting exposed all over the place. This is an example of access restriction that goes way beyond simply marking your fields as private. By implementing interface segregation, we were able to keep the BatchManager out of a lot of places that really didn’t need it, and likewise with the ShutdownManager.

Furthermore, by keeping the ShutdownManager as an instance member in the module and by having the ShutdownManager act as a supplier of the required interfaces, instead of as an instance of them, it became unnecessary to expose the ShutdownManager to the world through injection.

Circular Dependencies are Solvable

Once you’ve identified the cycle of dependency links (logging helps), it’s possible to analyze these links and find ones that can be broken or reversed in order to break out of a cycle. No galactic chickens required.

--

--

Brian Mearns
Software Ascending

Software Engineer since 2007 ・ Parent ・ Mediocre Runner ・ Flower and Tree Enthusiast ・ Crappy Wood Worker ・ he/him or they/them