Ruby on Rails. Django. Spring. Symfony.
You probably know some of these frameworks and, like me, you’ve probably worked with one or more of them.
Web frameworks have established themselves as an important part of web development where they attempt to provide a one-solution-fits-all that has you covered from A to Z, promising automated scaffolding, easy management of entities and their corresponding database tables, out-of-the-box security and comprehensive documentation to get you started.
The rise of cloud computing
Cloud computing has been around for a while now, but it really took off when Amazon introduced the world to their cloud computing platform AWS in 2006. As we know, it was a huge success (source), and Google followed suit with their Google Cloud Platform in 2008, and Microsoft with Azure in 2010. Today, we’re developing software for the cloud and building platforms that scale efficiently as demand grows.
Consequently, we have taken web development to a new level. We offer publicly available API’s from a group of I/O-optimised instances while outsourcing the more compute-heavy workload to compute-optimised instances that work in tandem with cloud functions, micro services and cloud-hosted cron jobs for scheduled tasks. And everything is most likely tied together using a high-performance pub/sub mechanism.
The classic web framework does not belong in this world. In fact, it will probably hurt your project to use one.
The monolithic framework
These frameworks are monoliths with a very high level of cohesiveness — and as I will argue in the following, cohesiveness is anathema to the modern cloud developer.
Perhaps you’ve read Uncle Bob’s excellent book “Clean Architecture”. If not, I highly recommend it. Here’s a key point where he’s talking about a concept called “screaming architecture”:
Business logic is essential. The fact that it’s delivered over the web, is a detail. When evaluating a framework to use, be careful not to pick one that invades your entire code base. It should be a detail.
How to interpret this? Consider that you are developing a piece of software for a client, and that the software must satisfy their requirements. The implemented solution is called the business logic. It’s a set of requirements that make up the core of the software. They are characterised by being fairly high-level: they say nothing about how things should be done, and everything about what should be done. It’s why we’re building software.
The frontend — regardless of how important it is perceived to be — is a detail as well. Frontends can be replaced, but the business logic is essential to your client, and it’s the reason that the software exists in the first place. It needs to be protected from elements that threaten to destabilise or otherwise threaten the existence of your software: driver issues, bugs in external dependencies, changes in requirements to the technology stack, hosting provider etc. Your overall aspiration is to keep your software viable at all costs.
When you decide to use a web framework, you effectively tie your business logic directly to the underlying database (or, in the case of most frameworks, the DBAL), the request/response cycle, a specific logging library, a specific templating engine and, most prevalently, the framework itself. The framework permeates your entire backend, sets constraints on how your code interacts with it, and pretty much also dictates how you layout your application.
The drawbacks of frameworks
Web frameworks will help you get your project off the ground faster but at a huge cost. The framework constitutes a dominating dependency that becomes a liability in the long run by locking your code base into a rigid structure that you cannot undo later. It ends up dragging down your project by the simple means of limiting your options as the code base grows.
- What if a serious bug in the framework prevents you from shipping your software?
- What if the authors abandon their framework and your code becomes legacy overnight?
- What if the framework evolves in an unexpected — or disagreeable — direction?
- What if serious performance issues are introduced in the framework due to efforts into making it more approachable to newcomers?
- What if a new and super-performant version of the framework is released with no upgrade path from the version you are using?
This, too, can happen to any number of libraries that you use. But libraries are replaceable — frameworks are not. These are not just theoretical considerations. I’ve witnessed them first-hand, and perhaps so have you? They are scary because they endanger the survival of your project.
In one project, I was fairly new to Node.js and decided to try a promising framework, mean.io. However, as the project progressed, it became clear that I had made a poor choice. At the time, the documentation was very basic and didn’t provide answers to the most pressing issues, and the framework seemed to make everything more difficult. In retrospect, my productivity dwindled as I spent precious time researching and fighting the framework. Adding to the damage, it also became clear that the framework was lacking the flexibility that I needed. After careful consideration, I decided that the framework would become a bottleneck for further development and I had a colleague help me cut it out. Removing it was quite a lot of work almost identical in scope to rewriting the application from scratch.
You could argue that I shouldn’t have picked a framework that I wasn’t already familiar with, and you’ll probably have a point. But developers make these decisions all the time. We go looking for libraries and frameworks that solve our immediate needs, and it happens that the first choice doesn’t fulfil the requirements so we replace it with something else. A good architecture accommodates this and allows you to replace things without too much hassle. And this is where a tightly integrated framework can really get in your way.
In another project, I developed a large application for managing mailing lists. The project was written in PHP, using the popular Symfony framework. Although Symfony allowed me to be productive right from the outset, the presence of a framework later caused me many headaches. The need arose to separate out a large part of the mail handling logic so it could run on separate servers that could be scaled up as the demand (and load) on the system increased. The tight coupling between my entities, business logic and the framework itself made this nearly impossible. I ended up with an acceptable solution where some of this logic was duplicated outside the main application, but it was not an ideal solution.
In a third project based on Node.js, there was no framework as such, but a very large dependency on an ORM layer called TypeORM. TypeORM was praised in our team for its stability and flexibility and it allowed us a great deal of much-needed productivity up front. However, several problems presented themselves down the line. For one, we wanted to cut out part of the functionality for generating invoices, sending emails etc. and convert that code into cloud functions in order to decrease the load on the web server. Since this code was already tightly integrated with TypeORM, it was impossible to do this without having a full copy of TypeORM in the cloud functions which were intended to be lightweight in nature. The project was pushed until a later point due to the time required and has yet to be done.
Any project runs into difficulties at some point. The problems I just described have one thing in common: an overshadowing dependency that limited our options and made change difficult. Which brings me to the next point.
The Stable Dependencies Principle
Depend in the direction of stability. Or, in a nutshell, don’t let unstable/flexible components be depended upon by other components.
By protecting the business logic, all of these problems could have been avoided. How so? The answer is basically to avoid frameworks that attempt to tie everything together and to adopt Uncle Bob’s approach to clean architecture. Clean architecture is an entire topic in itself, and I cannot cover it here, but the basic premise is to avoid dependencies around your business logic and instead let details such as routers, logging, cron jobs etc. depending on your business logic through dependency inversion.
So feel free to use libraries such as ORM’s, loggers, routers and caching mechanisms, but don’t let them permeate your business logic. These dependencies need to be pushed to the edges of your application where they cannot hurt you or your application when they fail or become discontinued. Stick to the standard library that’s included in your programming language, along with a few well-chosen and trustworthy libraries that you know well and can rely on.
Think twice before you decide to use Rails for your Ruby project, Spring for your Java project, and Symfony for your PHP project. I don’t say this lightly. I’ve built and shipped numerous projects based on Symfony, and while a well-supported framework can certainly be a perfect match for some projects, they are certainly very far from being a one-size-fits-all.
And to the beginner who wants to learn a new language by picking up the most popular framework: don’t. It locks you into a certain way of thinking, limits your options, invites to trouble down the road and most likely also prevents you from ever learning how your golden new language can shine on its own.