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

Daniel Brain
9 min readApr 3, 2016

--

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:

  • Directives — specifically, using directives as components with tight interfaces and localized state (or some kind of flux derived pattern)
  • ui-router, which we’ve built on to create distributable ‘routable components’, which exist as nice self-contained flows with multiple child-routes, and that can be dropped into an app with two or three lines of code
  • Declarative templating, which picks up on model changes automatically (like all the decent modern front-end frameworks these days. One-way binding is still cool, right? Right..?)
  • Karma testing — moreover, being able to write synchronous karma tests with Angular taking care of de-asyncing everything, which for my money makes tests a whole lot more consistent and easier to reason about.

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:

  1. Creating good interfaces
  2. Avoiding shared state
  3. Isolating and encapsulating reusable code

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:

  1. One mechanism to specify file dependencies, e.g. ‘./mydependency’
  2. Another mechanism to specify angular module dependencies, e.g. [‘mydependency’]
  3. Yet ANOTHER mechanism to specify which specific factories/services you want to inject, e.g. SomeHelperFromMyDependency

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:

  1. myapp, which exposes $SomeController and depends on mymodule
  2. mymodule, which exposes $SomeComponent and has no dependencies.

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:

  1. I can just do $injector.get(‘foo’) from anywhere in my app, no matter which module I’m in and which module ‘foo’ is defined in, and so long as ‘foo’ is registered somewhere, Angular will look it up and return it for me.
  2. If I register a dependency name twice, the second register will silently overwrite the first. So, I can have two modules lay claim to the same factory name, and it will cause an untold number of bugs until I figure this out for myself— because Angular sure isn’t going to tell me.

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:

  1. Stick with ES6 or commonjs imports, by default
  2. If you need to do DI, just implement it yourself in plain-old-javascript, by passing around references (like in the example above)
  3. Then, and only then, use Angular’s DI, if there’s no other way. For example, if you want to register an $exceptionHandler, there ain’t no other way.

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

  • Angular’s DI tries to force a solution into a framework for a simple paradigm, that you can implement easily yourself when you need it
  • In doing do it makes your code a lot harder to read and reason about — even the parts which don’t need DI
  • Angular’s DI only has nominal value even when applied to code which would benefit from DI — given that it forces you to couple everything tightly with Angular modules in order to benefit from it.
  • Thankfully we can avoid this by relying more on ES6 imports or commonjs, and using DI explicitly when we need it.
  • But anyway, things look like they’re going to be much (a little) better with Angular 2.

--

--

Daniel Brain
Daniel Brain

Responses (4)