Dependency injection in Angular isn’t worth it. More lessons learned from scaling PayPal Checkout.

When we started building PayPal’s Checkout front-end in Angular 1.x, we took the classic ‘all in’ approach of trying out absolutely everything the framework had to offer. Directives, these are cool! Dependency injection, looks interesting! Two way binding? Well, maybe not… but the rest seems neat!

Not that there’s necessarily anything wrong with this approach — it’s difficult to know which framework features are going to be a hit and which are going to be a miss, before you throw them at the wall and see which stick.

Some features are great when developing some internal tooling, others shine when building an external product with a ton of features. Sometimes things work well when you’re building an app on your own, but end up causing huge problems when your dev teams scales up further than one or two people.

If I had to pick one complaint about Angular above all others, it would be that it has too many features like this — they look useful, but they don’t really scale when you have a lot of contributors and a lot of code to manage.

Uh oh, more Angular complaints?

After a previous article I wrote on the way we’re using Angular at PayPal, a lot of people were saying: ‘well done guys, looks good, but you’re essentially writing React code in Angular’.

That’s kind of true, to an extent — we’re using (or at least, trying to use) a purely component based, functional approach to building Angular apps, and avoiding things like two way binding between components, among other things. It’s not perfect, but by keeping some fairly heavy constraints on what we do use Angular for and what we don’t, we’ve ended up with something fairly sane.

Despite my rants, I’m not an Angular hater by a long shot. I think it has enough great parts to build a fantastic app, including:

So yeah, I think the above few things are still well polished enough to make Angular a great framework to work with.

So dependency injection didn’t make the cut?

Well… no.

After using Angular’s dependency injection for more than a year, my opinion is this:

DI is a really useful way of decoupling parts of your app when used specifically and intentionally for that purpose, but in Angular 1.x the trend of using DI for everything is massive overkill — and its implementation leaves a lot to be desired.

Let’s take a couple of steps back… what even is DI?

Aside from being one of those things that reads like Comp-Sci lecture notes in the Angular docs, dependency injection is actually incredibly straightforward.

Let’s break out some pseudocode — say I have a notification component.

let notificationComponent = NotificationComponent({
title: 'My Notification Widget!'
});

What if I want that notification component to do some logging every time we ask it to display a new notification?

I could just have my component call whatever logger it wants — like console.log(), or AbstractLoggerBeanFactory.getInstance().log()

But that’s not great, because now my component is tightly coupled to some particular logging library or implementation. Besides which, logging is really only a secondary concern for this component, and having it make the decision of which logger to use seems like an awfully big responsibility for such a small component. Also — what if five different components I pull in each try to use a different logger? Then I’m kinda screwed.

So instead, let’s just pass down the logger we want it to use to our component:

let notificationComponent = NotificationComponent({
title: 'My Notification Widget!',
logger: window.console
});

That’s it. That’s dependency injection. I pass in the dependency, rather than my component deciding which dependency it wants to use. Pretty simple!

Of course, there are more things to consider; like, for example, the component could fall back to a default logger if one isn’t passed — and I also have to make sure whatever logger I use has a standard interface that the component can understand.

But at its core, DI is pretty simple. It’s just passing down dependencies.

If DI is so great, what’s wrong with Angular using it?

There are a number of reasons I’m firmly convinced that DI in Angular 1.x is overkill.

1. Everything is an injected dependency

Every time you create an Angular factory, service, value, directive, controller — any time you register anything with Angular — it becomes a dependency which can be injected.

This dilutes out the utility of DI, and moves it from ‘I want to decouple this particular library code’ to ‘everything is an injected dependency’.

So, that means everything is magically decoupled now, right?

Well, no. It’s not really that simple. Once everything is DI, you tend to stop thinking altogether about how it’s really helping you, and you end up writing dependencies that are still tightly coupled to your app , even though technically they’re using a dependency injection pattern.

I’m a strong believer that a framework can’t just give you decoupling entirely for free. If you want to decouple your code, you actually have to be rigorous about:

Just saying ‘let’s make everything an injected dependency’ doesn’t really give you any of the above, you still have to work for that.

2. There’s a whole custom module system to support it

Let’s take a look at some Angular code:

require('./mydependency');angular.module('mymodule', ['mydependency'])
.factory('SomeHelper', function(SomeHelperFromMyDependency) {
return function() {
...
};
});

OK, where to begin…

Firstly, there’s so much framework-specific boilerplate here. Do I really need all of this, just to do something similar to my example above with the logger? Doesn’t all of this code just obscure what’s really happening?

Secondly, how about interoperability with commonjs or es6? Well… you can get part of the way there, but whatever you do, you still end up with a system where you have three different things going on just to get a dependency pulled in:

Pro tip: if your framework needs to re-invent modules to get where it’s going, it’s probably overkill, especially when those modules end up being tightly coupled to the framework itself.

3. Dependency injection cascades down through your app

Angular provides some very useful things, like $http, $q, etc. to help you build your app. The problem is, any of your code that wants to use any of these needs to itself be a DI. The only way I can get access to things like $q is by specifying them as arguments to my factory:

angular.module('mymodule', [])
.factory('SomeHelper', function($q) {
return function() {
...
};
});

So, I can’t use any of the cool stuff angular provides, without putting pretty much all of my code under DI.

Consider this: DI is supposed to make my code decoupled. So great — I’m totally decoupled now from $q and I can re-implement it with the same interface whenever I want.

But to get to that point, I had to 100% totally couple myself with Angular, because to even use $q, all of my code has to buy into its DI system. So I’m robbing Peter to pay Paul — and I’m paying back more in interest than I’m saving in the short term.

4. Angular doesn’t even enforce the dependencies I specify

This one really gets me. Let’s say I have:

Using conventional module thinking, we can assume that since myapp has specified mymodule as a dependency, it will have access to $SomeComponent. That would be totally correct.

Unfortunately, the way Angular is implemented, it’s also possible for mymodule to get access to $SomeController, even though it’s part of the parent module.

So using DI, I can code myself into a hole by writing all kinds of circular dependencies, which would be much more easily preventable using ES6 or commonjs imports.

Why is this even the case? It’s because —

5. Angular registers all dependencies in a global hash-map

So even if you’re meticulous about defining your Angular modules hierarchically, at the end of the day, Angular will just treat them as essentially, globals.

This has all of the implications you’d expect:

Here’s a reference DI implementation using Angular’s approach to demonstrate how little this helps us:

window.dependencyManager = {

dependencies: {},

register(name, dependency) {
this.dependencies[name] = dependency;
},

get(name) {
return this.dependencies[name];
}
};

6. Specifying dependencies using parameter names is just…

By default Angular lets you specify dependencies using function parameter names. Like in this example, Angular will stringify and parse the function to figure out that it needs to pass $q to my factory:

angular.module('mymodule', [])
.factory('SomeHelper', function($q) {
return function() {
...
};
});

A lot has been said about this already, so I won’t rant too much. Aside from breaking minifiers which mangle variable names, this just breaks so many conventions that it’s amazing it was ever shipped.

Yeah, it’s possible to avoid the minifier problem by annotating your dependencies. But, I mean, this is just more icing on the boilerplate cake:

angular.module('mymodule', [])
.factory('SomeHelper', ['$q', function($q) {
return function() {
...
}];
});

7. Try figuring out where a dependency came from

Take a look at the following:

import 'module-a';
import 'module-b';
import 'module-c';

angular.module('mymodule', ['module-a', 'module-b', 'module-b'])
.factory('SomeHelper', function(x, y, z) {
return function() {
...
};
});

Can you figure out which module x, y or z came from?

Nah, me neither. It’s not actually possible without literally grepping for x, y and z in your entire codebase. Because of the problem in #5, they might not even be from module-a, module-b or module-c! So you’re pretty much screwed here.

Hmm, how can we make this easier?

import { x } from 'module-a';
import { y } from 'module-b';
import { z } from 'module-c';

export function SomeHelper() {
....
}

So much less code, and it tells me so much more!

8. Factories, Services, Providers, Values, Constants…

In my last article about this I recommended “just stick with factories”. There is no good reason for Angular to have this myriad of DI types. It’s just yet another layer of abstraction to learn, and the big secret is — they all pretty much do the exact same thing, with some very subtle differences that don’t really justify their existence.

So if you must use DI, pick one of the above and no more.

So how can we do better?

My advice is to:

Avoiding Angular’s DI is a little bit more complex than just ‘use es6’ though…

Using Angular without DI

If you’re using webpack, here are a few loaders I’ve been playing around with, to use Angular without being forced into DI. They’re pretty alpha at the moment, so use at your own risk:

In Summary

works for PayPal, as a lead engineer in Checkout. Opinions expressed herein belong to him and not his employer. daniel@bluesuncorp.co.uk

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store