This is the first part in a multi-part series on building service- oriented applications. Because a lot of the work I’ve been doing lately has revolved around migrating a Rails application from a monolithic application to a more service-oriented model, and because Rails is very popular these days, this will focus on Rails. The goal is to not be Rails-specific but to provide meaningful examples that just happen do be written in Ruby and use Rails.
For those unfamiliar with the term “monolith” as a software development paradigm, it refers to a piece of software that has a single codebase and the end result is a single application. To give an idea of monoliths you may be familiar with, consider a prototypical Rails web application or Java desktop application. Monoliths are all around – the Linux kernel is a monolithic kernel; it runs a single process to perform its functionality, all drivers are inserted as modules. Compare this to something like QNX where drivers are their own processes that communicate with the core kernel to provide additional functionality.
Why do we care?
Monolithic applications are a natural expression of a product as code. For a lot of products, this model is completely acceptable; there is no reason to do anything differently. Most products will not reach the level of developer input or site traffic to the point where the fact that you have a monolithic code base becomes a hinderance. Contrary to this, monolithic applications have a number of benefits:
- They are easier to reason about
- All code is located in the same place
- Performance is likely to be better initially
- Implementing business logic is not tied up in technical decisions
Reasoning about a monolithic application
For most software projects, the total level of complexity is low enough that a monolithic application is easier to understand for new and existing developers alike. For any piece of software that you can fit the model in your head, a single codebase helps to keep your code organized and is easier to work on. All of the code that is executed is contained in the same place meaning that the path of a single user request can be followed line by line through to completion without having to look at any other code-base (except perhaps some external libraries).
For a small team of developers, it is a reasonable way to build a piece of software. To do otherwise would increase the complexity to such a degree that it would be challenging, to say the least, to deliver a complete piece of software. Increasing the number of components that you have to manage has an adverse effect on ability to gain traction.
“Premature optimization is the root of all evil.” –Donald Knuth
Choosing your application architecture, at first, should not be something you need to think too much about. Getting a product to market that performs sub-optimally, and can be improved, is better than not delivering. This emphasis on productivity has led to the rise in popularity of frameworks like Rails, Django, Laravel, and a host of others; they permit developers to focus on building something quickly rather than fuss around with configuration. They choose “convention over configuration”. That convention is to build a single, easily configured, monolithic application that contains all of your data models, business logic and display components in one place.
On the other hand, choosing a service-oriented architecture up-front for a small team would likely be disastrous. The complexity of deploying, managing, and even reasoning about your application would be too high for a small team to keep track of all the components. It would also be unlikely that your team would be able to identify where the boundaries between components lies and how to best model them, store them, and interact with them. As a result you would likely make a number of wrong choices in the implementation at this point so now you have two problems: a complex architecture, and the wrong implementation for a number of different components.
Scalability (Or, it’s not just about transactions per second)
Scalability is defined as:
the capability of a system, network, or process to handle a growing amount of work, or its potential to be enlarged in order to accommodate that growth.
This definition does not mention what that growth is measured in. More often than not, people will say that scaling a piece of software implies that it will support more requests per second. This is largely an artifact of two things: living in a world where a lot of systems operate on network traffic (web applications being one of the primary examples), and the fact that engineers work on these systems and think of things in engineering ways.
Other units of measurement
The fact is that there are other units of measurement for the scalability of your software. These units of measurement are equally as important as its ability to handle more traffic. In fact, looking at the long-term landscape, they might be considered to be more important. One of those dimensions is the system’s ability to support more developers, and another is its ability to add new features. As your traffic (and typically the size of your user base) grows, each of these dimensions will be pushed outwards. If your software is architected in such a way that it takes a week to add a button, or if adding new developers requires weeks of on-boarding followed by a very carefully orchestrated dance to ensure that they don’t step on each other, then your software is not scalable along those dimensions.
On being “good enough”
No software is perfect, no architecture flawless. Instead, they are all built on a series of compromises that are made to get to the right place at the right time. In some ways, software development is a much different discipline than building physical objects. Your software is much more malleable than, say, a 40 story sky-scraper. It is possible to replace the underlying platform of a software application, whereas it is likely to be impossible to rebuild the first three floors of a 100-story building because they were done incorrectly.
Building what you need now
There is a software development philosophy, YAGNI (You Aren’t Gonna Need It) that emphasizes building functionality and architecture when you actually need it, not when you think you are going to need it. The argument is that you will spend un-needed time building for a future state that may, or may not, exist. There are, however, times where you have an opportunity to plan for the future without building for the future. In these cases it is reasonable to make forward-thinking technical decisions that do not waste time by build features prematurely though are still paving the way for the future.
Somewhere in the Middle
It’s entirely possible to build an application in a way that is easy to reason about without closing the door to a more distributed architecture in the future. This requires a little planning up-front and a concerted effort to enforce separation of concerns in such as way to prevent leaking data between these concerns. In the next installation we will look at how to build fences around your application’s core components to enable isolation of concerns in your code that should help you to transition to services if (or when) it makes sense to do so.