At AmberBit we not only work on small MVPs or prototypes for emerging start ups. We have a solid base of long-term customers, and we have to support their application not for months, but for years. All applications tend to grow in time, and issues such as technical debt and bad architectural decisions can arise, and bide you after many months the mistake was made. There has been a lot of talk recently about making the applications more modular, to tackle this as well as other problems, such as performance. But how would one do that? I’ll explain you my approach.
Reasons to split the app
As shortly mentioned above, big applications tend to be harder to maintain that the small ones. This is probably related to the amount of information human brain can process at given time. This is why we create classes, modules, small functions calling other small functions and using other, separated modules. The same principle can, and should be applied to whole applications architecture.
Having set of small applications allows us to focus on working on isolated parts of functionality. We might have more chance of not breaking other parts of the application this way, as well as having a clearer vision of the piece of software that has to do given job.
Suite of connected applications is also easier to deploy. You no longer have to deploy whole big application, which might take tens of minutes in worst cases, but you can push a hotfix to only affected module, leaving rest untouched.
Performance implications are important, but generally not deciding factor. However, you can achieve significant improvements if you make things work more in parallel. You can make also the infrastructure cheaper, for example if you use services as Heroku. The unexpected yet pleasant result might be that your app will require less paid dynos, being split among more free-tier dynos instead.
Reliability and fault tolerance is another good reason. If part of your application is affected by a bug, or an infrastructure failure, you have better chances of keeping the whole thing usable if it’s split among smaller applications. Syntax error made by your junior developer in ‘About us’ static page controller will no longer result in the whole checkout process being broken, because the app failed to start properly.
Having a suite of co-existing apps instead of single big one, allows you also choose the tools for the job more wisely. We know Rails is great, but how about some other Ruby frameworks? You can keep some of the services really damn slim, make others use Sinatra or experiment with Clojure, yet to the end-user the app will be visible as single entity.
Another important factor is ease of migration or upgrade. Instead of another huge Rails 2 to Rails 4 upgrade, you can update individual modules gradually. Smaller deployments are less likely to blow the whole damn thing up, remember! Heck, you can even try swapping around modules to their newer/rewritten versions, and if something fails, silently replace them with proved, working versions. You might call it microservices if you want.
Reasons not to split the app
There are, however, good reasons not to split the app. If you are working on a prototype or a MVP, and you have very limited development budget, you might want to keep it simple for time being. Developing multiple applications means having certain amount extra work added.
If your team is inexperienced or lacks communication skills, stick to the old ways for now and look for help instead.
Your app might be just the right size. Don’t make things more complicated if you don’t have to. If you follow KISS principles and have a nice lean app, do not add extra layers of indirection by splitting it up.
How to divide the app?
Good question, Watson. And it is a big decision to make.
The obvious way to split the app is to divide it to front-end and
back-end. While /api
requests go to one web applications, the rest of
requests end up in front-end. This is simple yet effective and makes a
lot of sense if you are already using front-end development frameworks
such as Angular or Ember.
To avoid same-origin policy conflicts when using JavaScript AJAX calls, you can proxy the requests. On production, we found simple Nginx reverse proxy rules to be very effective. During development, we use rack-reverse-proxy with some patches of our own, as we found it not maintained anymore (however, with our tweaks it works again).
Here is sample configuration for Nginx:
server {
listen 80;
server_name www.example.com;
root /home/ubuntu/dummy/current/public;
rewrite_log on;
location / {
proxy_pass http://some-frontend-app.example.com;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
}
location /api {
proxy_pass http://some-backend-app.example.com;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
}
}
What we are using in production is a EC2 instance that serves us as a proxy, and Heroku instances sitting in the same AWS region, to make the proxying fast and local to the internal Amazon network.
In development, we are launching the individual applications on different ports with a startup script, and have a simple rack reverse proxy module to avoid configuring Nginx locally:
require 'rack/reverse_proxy'
use Rack::ReverseProxy do
reverse_proxy_options :preserve_host => false, :matching => :first
reverse_proxy(/^\/api(.*)$/, 'http://localhost:3001/api')
reverse_proxy(/^\/(.*)$/, 'http://localhost:3000/$1')
end
app = proc do |env|
# not sure what this does, can prob be removed ;)
[ 200, {'Content-Type' => 'text/plain'}, "b" ]
end
run app
In reality, we figured we need to split the app even more. We have separate front-ends for public pages, user settings/dashboard and main functionality of application. Our back-end is also split up among several applications, including some Node.js and some Rails.
Splitting up the APIs
There are probably low hanging fruits you can easily extract from the APIs. The rule we are following is that if you split the APIs, you need to have them using separate databases, or, interact with each other. I do not believe that having two services reading and writing to the same database is good approach. However, I would add an exception here: when one of services is only writing to the database, and another one is reading, it might be good idea to separate them and use separate connections to the same database. This gives you opportunity to add extra sugar on top of reading service, like caching.
Things you might want to avoid also includes: cookies and cookie-based sessions. It is much more convenient to use an authentication token instead, and hook up to some central authentication service wherever it is needed. A nice approach could be also using a CAS solution, such as Casino.
Another thing to remember is that your API should still be consistent and follow the same principles. Even if these are separate applications, to end user (in such case front-end developer) they should look and behave in familiar, predictable manner. Having a proxy server is one step that makes it easier, but having it designed the proper way is totally up to you.
Splitting up the front-ends
At some point we have decided to split the front-end into individual applications. We have built the majority of the application in client-side Angular MVC framework. However, as it turned out, some pages had to be accessible using plain old HTTP get. This is both for SEO purposes, social networking sharing and performance, and the previous solution based on pretender.io turned out to be too heavy.
Our before-login front-end pages had been extracted to plain old Rails application, which contacts API endpoints via HTTP from Ruby, and renders HTML server-side. Majority of our application ended up in two additional Rails applications, who serve Angular client-side application. Common behavior for JavaScript should be extracted to modules that are included in both applications using bower or other asset sharing techniques.
Lessons learned the hard way
We have tried maintaining one large repository that would include all the applications. However, both git submodules and git subtree merge techniques turned out to be far from perfect. For now, we are sticking to a meta-repository with a shell script to update all the subdirectories with relevant repositories. We might want to try Google’s repo but it is not a huge priority at the moment.
Deployments are fast and fun, yet you need to think about dependencies. If you deploy the apps in wrong order, some of the clients will end up being served by wrong API or front-end, which will cause confusion or a failure error.
Code duplication is another issue you might or might not want to fight till you bleed. Personally, I do not mind minor code duplications if the effort to make it DRY is too high. However, if you are duplicating too much code between modules, it is a clear sign you have divided it wrong and might want more or less applications to compose the suite.
Full-stack testing is not so great anymore. Well, it was not great before, as you might have experienced testing heavyweight Rails applications full of JavaScript. We are still to figure a good way out, but for now, having separate tests repository and a fairly complicated script to bring everything up will have to do. We have also shifted focus on testing individual modules more rather than full fat scenarios.
Summary
Split it up if it’s too big. I do not care if you call it microservices, SOA or whatever, the principle is always the same: if you have a problem too big to tackle at a time, split it up into smaller ones. Divide and conquer!
Post by Hubert Łępicki
Hubert is partner at AmberBit. Rails, Elixir and functional programming are his areas of expertise.