Large-scale JavaScript: A monolithic service layer

When I started architecting the initial JS apps for YouNow, I sought to build structures that could be used both by our Backbone.js applications and one-off scripts/projects. These structures grew quite quickly over time; they became monolithic. In this article, I’ll examine the motivations, pros, and cons of a backend api service layer and suggest/brainstorm alternative implementations.

Backend Api Service Layer

However you implement your backend api (MVC or otherwise), it’s cumbersome to remember and re-implement the details of api endpoints. When a teammate says they’re building something that needs login functionality, do they need to reinvent the wheel or can you give them a pre-built JS function to call?

I wanted to build a JS client to solve these problems, called younow.js,  that would allow any JS app to interact with our backend. Had a growth hack that needed login functionality? No worries! Just call YouNow.Api.login() and bind to the resolved promise. The service layer, in this example, exposes the login function within a YouNow.Api namespace.

YouNow.Api.login()
  .done(function (loginData) {
    // Do what you need with the login data
  })
  .fail(function (errorMsg) {
    // Handle the error as you wish
  });

Note: For the implementation of the endpoints of this JS service layer, you could swap promises for accepting success/fail callbacks to these endpoints if you don’t like the the “boilerplate” of jquery deferreds. I personally like deferreds for their chaining/piping ability and standard interface. Greetings, haters.

Sounds good so far, no?

Pro: For every endpoint in the YouNow.Api namespace, the service layer younow.js would have documentation about the expected parameters and tuck away complexity. In particular, users of the service didn’t have to worry about whether the request was a GET or a POST, coming through the CDN or us directly, nor how to structure the url and deal with jsonp. All backend interaction was isolated to a single location.

Con: This grew quite quickly, as you could imagine. For every backend call, we needed a new endpoint in YouNow.Api.  Internally, you could abstract things into helper functions to deal with promises, jsonp, cdn urls, and $.ajax calls.  However, the file bloated to about 40kb for just a single application’s needs. Every application had its own set of api endpoints, each of which was integrated into younow.js. Ouch for maintainability.

Now imagine a simple little application like a media player (a JS wrapper and initializer of JWPlayer, for example). It maybe needs one or two endpoints (login, broadcast information retrieval). But now, it has to download the entire 40kb payload…

Alternative implementation 1: A service layer per application

The original service layer could simply be a lean collection of (ajax, cdn, promise) helper functions and every app would extend the YouNow.Api namespace with its own service layer functions on load.

Pro: This solves/controls the bloat on the main service layer.

Pro: With enforcement on when and how to extend the namespace within an app, this solution can cleanly scale out to many applications.

Con: Every app’s individual service layer could become incredibly bloated.

Con: What if two apps have a common need for a particular endpoint?

Alternative 2: Group endpoints into mixins

Login functionality could go into a LoginService mixin that apps can mixin or use directly:

// loginservice.js
YouNow.Mixins.LoginService = {
  login: function () {},
  logout: function () {}
};

// broadcastservice.js
YouNow.Mixins.BroadcastService = {
  get: function () {},
  delete: function () {}
};

Pro: The service layer becomes a series of groups of endpoints. Growth will happen in two directions: in the number of groups and the number of functions. Monolithic structures have growth in a single direction, and so this solution could scale more easily.

I’m trying to think up cons for this solution that would make the third alternative seem golden, but I quite like this method; it fixes the model bloat problem with the third alternative below.

Please comment if you can see big holes in this method.

Alternative 3: Do away with service layers entirely. Tuck communication into models.

With a service layer, Backbone models that wanted to interact with the backend simply called YouNow.Api functions. The model’s implementation was slim and trim, but you can also interpret that as the model unnecessarily passing its work off to another place. It feels like the model should own that functionality.

Doing away with service layers, we’ll have a UserModel that will now have login/logout functions and the full implementation guts (possibly extending from a BaseModel that we could use to tuck away the cdn and ajax helpers).

Pro: The appropriate model rightly owns that functionality.

Con: Over time, more and more functions will bloat this model. Not sure if this is a definitive con though; advocates of fat models might label this as a Pro.

[UPDATE] Con: Smaller apps that want to instantiate your model now require Backbone (or another framework, if you’re using one) and its dependencies. It feels unfortunate that small apps will need to buy in to a bit of bloat, but this could be a fair tradeoff for uniformity across your system.

Any app that wants login/logout functionality has to instantiate a UserModel. That doesn’t really feel like a smell. The media player app, for example, needs to login a user. So there’s your user: it’s an instance of UserModel. Now call login on it. Compare that with originally calling YouNow.Api.login().

With alternative 2 above (the mixin method), the UserModel wouldn’t explicitly have the login/logout functions. Instead, it would mixin, possibly using Cocktail.js, for which I’m a maintainer ;), the LoginService.

This could be viewed as indirection. I mean, you look at UserModel definition and don’t see login/logout methods; so how is it obvious that the UserModel has that capability at first glance? Also, technically, we’re not passing work off to another service; the LoginService gets mixed into the UserModel itself, so the UserModel object is doing its own work.

So what’s the answer?

I think the solution involves a mixture of alternatives 2 and 3.

The service layer was a good idea at the time, but not at multiple-application scale. Currently, I’ve moved to the fat model implementation (alternative #3), but haven’t incorporated grouped mixins (alternative #2) yet.

Any thoughts on yet another alternative?

Tweet at me: https://twitter.com/mrjoelkemp

Discuss it here:
Reddit: http://www.reddit.com/r/javascript/comments/1svskc/largescale_javascript_a_monolithic_service_layer/

Hacker News: https://news.ycombinator.com/item?id=6906790

Comments

comments