DEV Community

Timevolt
Timevolt

Posted on

Design Patterns: The Dependency Injection Matrix

The Quest Begins (The "Why")

Honestly, I used to feel like I was stuck in a boss fight where every hit I took just made the next one harder. I was building a small e‑commerce checkout service in Node.js, and the code looked harmless at first — a PaymentProcessor class that directly instantiated a StripeGateway, a PayPalGateway, and even a FraudChecker. Everything worked locally, but as soon as we needed to swap Stripe for a new provider or write a unit test, the whole thing turned into a tangled mess of new keywords and hard‑coded secrets. I spent an entire afternoon trying to mock the Stripe SDK in Jest, only to realize I couldn’t because the class was creating its own dependency inside its constructor. The frustration was real — every change felt like pulling a thread on a sweater and watching the whole thing unravel. That’s when I knew I had to find a better way, or I’d keep losing hours to avoidable bugs.

The Revelation (The Insight)

The treasure I uncovered wasn’t some mystical rune; it was a simple shift in mindset: stop letting classes create their own collaborators. Instead, hand them the objects they need from the outside. This is Dependency Injection (DI) in its purest form. By injecting dependencies, you decouple the what from the how. Your class no longer cares how a payment gateway talks to the network; it only knows it receives something that follows a PaymentGateway interface. The payoff? Instant testability, painless swaps, and a codebase that feels like it’s breathing rather than holding its breath.

I still remember the moment the lights went on. I rewrote the PaymentProcessor to accept a gateway via its constructor, wrote a fake gateway that just returned a success promise, and watched my tests pass in seconds. It felt like when Harry Potter finally learned to cast Expecto Patronum — sudden clarity, and the dementors of tight coupling fled.

Wielding the Power (Code & Examples)

Before – The Struggle

// paymentProcessor.js
const stripe = require('stripe')(process.env.STRIPE_KEY);
const paypal = require('paypal-rest-sdk');
const { FraudChecker } = require('./fraudChecker');

class PaymentProcessor {
  constructor() {
    // 👉 Hard‑wired dependencies – the root of the evil
    this.stripeGateway = stripe;
    this.paypalGateway = paypal;
    this.fraud = new FraudChecker();
  }

  async charge(amount, method, token) {
    if (await this.fraud.isSuspicious(token)) {
      throw new Error('Potential fraud');
    }

    if (method === 'stripe') {
      return this.stripeGateway.paymentIntents.create({
        amount,
        currency: 'usd',
        payment_method: token,
        confirm: true,
      });
    }

    if (method === 'paypal') {
      // … PayPal SDK call …
    }
  }
}

module.exports = PaymentProcessor;
Enter fullscreen mode Exit fullscreen mode

What’s wrong here?

  • The class creates its own Stripe, PayPal, and FraudChecker instances.
  • Swapping a gateway means editing this file.
  • Unit tests are impossible without messing with environment variables or rewriting the constructor logic.
  • Any change to the fraud checker’s interface forces a rebuild of every consumer.

After – The Victory

// paymentProcessor.js
class PaymentProcessor {
  /**
   * @param {PaymentGateway} gateway   - follows .charge(amount, token)
   * @param {FraudChecker}   fraud     - follows .isSuspicious(token)
   */
  constructor(gateway, fraud) {
    this.gateway = gateway;
    this.fraud = fraud;
  }

  async charge(amount, token) {
    if (await this.fraud.isSuspicious(token)) {
      throw new Error('Potential fraud');
    }
    return this.gateway.charge(amount, token);
  }
}

module.exports = PaymentProcessor;
Enter fullscreen mode Exit fullscreen mode

Now look at how we use it:

// stripeGateway.js
class StripeGateway {
  constructor() {
    this.stripe = require('stripe')(process.env.STRIPE_KEY);
  }
  async charge(amount, token) {
    return this.stripe.paymentIntents.create({
      amount,
      currency: 'usd',
      payment_method: token,
      confirm: true,
    });
  }
}

// fakeGateway.js – for tests
class FakeGateway {
  async charge(amount, token) {
    return { id: `test_${Date.now()}`, status: 'succeeded' };
  }
}

// usage in production
const processor = new PaymentProcessor(new StripeGateway(), new FraudChecker());
// usage in tests
const testProcessor = new PaymentProcessor(new FakeGateway(), new FraudChecker());
Enter fullscreen mode Exit fullscreen mode

Why this feels like leveling up:

  • The PaymentProcessor no longer knows which gateway it’s talking to.
  • To switch providers, you just pass a different class — no surgery on the core logic.
  • Tests become trivial: inject a fake that returns predictable results.
  • Adding a new fraud detection strategy? Just pass a different implementation; the processor stays untouched.

Why This New Power Matters

Admitting that DI changed the way I write code is an understatement. It turned a fragile, tightly coupled module into a plug‑and‑play component. Suddenly, onboarding a new teammate meant showing them an interface, not a maze of new statements. Deploying a new payment provider became a configuration change, not a code‑red emergency. And the best part? My test suite went from flaky, slow, and brittle to fast, reliable, and actually enjoyable to run.

If you’ve ever felt the dread of a “works on my machine” bug that only shows up in production because a hard‑coded API key leaked, you’ll appreciate how DI forces you to think about boundaries. It’s not silver‑bullet magic — you still need to define clear interfaces and respect them — but the payoff is immediate and lasting.

Your Turn – The Challenge

Here’s a quest for you: pick a class in your current project that instantiates its own dependencies (maybe a service that creates its own repository or a controller that builds its own logger). Refactor it to receive those dependencies via constructor injection. Write a tiny test using a mock or fake implementation. Notice how the stress melts away.

What’s the biggest “aha!” moment you’ve had when you finally stopped letting your classes create their own worlds? Drop a comment below — I’d love to hear your war stories and maybe learn a new trick of my own. Happy injecting!

Top comments (0)