Four principles of scalability

So, after careful reflection on the merits and demerits of a single page application, you’ve decided to create one (you did reflect, right? Good.). You may be tempted to dive in without thinking too much about the overall architecture of your app. What you don’t know now is that your little app will be a hit, and in two years the functionality it encompasses and the team that is developing it will multiply by a factor of ten. Before you know it, you will wish that you had taken the time to plan it out a little more. So before that happens to you, take some time and think with me through one type of scalable SPA architecture. We will go through four principles that will help you build a SPA that will stand the test of exponential growth.

Principle #1: group functionality into modules

Every application has related functional units that can be grouped into modules. This functional separation of concerns exists a level above the separation of Model, View, etc. Even the ubiquitous to-do list might have the following natural functional groupings if you were making an actual application:

  1. Account (account creation and modification, login)
  2. List (creating and storing of to-dos)
  3. Admin (internal user admin, reporting and statistics)

By grouping these similar functions into modules, you will lay the foundation for scalability in your SPA. By enforcing a functional separation of concerns, you will make it much easier if you ever have to

A. set up multiple dev teams to work on the same SPA - Each dev team can take responsibility for a module. - Each team will have clearly-defined responsibilities. B. combine your SPA with another SPA - You’ve already identified your functional groups and know the interactions between them. - It will be much easier to swap out your Account module for new Account functionality, for example, than it would have been otherwise. C. add an entirely new set of functionality

Principle #2: establish loosely-coupled intra-module communication

Now that you have grouped your functionality into modules, how do you use that functionality between modules? The naive approach is for each module to set up an explicit dependency on each other module that it needs to use (List might need to get Account information, for example). This would be a mistake; the larger your SPA gets, the more you need to maintain module independence. You need to find an alternative in the framework you are using that provides looser coupling. One alternative that I especially like is a central messaging bus.

A central messaging bus provides for your SPA modules what HTTP provides for a micro-services architecture. It has channels (like addresses) which pass requests on to the module in charge of that channel, and returns the response data to the calling code. In addition, it usually also provides event binding so that modules can emit events to be consumed elsewhere in the application. This loose coupling is more flexible than the tight coupling of regular dependencies. It encourages the development of a separate, limited external API to be consumed by the application in general, while abstracting away the details about what goes on inside the module.

Principle #3: publish contracts for your modules

You could stop with principle #2 if your app is small enough. But as it grows in complexity and evolves over time, you will notice that you occasionally forget to update the List module when you’ve made a breaking change to the Account module’s requests or events. This is pretty easy to do in a loosely-coupled application because your development tools don’t necessarily know when such a change has taken place.

What you need is a way to know if the public API of a module has changed. This is why you publish a contract for your module. Any changes to the contract should be identified as either breaking or non-breaking changes. Versioning of the contract allows you to identify when you must update the message calls on other modules.

Why not just scrap the idea of loose coupling in general and revert to an explicit dependency? You would at least know when an API has changed (your project would fail to build) and could update accordingly. The main reason is that you could then never achieve our next principle.

Principle #4: let each team push non-breaking updates separately

Your application how has four distinct user-facing modules. Each of these functional groups is adding features and fixing bugs to meet the demands of your users. In order to cope with the sheer volume you have established a development team for each module. Now how do you get these great features out to your users without coordinating a massive release of everything at once? Fortunately for you, you didn’t scrap the idea of loose coupling. If you had, you’d be hamstrung by your explicit dependencies. Every time team A made a change, teams B, C, and D would have to update their dependencies in order to get the new code.

Instead, you take one final step of decoupling: instead of a single build process to produce a single JavaScript file with your entire app in it, you set up a build process per module, and one for the app as a whole with lazy-loaded modules. This works as follows:

  1. Each module is built to its own package with all of its unique dependencies included. This package is deployed to a place where the main application can retrieve it.
  2. The application shell is built to a package that doesn’t include the module packages. Instead, it is coded to retrieve a list of modules and the current version’s deployment location.
  3. The application shell is served from the app’s index page. When it starts up, it gets the list of modules, retrieving each module package as needed from the configured deployment location.

Now when a module team has an update to deliver to the users, as long as it doesn’t break the contract, it can publish autonomously. It builds a new module package, uploads it, and updates the module’s deployment listing. The next time a user loads the application, it will load the new version, and the user will get all the shiny new functionality and bug-fixes.

Slay the dragon I hope these principles and examples have helped you to think about scalability in SPA applications. Even if you never need all of these principles, I hope you take away how they build on one another to reach the goal of a single scalable SPA with flexible deployment processes. Now, go slay your SPA dragon!