The Dependency Inversion Principle
The goal of this post is to teach you a pattern I use to facilitate change in the code I write. It’s called the Dependency Inversion Principle (DIP).
Let’s get to it.
The Inspiration
Changing code is difficult. A change in data structures, APIs, third-party libraries, or damn near anything else can cause ripples across your codebase well beyond the thing you’re directly changing.
Here’s an example: you have a 3rd party library that you want to upgrade. However, that upgrade introduces some breaking changes to your system. Perhaps a property that is returned or expected has changed in name or structure, or methods that you used to call have now been deprecated or removed. To upgrade is going to involve you changing not only the 3rd party library but possibly the code you wrote that interacted with that library.
I’ve noticed this pain point across multiple teams and tech stacks, so I knew it wasn’t a technology problem, but an architecture problem.
The Dependency Inversion Principle (DIP)
One principle we can use to organize our code is the DIP, which is defined as the strategy of depending upon interfaces or abstractions rather than a specific implementation.
You probably have a perfect example of the DIP pattern nearby: the humble electric socket. The electric socket is an example of an abstraction. It accomplishes this by allowing us to depend on an interface (the socket) rather than a specific implementation of electric power.
Let’s unpack that a bit. When you need to power a device do you have to worry how that power is supplied, wind, solar, gas, etc? Nah you just plug in your device and immediately start receiving power.
By placing an abstraction between how electrical power is specifically implemented and how we use this power, electrical engineers have made it easy for us to swap power sources. We can achieve this same benefit in our code by creating an electric-socket-like abstraction rather than relying on a specific implementation.
In Practice
Here’s a basic example — keep in mind this isn’t the ideal use case for DIP. Wrapping every dependency in an abstraction would be tedious. I just want to walk through something simple before getting to the real-world example.
Let’s set the stage for a common scenario in development. You have a feature that is going to require you to parse and manipulate time in your JavaScript application. So in whatever framework of choice, you might have some code like this:
import moment from "moment";
const formattedDate = (date) => {
return moment().format(date, "MM/DD/YYYY");
};
// in your template language of choice
<p>The date of your transaction is {formattedDate}.</p>;
Consider what the scenario would be if you decided to change out Moment for something else? You’d have to change your code everywhere to be:
import { format } from "date-fns";
const formattedDate = (date) => {
return format(date, "MM/DD/YYYY");
};
// in your template language of choice
<p>The date of your transaction is {formattedDate}.</p>;
Every module that is directly dependent on your specific implementation of Moment is now going to have to change. That means lots of refactoring and perhaps lots of QA testing.
Let’s visualize this example in terms of a diagram.
The Main module would be considered our presentation layer, the UI (the template, event handlers). The Mid layer in our case would be the formattedDate method and the Detail module would be the 3rd party library, Moment or date-fns.
So what you’re seeing is a tightly connected chain of components. A change in any of them is going to possibly involve changing the other two.
However, we can invert this with the DIP pattern. We create an abstraction file that owns the moment import:
// date-formatter.js
import moment from "moment";
export function dateFormatter(date) {
return moment().format(date, "MM/DD/YYYY");
}
And our UI imports from that abstraction instead of directly from moment:
// main module
import { dateFormatter } from "./date-formatter";
// in your template language of choice
<p>The date of your transaction is {dateFormatter(date)}.</p>;
Notice that the UI now has no idea moment is involved — it only knows about dateFormatter. If we swap moment for date-fns, we change one line in date-formatter.js and the UI is none the wiser. That’s the inversion: the UI depends on the abstraction, and the abstraction depends on the specific implementation — never the UI directly on the implementation.
So, our diagram now looks like this:
By using an abstraction, we’ve inverted the flow of dependency. The high-level policy, which is our UI, no longer directly depends on a specific implementation. We instead use an abstraction which wraps around the specific implementation.
Let’s dive into the real-world example.
Payment Processor DIP Example
In a recent project, I needed to add our payment processor to another part of the system. For those who’ve never had the joy of working with a payment processor, the flow typically works like this:
- Collect data from a form (UI)
- Massage the data into a specific structure in order to pass the credit card info into a payment processor such as Paypal, Stripe, WePay, etc
- The payment processor will pass back an authorization code or an error code
- You can then pass the authorization code along with any other information into another API call to make a payment
What I noticed is that I had just done something similar in another part of our system. That’s when I looked around and saw that we were doing this everywhere. Functionally, there wasn’t anything unique across each implementation. The boilerplate was repeated multiple times, coded slightly differently each time — sometimes in a chain of .then() callbacks, sometimes with async/await, sometimes via a helper function. You get the point.
In summary, lots of direct dependency on a specific implementation. So, here’s how I inverted that dependency flow with the DIP pattern.
The Presentation Layer
I no longer directly import the specific payment processor (PayPal, WePay, etc) instead, I import the interface which serves as the abstraction.
import { paymentProcessor } from 'payment-processor-interface';
processCreditCard = async (creditCardInfo) => {
const creditCardAuthorization = await paymentProcessor.processCreditCard(
creditCardInfo
);
// do more stuff
};
// template
<form>
<input ....>
<button onclick={processCreditCard}>Submit</button>
</form>
Interface aka Abstraction
This abstraction is what hides away the specific implementation of the payment processor but still allows us to access it. It does this by serving as a contract between the UI and the specific code the UI needs to perform a particular use case.
Let’s unpack the word contract a bit. Contract by definition is, a written or spoken agreement that recognizes and governs the rights and duties of the parties to the agreement. We apply the spirit of the word similarly by having this abstraction declare what it expects and what it will return.
That’s how the DIP pattern derives its power and flexibility. The UI part of the front end system knows what data format it needs to pass to the interface and what it will receive. If I end up having to change one payment processor to another, as long as I continue to fulfill that expectation then my UI layer is none the wiser to the change!
import {
wepayService as processor,
wepayAdapter as adapter,
} from "./wepay-specification";
const paymentProcessor = {
processCreditCard: function (userCardInfo) {
return new Promise((resolve, reject) => {
// the adapter is the thing that translates the payload from the UI
// to what data format this specific payment processor needs
const massagedData = adapter(userCardInfo);
return processor
.processCreditCard(massagedData)
.then((res) => resolve(res))
.catch((err) => reject(err));
});
},
processACH: function (userBankInfo) {
// another interface point stuff
},
};
export default paymentProcessor;
A couple of points about the code above:
-
I like to use aliased imports so if I change my payment processor the only thing that will change in the abstraction file is the first line, so e.g.
import { paypalService as processor, paypalAdapter as adapter } from "./paypalLowLevelModule" -
I wouldn’t call files
something-specificationI’m only doing that here to be a bit more explicit as to what’s what.
Specific Implementation
This is going to hold all the code directly related to my specific implementation of the payment processor. It’s going to import the library, call the specific endpoints, etc. It also includes the adapter — the function that translates the UI’s data shape into whatever structure WePay specifically requires.
Here’s where the word inversion comes in. Normally we would have the UI dependent on the payment processor by directly implementing it and responsible for giving it whatever it needs. We inverted that by creating an abstraction that the payment processor implements.
// wepay-specification.js
import wepayAPI from "wepay-api";
// translates the UI payload into the shape WePay expects
export function wepayAdapter(userCardInfo) {
return {
account_number: userCardInfo.cardNumber,
expiration_month: userCardInfo.expiryMonth,
expiration_year: userCardInfo.expiryYear,
cvv2: userCardInfo.cvv,
};
}
export const wepayService = {
processCreditCard: function (payload) {
wepayAPI.createCreditCard(payload);
},
};
If we switch to PayPal, we write a paypalAdapter that maps the same UI fields to PayPal’s expected format, and a paypalService that calls PayPal’s API. The abstraction layer doesn’t change. The UI doesn’t change. We still have to make sure to give the processor the data it needs, but we are no longer directly dependent on it.
Summary
The Dependency Inversion principle can be used to facilitate change in your system by making logic easier to reason about, which in turn helps with contracting and expanding a system.
We can follow this principle in front end systems by placing an abstraction between the UI functionality and the specifics of that logic. This allows us to centralize that logic which will promote reuse and can help reduce the effects of change.
A couple of things to be aware of
I wouldn’t recommend abstracting all your dependencies. What you need to do is assess the volatility of whatever you’re thinking about abstracting. The higher the chance of it changing and changing drastically the more you should consider abstracting it away.
This doesn’t always prevent change! If all of a sudden you need to add a new field to your form, you’re going to have to change all the relevant pieces, however, it should be clearer as to where you’ll add your changes, and it should cut down on the changes you’ll have to make across the system.
Where to Start
- Look for patterns in your codebase
- Where are you repeating logic?
- Where are you heavily relying on something that:
- is outside your system thus outside your control
- is a feature under active iteration
- if modified will ripple changes throughout your system
If you made it this far thanks for reading. Any questions or comments feel free to reach me at my twitter account.
👋🏾