š The Ultimate Guide to Dependency Injection Without Libraries!

Introduction
Greetings, fellow developers!
Today, we embark on a journey into the realm of dependency injection in JavaScript, with no external libraries.
Brace yourself for an immersive exploration of this powerful technique as we delve into creating Janeās Coffee Shop. With a touch of pragmatism and a sprinkle of technical finesse, weāll unravel the secrets of dependency injection, enabling you to craft flexible, scalable, and maintainable JavaScript applications.
Software, as opposed to hardware, is meant to be easily changeable. Hence the name.
Unveiling the Essence of Dependency Injection
Before we dive into the practical implementation, letās understand the essence of dependency injection.
Imagine Janeās Coffee Shop, a bustling hub for caffeine enthusiasts. Jane, a coffee connoisseur, realises that she canāt excel in every aspect of her business single-handedly. Instead, she gets help from a coffee vendor (Coffee Guru) that provides the beans and a barista (Barista Bob) to turn those beans into āļø. This allows Jane to focus on what she does best ā running her business and serving delightful cups of coffee to her loyal customers ā by delegating the responsibility of buying the beans, roasting them and finally grinding them to make coffee.
Much like how Jane depends on others to help with her business, in software development, your code has different components, objects and modules that depend on each other to function correctly.
Setting the Stage
To kickstart our journey, we lay the groundwork for our application. We initialise a simple NodeJS project called coffee-shop
with the following directory structure. The code for it is available at https://github.com/vjrngn/coffee-shop

To express Janeās coffee shop in code, weāll create a few classes. The CoffeeGuru
class is our vendor. It provides all the raw materials required to make coffee.
class CoffeeGuru {
getCoffeeBeans() {
// Fetch the finest coffee beans
}
getWater() {
// Retrieve purified water
}
getMilk() {
// Procure fresh milk
}
}
The Barista
class is responsible for actually making the coffee.
class Barista {
brewCoffee() {
// ... makes amazing coffee
}
}
Finally, we create a CoffeeShop
class to bring everything together. Letās actually see how they all fit together.
class CoffeeShop {
constructor(barista) {
this.barista = barista;
}
makeCappuccino() {
return this.barista.brewCoffee();
}
}
Harnessing the Power of Dependency Injection
Jane uses CoffeeGuru
to procure all the materials she requires so letās use an instance of CoffeeGuru
by āinjectingā it into the Barista
.
class Barista {
constructor(coffeeGuru) {
this.ingredientProvider = coffeeGuru;
}
brewCoffee() {
// ...
}
}
In code, injection means constructing the class with an instance of CoffeeGuru
. This is known as constructor injection. There are other forms of injection such as method injection and property injection but the easiest and most widely used is constructor injection.
As simple as that! Letās prepare some hot, piping cappuccino!
Weāll update our Barista
so they can use all the wonderful beans and milk and other ingredients from CoffeeGuru
class Barista {
constructor(coffeeGuru) {
this.ingredientProvider = coffeeGuru;
}
brewCoffee() {
const coffeeBeans = this.ingredientProvider.getCoffeeBeans();
const water = this.ingredientProvider.getWater();
const milk = this.ingredientProvider.getMilk();
// Brew the perfect cup of cappuccino using the ingredients
// ...
}
}
Bringing it all together
Now that we have our vendor and barista all ready to go, we can start actually selling our coffee to customers.
We start by creating an instance CoffeeGuru
. Next, we create an instance of Barista
, injecting it with an instance of CoffeeGuru
. Finally, we create our CoffeeShop
and make it use (or inject) the Barista
.

With the Coffee Guruās expert guidance and the art of dependency injection, Jane can now serve her customers with the finest caffeine experience.
Phew! That was a LOT of work just to serve āļø.
āWhy go through all this hassle? š¤ā, you may wonder. Good question!
To see why this technique is so powerful, letās say Jane decides her vendor is too expensive and she can no longer buy from them. In this contrived example, this change is simple. However, it showcases a very common occurrence in software development where things change frequently. For example, we decide to use Stripe over RazorPay for accepting customer payments, use raw SQL instead of an ORM to perform database queries etc...
You will very soon realise, dealing with all this without DI becomes untenable. Software, as opposed to hardware, is meant to be easily changeable. Hence the name. DI is one of the techniques that allows software to beā¦ āsoftā
To deal with a change in vendor, all we would need to do is create another class, say CoffeeCo
, and use that instead of CoffeeGuru
.

By doing so, we only needed to change the vendor and nothing about the coffee making process needed to change! Awesome right!?š„
Letās do a quick refactor
The astute amongst you would have noticed I named the variable this.ingredientProvider
in the Barista
class instead of this.coffeeGuru
.
class Barista {
constructor(coffeeGuru) {
this.ingredientProvider = coffeeGuru;
}
brewCoffee() {
const coffeeBeans = this.ingredientProvider.getCoffeeBeans();
const water = this.ingredientProvider.getWater();
const milk = this.ingredientProvider.getMilk();
// Brew the perfect cup of cappuccino using the ingredients
// ...
}
}
Since Barista
doesnāt really care about which exact vendor provides the beans, naming our variable this way allows us to reference the provider in a more abstract form rather than using this.coffeeGuru
as the vendor may change in the future. We can now update our Barista
class like so:
class Barista {
constructor(ingredientProvider) {
this.ingredientProvider = ingredientProvider;
}
brewCoffee() {
const coffeeBeans = this.ingredientProvider.getCoffeeBeans();
const water = this.ingredientProvider.getWater();
const milk = this.ingredientProvider.getMilk();
// Brew the perfect cup of cappuccino using the ingredients
// ...
}
}
This change is minor and doesnāt affect the functionality of the Barista
class. However, since code is read more than it is written, this enhances intent, as the dependency ingredientProvider
clearly states that the Barista
needs a vendor to provide ingredients but doesnāt care about a specific vendor such as CoffeeGuru
.
If you would like to understand further about how dependency injection helps with maintaining and testing code, subscribe to get notified of my next post where I talk about exactly this concept.
If you liked this article and you learned something, donāt be shyā¦ show some love with a š . It really helps me reach a wider audience and motivates me to write more useful articles. šš»
Until next timeā¦