The nature of building software requires that things get re-written from time to time. A user reports an issue, engineers fix it, and the whole process repeats. Eventually you might get to a point where it's necessary to do a big house cleaning. Today we're going to talk about one way to do that: decoupling components.
There are a number of benefits to this:
- Faster load times - Single page applications can be hosted entirely on a CDN.
- Smarter caching of data - Allows us to make more efficient calls to the back-end API.
- Better response time - Single page applications typically provide immediate feedback to the user instead of making them wait for the data to load.
- Easier maintenance - Single page applications also provide better decoupling from the backend resulting in a logical organization of code and division of labor.
- Faster deploys - It takes seconds to release and rollback front-end code to production.
Transitioning from a server-side rendered application to a single page application, however, can be a hefty challenge. It’s easy to rebuild an application from scratch, but this typically causes more problems in the end (see Things You Should Never Do).
Instead, we decided to rebuild our new front-end on top of our existing PHP application. Martin Fowler calls this a strangler application.
Challenges with Strangler Applications
Strangler applications can be difficult to implement. It's easy to re-write code using new tools, but that could lead down the path of perpetually refactoring things until you have a system that's even more mangled than your old one. The trick is to design a new architecture that is compatible with old code and eventually phases out the host application completely.
Another challenge in developing strangler apps is that they often need lots of abstractions to wrap around old code. These sort of adapters can be tough to implement considering their design implications. Newer, abstracted code needs to be written for the new application while remaining flexible enough to account for edge cases that will certainly come up with older code.
Decoupling Dependencies from Application Code
In our case we’re building new front-end code that needs to be compatible with older APIs, but built for newer APIs. That means that we’re actively writing adapters for service endpoints that don’t exist.
Here's a diagram that illustrates what we're doing:
Currently we serve JSON with HTML, hydrating an in-memory “database” that is abstracted from the rest of the application. The rest of the application executes CRUD operations against the broker as it would with any other data source. Eventually we’ll swap out the in-memory database with a proper API adapter and everything should just work. This abstraction on the other hand is a tricky thing to implement since the logic required to wire-up an application to load data from HTML is wildly different than the logic required to load data from a remote data source. We’re using Mesh to help with that.
Mesh is a data synchronization library that makes it easy to map how an application interacts with a data source. We use it to hide complex service logic from the front-end application, allowing us to focus on writing proper React components, and service models that won’t need to be refactored in the future.
Based on the diagram above, here’s a simplified example of how we’re currently using mesh to facilitate transactions to the backend:
The in-memory database only accepts CRUD operations, a concept which is common across just about every type of data store (even HTTP).
We should be able to swap it out for an API adapter like so:
Taking the same application code above, here's how we might map one operation to an API request:
The application bus decouples the front-end entirely from the backend, allowing us to build application code that’s easier to test and resilient to any API changes.
Abstracting the Abstracted
Abstractions, on the other hand, can sometimes be a risky thing to implement. A good one will be compatible with most of the old application codebase, but it's almost always uncertain whether they might eventually become leaky. There is, however, a way to avoid this kind of hazard, and that is to abstract the abstraction: abstractception (yes, I just made that up).
An example of abstractception is the good 'ol MVC pattern. If we follow how MVC works (in theory), we can see that models abstract business logic, and controllers abstract how views interact with models. If any sort of business logic changes, the only place we need to update is the model layer.
In our case we're adding an additional "service" to the model-view-controller philosophy I dub: "SMVC". The twist here is that models are only used to handle model relationships and business logic specific to the data they're representing. All data transactions (e.g., CRUD) are passed to the service bus.
Here's what it is:
All models and collections communicate with the service bus in a consistent way. If for instance I want to update a user model, all I'd need to execute is something like this:
All service logic specific to the user model is defined within the service layer. This guards the user model from any change in the future, and it also makes our models a bit more DRY. With the example above, we could easily create a base class for all models to use.
Note: A better implementation of this would be to use a mixin instead of a base class. That way subclasses can opt-in & out of load/save/remove methods.
This pattern enables us to do a better job designing models and collections for the front-end, not the service they're persisting data to. The other nice thing about this is that the service layer is interchangeable. If for instance we want to use these models on the back-end, we could easily do that by passing the new service bus to the constructor of a model, (just a few lines of code).
Here it is:
Decoupling the service layer from the model layer also makes things more testable.
Here's how we might test our user model:
This example is just about as decoupled as the model it's testing, and it's pretty resilient to any change that might happen to the API apart from data structure or relationship changes.
Decoupling the service layer is one of many things we're doing to ensure a graceful transition to a single page application. Next time we'll cover additional philosophies in building strangler app which includes testing, picking the right tools, building React apps outside of Flux, and more. Stay tuned!
If you're interested in checking out our documentation, have a look around here.