BattlefyBlogHistoryOpen menu
Close menuHistory

Dependency Injection with NodeJS

Feng Wu June 15th 2018

How we achieved better separation of concerns with factory function pattern

If you work with NodeJS, you probably have been using require() to get all the dependencies for your code. It can get in the way during unit testing but proxyquire made it simple again.

So, why would you want to read more about dependency management? What if I told you you might be using require for something it’s not intended for? Furthermore, by adopting this dependency injection pattern, you will enjoy more modular code, better ways of dealing with dependency exceptions, and an easier time of writing unit tests.

Stateful Dependency

Imagine you are building a backend server with controller/service/model layers. Models need database connection to read or write to it. Your database connection can be in a module like:

const dbDriver = require('some-db-driver');  
const connection = dbDriver.init('db-uri');  
module.exports = connection;

Then in your model modules you just:

const dbConnection = require('./dbConnection');  
module.exports.find = dbConnection.table('cupcakes').find;

Everything is working perfectly. Nice! But, the next day the db driver library released a new version and told you driver.init() is a very dangerous || expensive || slow operation and should be called only once when your server is running.

Now, how do you ensure dbDriver.init is called only once when you have 7 model modules all have require('./dbConnection')?

Module State Managed by Require Cache

After digging into posts about how require() works, you now know require utilizes some caching mechanism. So when you require the same file 7 times it doesn’t blindly load the file again and again from the disk or driver.init() 7 times. Basically, your dbConnection module will behave more or less like an initialized singleton, shared by your model modules who all required it.

The singleton maintained by require.cache might be enough for many use cases. There are pitfalls to be avoided. And you have to trust that the other part of the application doesn’t invalidate the cache at some point. The official document states that:

Multiple calls to require('foo') may not cause the module code to be executed multiple times.

So there is no guarantee that multiple require('foo') will always result with the same object. To some people like me, it feels that require is not designed for managing module states.

Centralized Dependency Management

Instead of relying on require.cache, we manage our dependency in a more manual and centralized fashion. Our model modules all look like:

const statelessDeps = require('lib');  
module.exports = (dbConnection, otherStatefulDeps) => {  
  const self = {  
    find() {  
      return dbConnection.table('cupcakes').find();  
    }  
  };  
  return self;  
}

And we need a place to pass all those dependencies into those modules. It could be server.js :

const dbDriver = require('some-db-driver');  
const CupcakeModel = require('./models/cupcake');  
const CupcakeService = require('./services/cupcake');  
const CupcakeController = require('./controllers/cupcake');try {  
  const connection = dbDriver.init('db-uri');  
  const cupcakeModel = CupcakeModel(dbConnection);  
  const cupcakeService = CupcakeService(cupcakeModel, trashbinModel):  app.use('/cupcakes', CupcakeController(cupcakeService, orderService));  
} catch (error) { //catch driver initialization error here}

In this example we manage the dependencies of modules manually and explicitly. When implementing those individual modules, you’ll always assume stateful dependencies are passed to you via the factory function pattern. No need to worry whether your dependencies are in the proper state anymore!

Conclusion

We use factory function pattern to inject stateful dependencies on all our backend modules. It helps us separate the concerns of managing states of dependencies versus using them to implement business logic.

One concrete example of such benefit is that centralized dependency management makes it easier for exception handling. Instead of duplicating error handle codes for dependencies where they are used, now it’s all in one and only one place.

A bonus point of using factory pattern is that unit testing those modules are as easy as making some plain old JS objects as mock dependencies and passing them in. No need to find a proxyquire alternative when migrating to import !

Factory function pattern is a big topic. You can read more about it here.

2024

  • MLBB Custom Lobby Feature
    Stefan Wilson - April 16th 2024

2023

2022

Powered by
BATTLEFY