Technology10 minute read

Hold the Framework: Exploring Dependency Injection Patterns

There are two dominant patterns of implementation for inversion of control. Which one is better, and is there a middle path to be found between them? How should you approach IoC to get the most out of your code?

Join Toptal Java Developer Martin Coll in exploring the familiar and unknown landscape of IoC development.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

There are two dominant patterns of implementation for inversion of control. Which one is better, and is there a middle path to be found between them? How should you approach IoC to get the most out of your code?

Join Toptal Java Developer Martin Coll in exploring the familiar and unknown landscape of IoC development.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Martin Coll
Verified Expert in Engineering

Martin is an all-around full-stack developer with years of experience in a wide range of technologies including Java, C#, Python and others.

Expertise

PREVIOUSLY AT

Microsoft
Share

Traditional views on inversion of control (IoC) seem to draw a hard line between two different approaches: the service locator and the dependency injection (DI) patterns.

Virtually every project I know includes a DI framework. People are drawn to them because they promote loose coupling between clients and their dependencies (usually through constructor injection) with minimal or no boilerplate code. While this is great for rapid development, some people find that it can make code difficult to trace and debug. The “magic behind the scenes” is usually achieved through reflection, which can bring a whole set of new problems.

In this article, we will explore an alternative pattern that is well suited for Java 8+ and Kotlin codebases. It retains most of the benefits of a DI framework while being as straightforward as a service locator, without requiring external tooling.

Motivation

  • Avoid external dependencies
  • Avoid reflection
  • Promote constructor injection
  • Minimize runtime behavior

An example

In the following example, we will model a TV implementation, where different sources can be used to get content. We need to construct a device that can receive signals from various sources (e.g., terrestrial, cable, satellite, etc.). We will build the following class hierarchy:

Class hierarchy of a TV device that implements an arbitrary signal source

Now let’s start with a traditional DI implementation, one where a framework such as Spring is wiring everything for us:

public class TV {
    private final TvSource source;

    public TV(TvSource source) {
        this.source = source;
    }

    public void turnOn() {
        System.out.println("Turning on the TV");
        this.source.tuneChannel(42);
    }
}

public interface TvSource {
    void tuneChannel(int channel);
}

public class Terrestrial implements TvSource {
    @Override
    public void tuneChannel(int channel) {
        System.out.printf("Adjusting dish frequency to channel %d\n", channel);
    }
}

public class Cable implements TvSource {
    @Override
    public void tuneChannel(int channel) {
        System.out.printf("Changing digital signal to channel %d\n", channel);
    }
}

We notice some things:

  • The TV class expresses a dependency on a TvSource. An external framework will see this and inject an instance of a concrete implementation (Terrestrial or Cable).
  • The constructor injection pattern allows easy testing because you can easily build TV instances with alternative implementations.

We’re off to a good start, but we realize that bringing in a DI framework for this might be a bit of an overkill. Some developers have reported issues debugging construction problems (long stack traces, untraceable dependencies). Our client has also expressed that manufacturing times are a little longer than expected, and our profiler shows slowdowns in reflective calls.

An alternative would be to apply the Service Locator pattern. It’s straightforward, doesn’t use reflection, and might be sufficient for our little codebase. Another alternative is to leave the classes alone and write the dependency location code around them.

After evaluating many alternatives, we choose to implement it as a hierarchy of provider interfaces. Each dependency will have an associated provider which will have the sole responsibility of locating a class’ dependencies and constructing an injected instance. We will also make the provider an inner interface for ease of use. We will call it Mixin Injection because each provider is mixed in with other providers to locate its dependencies.

The details of why I settled on this structure are elaborated in Details and Rationale, but here is the short version:

  • It segregates the dependency location behavior.
  • Extending interfaces doesn’t fall into the diamond problem.
  • Interfaces have default implementations.
  • Missing dependencies prevent compilation (bonus points!).

The following diagram shows how the dependencies and the providers interact, and the implementation is illustrated below. We also add a main method to demonstrate how we can compose our dependencies and construct a TV object. A longer version of this example can also be found on this GitHub.

Interactions between providers and dependencies

public interface TvSource {
    void tuneChannel(int channel);

    interface Provider {
        TvSource tvSource();
    }
}

public class TV {
    private final TvSource source;

    public TV(TvSource source) {
        this.source = source;
    }

    public void turnOn() {
        System.out.println("Turning on the TV");
        this.source.tuneChannel(42);
    }

    interface Provider extends TvSource.Provider {
        default TV tv() {
            return new TV(tvSource());
        }
    }
}

public class Terrestrial implements TvSource {
    @Override
    public void tuneChannel(int channel) {
        System.out.printf("Adjusting dish frequency to channel %d\n", channel);
    }

    interface Provider extends TvSource.Provider {
        @Override
        default TvSource tvSource() {
            return new Terrestrial();
        }
    }
}

public class Cable implements TvSource {
    @Override
    public void tuneChannel(int channel) {
        System.out.printf("Changing digital signal to channel %d\n", channel);
    }

    interface Provider extends TvSource.Provider {
        @Override
        default TvSource tvSource() {
            return new Cable();
        }
    }
}

// Here compose the code above to instantiate a TV with a Cable TvSource
public class Main {
    public static void main(String[] args) {
        new MainContext().tv().turnOn();
    }

    static class MainContext implements TV.Provider, Cable.Provider { }
}

A few notes about this example:

  • The TV class depends on a TvSource, but it doesn’t know any implementation.
  • The TV.Provider extends the TvSource.Provider because it needs the tvSource() method to build a TvSource, and it can use it even if it’s not implemented there.
  • The Terrestrial and Cable sources can be used interchangeably by the TV.
  • The Terrestrial.Provider and Cable.Provider interfaces provide concrete TvSource implementations.
  • The main method has a concrete implementation MainContext of TV.Provider that is used to get a TV instance.
  • The program requires a TvSource.Provider implementation at compile time to instantiate a TV, so we include Cable.Provider as an example.

Details and Rationale

We’ve seen the pattern in action and some of the reasoning behind it. You might not be convinced that you should use it by now, and you would be right; it’s not exactly a silver bullet. Personally, I believe that it’s superior to the service locator pattern in most aspects. However, when compared to DI frameworks, one has to evaluate if the advantages outweigh the overhead of adding boilerplate code.

Providers Extend Other Providers to Locate Their Dependencies

When a provider extends another one, dependencies are bound together. This provides the basic foundation for static validation which prevents the creation of invalid contexts.

One of the main pain points of the service locator pattern is that you need to call a generic GetService<T>() method that will somehow resolve your dependency. At compile time, you have no guarantees that the dependency will ever be registered in the locator, and your program could fail at runtime.

The DI pattern doesn’t address this either. Dependency resolution is usually done through reflection by an external tool that is mostly hidden from the user, which also fails at runtime if dependencies aren’t met. Tools such as IntelliJ’s CDI (only available in the paid version) provide some level of static verification, but only Dagger with its annotation preprocessor seems to tackle this issue by design.

Classes Maintain the Typical Constructor Injection of the DI Pattern

This is not required but definitely desired by the developer community. On one hand, you can just look at the constructor, and immediately see the dependencies of the class. On the other hand, it enables the kind of unit testing that many people adhere to, which is by constructing the subject under test with mocks of its dependencies.

This is not to say that other patterns are not supported. In fact, one might even find that Mixin Injection simplifies constructing complex dependency graphs for testing because you only need to implement a context class that extends your subject’s provider. The MainContext above is a perfect example where all interfaces have default implementations, so it can have an empty implementation. Replacing a dependency only requires overriding its provider method.

Let’s look at the following test for the TV class. It needs to instantiate a TV, but instead of calling the class constructor, it is using the TV.Provider interface. The TvSource.Provider has no default implementation, so we need to write it ourselves.

public class TVTest {
    @Test
    public void testWithProvider() {
        TvSource source = Mockito.mock(TvSource.class);
        TV.Provider provider = () -> source; // lambdas FTW
        provider.tv().turnOn();
        Mockito.verify(source, times(1)).tuneChannel(42);
    }
}

Now let’s add another dependency to the TV class. The CathodeRayTube dependency works the magic to make an image appear on the TV screen. It is decoupled from the TV implementation because we might want to switch to LCD or LED in the future.

public class TV {
    public TV(TvSource source, CathodeRayTube cathodeRayTube) { ... }

    public interface Provider extends TvSource.Provider, CathodeRayTube.Provider {
        default TV tv() {
            return new TV(tvSource(), cathodeRayTube());
        }
    }
}

public class CathodeRayTube {
    public void beam() {
        System.out.println("Beaming electrons to produce the TV image");
    }

    public interface Provider {
        default CathodeRayTube cathodeRayTube() {
            return new CathodeRayTube();
        }
    }
}

If you do this, you will notice that the test we just wrote still compiles and passes as expected. We added a new dependency to the TV, but we also provided a default implementation. This means that we don’t have to mock it if we just want to use the real implementation, and our tests can create complex objects with any level of mock granularity that we want.

This comes in handy when you want to mock something specific in a complex class hierarchy (e.g., only the database access layer). The pattern enables easily setting up the kind of sociable tests that are sometimes preferred to solitary tests.

Regardless of your preference, you can be confident that you can turn to any form of testing that better suits your needs in each situation.

Avoid External Dependencies

As you can see, there are no references or mentions to external components. This is key for many projects that have size or even security constraints. It also helps with interoperability because frameworks don’t have to commit to a specific DI framework. In Java, there have been efforts such as JSR-330 Dependency Injection for Java Standard which mitigate compatibility issues.

Avoid Reflection

Service locator implementations don’t usually rely on reflection, but DI implementations do (with the notable exception of Dagger 2). This has the main disadvantages of slowing down the application startup because the framework needs to scan your modules, resolve the dependency graph, reflectively construct your objects, etc.

Mixin Injection requires you to write the code to instantiate your services, similar to the registration step in the service locator pattern. This little extra work completely removes reflective calls, making your code faster and straightforward.

Two projects that recently caught my attention and benefit from avoiding reflection are Graal’s Substrate VM and Kotlin/Native. Both compile to native bytecode, and this requires the compiler to know in advance of any reflective calls that you will make. In the case of Graal it is specified in a JSON file that is hard to write, can’t be statically checked, can’t be easily refactored using your favorite tools. Using Mixin Injection to avoid reflection in the first place is a great way to get the benefits of native compilation.

Minimize Runtime Behavior

By implementing and extending the required interfaces, you construct the dependency graph one piece at a time. Each provider sits next to the concrete implementation, which brings order and logic to your program. This kind of layering will be familiar if you’ve used the Mixin pattern or the Cake pattern before.

At this point, it might be worth talking about the MainContext class. It is the root of the dependency graph and knows the big picture. This class includes all provider interfaces and is key to enabling static checks. If we go back to the example and remove Cable.Provider from its implements list we’ll see this clearly:

   static class MainContext implements TV.Provider { }
//  ^^^
//  MainContext is not abstract and does not override abstract method tvSource() in TvSource.Provider

What happened here is that the app didn’t specify the concrete TvSource to use, and the compiler caught the error. With service locator and reflection-based DI, this error could have gone unnoticed until the program crashed at runtime—even if all unit tests passed! I believe these and the other benefits we’ve shown outweigh the downside of writing the boilerplate needed to make the pattern work.

Catch Circular Dependencies

Let’s go back to the CathodeRayTube example and add a circular dependency. Let’s say we want it to be injected a TV instance, so we extend TV.Provider:

public class CathodeRayTube {
    public interface Provider extends TV.Provider {
//  ^^^
//  cyclic inheritance involving CathodeRayTube.Provider
        default CathodeRayTube cathodeRayTube() {
            return new CathodeRayTube();
        }
    }
}

The compiler doesn’t allow cyclic inheritance and we are not able to define this kind of relationship. Most frameworks fail at runtime when this happens, and developers tend to work around it just to make the program run. Even though this anti-pattern can be found in the real world it is usually a sign of bad design. When the code fails to compile, we should be encouraged to look for better solutions before it’s too late to change.

Maintain Simplicity in Object Construction

One of the arguments in favor of SL over DI is that it’s straightforward and easier to debug. It is clear from the examples that instantiating a dependency will just be a chain of provider method calls. Tracing back the source of a dependency is as simple as stepping into the method call and seeing where you end up. Debugging is simpler than both alternatives because you can navigate exactly where dependencies are instantiated, right from the provider.

Service Lifetime

An attentive reader might have noticed that this implementation doesn’t address the service lifetime problem. All calls to provider methods will instantiate new objects, making this akin to Spring’s Prototype scope.

This and other considerations are slightly out of the scope of this article, as I merely wanted to present the essence of the pattern without distracting detail. Full use and implementation in a product would, however, need to take into account the full solution with lifetime support.

Conclusion

Whether you’re used to dependency injection frameworks or writing your own service locators, you might want to explore this alternative. Consider using the mixin pattern that we’ve just seen and see if you can make your code safer and easier to reason about.

Understanding the basics

  • What is meant by inversion of control?

    We talk about inversion of control when a piece of code is able to delegate behavior to another component that is not known at the time of writing that code (usually through well known interfaces and extension points).

  • What is dependency injection?

    Dependency injection is a pattern by which we can achieve inversion of control at the component level of a system. A class only declares the components it needs to do accomplish its goal, not how to find or create those components.

Hire a Toptal expert on this topic.
Hire Now
Martin Coll

Martin Coll

Verified Expert in Engineering

Buenos Aires, Argentina

Member since July 10, 2014

About the author

Martin is an all-around full-stack developer with years of experience in a wide range of technologies including Java, C#, Python and others.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

PREVIOUSLY AT

Microsoft

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.