Dependency Inversion with Redux-Thunk & Typescript
In this article I will explain how we used Dependency Inversion to decouple all I/O modules in our React-Redux-Application resulting in the ability to completely change the Apps behavior by setting a Boolean during bootstrapp time.
This is my first blog article, so feel free to give me feedback — and of course I’d like to hear your opinions and solutions on the topic.
One of my favorite and in my opinion most powerful software principles is the Dependency Inversion Principle — or short DIP. It is the D in the famous SOLID Principles and it drives most patterns I know that take care of decoupling and modularization. Even more: Usage of the DIP actually makes the difference between a real OO-Design and a procedural one according to Robert C. Martin:
Indeed, it is this inversion of dependencies that is the hallmark of good object-oriented design. […]. If [a program’s] dependencies are inverted, it has an OO design. If its dependencies are not inverted, it has a procedural design.
Robert C. Martin — Agile Software Development: Principles, patterns and practices
Now some people might stop and say: But this article is about redux which celebrates functional programming. What do I care about OOP and OO-Design?
In my opinion it doesn’t matter whether you use FP or OOP. Understanding and applying the DIP will help your tremendously in both disciplines, since you will always have to handle dependencies somehow — wether it is a class or a function.
What is Dependency Inversion?
Before we go into the details, I’d like to give a brief introduction to dependency inversion. If you are already familiar with it, you can skip this section.
As the name states, dependency inversion has to do with dependencies of classes / functions — more precisely with the abstraction of those dependencies.
Lets look at an example: We use a classical App-Structure which has some sort of View, a Data Service and an API or Database-Service:
The flow of control is like this: the View reacts to a certain user-interaction by calling a function on the DataService which itself calls a function of the API or Database to request some raw data, applies some domain rules before or after the call and then returns the data to the view.
What we can see on the Diagram really well is that the dependency-chain exactly follows this control-flow — the View depends on the DataService which itself depends on the API.
This is a problem though: As the DataService is part of our Domain and thus contains the fundamental rules of our application, it should not depend on anything else but other parts of the domain layer so that it stays testable, isolated an free of unecessary changes. After all, we expect the least changes to our domain and we want it to be the most stable part of our application, right?
With this current way of implementation however, if the API changes only a bit, most likely the domain has to change as well. And since the view depends on the Domain, this will most likely have to change as well — meaning we’d have to touch several or all parts of our application if only one minor detail on one of the outer layers of our application changes. This kind of cascading changes are a lot of work that is actually not really necessary.
Now lets see what can be done using an interface and thus applying DIP:
By simply putting the interface DataRepository between the domain service and the API, we already achieved a decoupled architecture. What is important here: The domain service owns that interface (or: the interface is part of our domain) while the API only implements it from the outside.
That means the domain now only depends on itself (this is what we wanted) and only specifies what it needs from the outside world — but not how it needs it. The DataService can work with anything that implements the DataRepository-Interface — let it be a real database, a HTTP-API or simply an In-Memory-Repository with mock data. All that needs to be done is inject the right dependency during the bootstrapping phase of our app.
Further: What we achieved now is that the ApiService actually depends on the domain rather than the domain depending on the API. That means the direction of the dependencies is inverse to the flow of control (red vs. black arrows) — hence the word Dependency Inversion.
How is this done with Redux-Thunks?
Now let’s transfer this to the classical react-redux application: There are some containers+components (the view) which dispatches actions (the domain) to redux to change the state. Some of those actions are asynchronous as they need to get certain data from an asynchronous API. A simple way of doing that is by using the redux-thunk middleware. Therefore, a simple app architecture could look like this:
The implementing code might look like this (of course, it is one of our beloved Todo Apps):
As in the abstract example before, the container (view) directly depends on the redux-thunk action (domain) which itself directly depends on the fetch-method (the API).
The same problems from the abstract example arise now more clearly:
- The direct coupling lets the rather stable domain-code (the thunk) depend on the usually rather unstable API. Whenever the API changes just a little bit (a path changes), we will have to touch the thunk and it’s tests as well.
- By directly coupling the action to the API, testing this thunk is very hard (right now you would have to somehow mock the global
- While developing, we don’t always have the luxury of an already existing or stable API and prefer to work with Mock-Data — our only chance to do so with this approach is to start some sort of JSON-Mock-Server that actually answers the HTTP-request on the correct URL.
So here we also want to use dependency inversion to abstract the API behind an interface and let the thunk only depend on that interface.
Lets clean it up
For the gists in this example I pulled out the most important code snippets. You can find a full working implementation of the example in my Github Repository
The target structure should look something like this:
So first, lets pull out a TodoRepository Interface and create an APIService named HttpTodoRepository implementing it. Note how this simple refactoring already separates the role (being a repository for data) from the implementation and the used technology (use HTTP to fetch the data):
Now we need to somehow pass in the HttpTodoService as TodoRepository to the thunk. We could do this by adding it to the parameters-list of the thunk itself — but that would mean all containers dispatching the action have to know about the service, and that is simply not their task (think Single Responsibility Pattern).
Luckily, redux-thunk allows us to specify a third parameter during bootstrap which will be passed to every called thunk. We can simply use that parameter as a dependency-injection mechanism:
And now we can use it in the thunk itself like so:
The TodoRepository is now the third parameter to the thunk-function. Further, the thunk-action knows nothing about the concrete implementation in HttpTodoRepository anymore. We successfully decoupled the domain from the API.
To see the real power of this abstraction, lets now also write a MockTodoRepository implementing our TodoRepository interface:
All that is left to do now is pass the MockTodoRepository instead of the HttpTodoRepository when our store is created and the app suddenly works completely independent of internet connections and without a running API.
This can be further improved by using a environment variable which will be passed from the outside to decide which of the two Repository-Implementation will be used:
The setting of this variable can be completely dynamic. We usually do it by using Webpack’s DefinePlugin where we set MOCK_API to either true or false depending on a given parameter at build time. This way, we can start our app in either mode from the CLI (
npm start vs.
With one simple variable, we can now completely change our apps data-behavior while not changing its logical behavior at all. With this architecture, changing your API from REST to GraphQL or from HTTP to HTTP2 is nothing more than writing a new service implementing the DataRepository Interface.
But how does this scale?
We are actually using this architecture in a bigger application and made some very good experiences with it. To be able to scale, we are actually not injecting the APIService itself, but (also environment specific) Factory holding all our dependencies. That way, every thunk has the possibility to request whatever dependency it needs instead of just the API.
When our API wasn’t available for two weeks, we still could develop our frontend app easily and even let our POs test it logically by faking backend behavior with an In-memory API (so Create/Update/Delete operations where possible).
- you can use redux-thunk’s
thunk.withExtraArgument(deps)function as a dependency injection mechanism
- to avoid tight coupling, let your thunk (or your domain) specify the dependencies it needs as interfaces, not as already concrete implementations
- implement those interfaces elsewhere and pass them into the thunks with the function above during bootstrapping
- use environment variables (e.g. set by Webpacks DefinePlugin) to decide which implementation of your domain-interfaces you pass into the app to control your apps behavior on the highest level