Developing micro-architectures in web apps and services
July 14th, 2009Note: the following article is in the context of the Ruby community and makes many Ruby-specific references, but the principles discussed are generally language-agnostic.
Intro
By now the benefits provided by macro-architecture in the context of web applications and web services via frameworks like Rails are well-understood and do not need to be re-explained. Changes in Rails and the introduction of ActiveResource have also contributed to the current acceptance of REST’ful principles as being beneficial in web apps.
However, one area that has been gaining more support in the community but has not been directly examined is that of micro-architectures. For the sake of this article I am defining a micro-architecture as a REST’ful web applications or service that is composed of several self-contained services communicated with over HTTP. Herein the general benefits of micro-architectures, as well as when and how to effectively move to them, well be discussed.
Test-driving the public interface
As with most software, begin TDD-style with tests first. I recommend outside-in style development, with heavy emphasis on the “outside” and little on the “in”. In other words, favor integration tests over unit tests. For example, in typical software development your goal might be the algorithmic transformation of some data set. In this case you would start out with some example base cases of how your library works, and implement them in the red-green-refactor cycle. If the implementation starts becoming too complicated then introduce some unit tests to help you manage complexity. However, remember that the purpose of the api is just to get the integration tests to pass, and any extra unit tests just make refactoring more difficult. Thus, unit tests (and even more extremely, mocking) are only a result of necessity due to complexity or test-suite run time savings.
The analogy in the web app/service world to our algorithmic transform library then would be to use integration tests at the highest level possible, namely through the http api. For a web application (something consumed by a human in a browser) use webrat so that you can exercise the same navigation and data-submission structure that users do in the browser in html. For testing web services (consumed programmatically as apis, mostly simple xml or json serialization formats), I recommend testing with rack-test.
One common and valid argument against micro-architectures is that they are a bit complicated to setup automated tests for, although I think this is beginning to change. Either way both webrat and rack-test can be used to test rack-based applications in-memory. It is advised to start this way to avoid setup pains.
Once you have your tests passing you can easily convert them to shoot real http requests at servers rather than in-memory rack-object interface testing. You have many options here for webrat, including mechanize if you want to go headless and selenium if you want to spawn up a real browser. I recommend going headless, at least during development so you don’t slow down your feedback loop. If you are using rack-test then something to look out for is rack-client. Although the library isn’t mature yet, it offers a single line of code change to go from in-memory rack-test to http-based rack-test. Since webrat has a rack-test adapter, it could also be used with rack-client instead of mechanize. First get your in-memory tests passing, then write up some rackup .ru files to manually spawn servers with, and then convert the tests to http with the methods described above. After that is done, you can automate the spawning servers with your testing framework’s equivalent of RSpec before(:all) and after(:all).
Development guidelines
I will assume that we are building some single web app or service to be hosted at some top-level domain (e.g. facebook, google, etc). As such, the customer will be facing an api that from the outside looks like a macro-architecture, where many things are possible in a somewhat complicated service. The opposite would be many Amazon AWS-style mini services like S3, SimpleDB, EC2, etc, but these kind of public micro-services are not too common.
So, once again we start building our public macro-service. Because such services have growth and complexity in mind, it’s a good idea to use a larger framework like Rails or Merb with enough conventions and organization to handle that complexity. It is easier to develop an entire macro-service with a macro-architecture, putting all of your controllers, models, views, etc in the same codebase to makeup your resources. In contrast separating code out into isolated internal micro-services takes some work, in the same way that organizing a pile of legos into piles of common color is harder than throwing everything into a single pile.
Additionally, while setting up http-level integration testing by forking a process in before(:all) and killing it in after(:all) is manageable, doing the same with several internal services consumed by one main one is a bit more complicated to setup. Nonetheless, it is still advisable to manually do this until some nice library pops up that can handle expressing these connections and spawning/tearing down servers from them elegantly.
You should design your macro-architecture in a way that makes separating out micro-services easy. In other words, think about how whatever part of your app that you are building could be backed by some service that stands on its own (e.g. a blog post comments controller could be backed by a generic annotation service that accepts a uri to some resource, as well as annotation text and author information). Get your tests passing this way against your integration-level macro-service api. Note that you don’t have to test the micro-service api, that is the fun part of refactoring (but if necessary due to complexity, be sure to add tests… use the same discretion between integration, unit, and mock tests as used in the simple algorithmic transform example above).
The reason why you should think about the internal-service that could comprise your macro-service is to avoid sharing code between this part and the rest of the application. If you haven’t noticed already this article is really about taking classic established software principles and applying to the world of web apps. In this case we want high cohesion and loose coupling between code in various parts of our public macro-service api. This is a good practice in general, and as long as you maintain it in a slightly stricter fashion than normal then it will make extracting a micro-service possible. Also when I avoid code sharing, that doesn’t prevent you from extracting common utility code into shared gems, just stay away from sharing business logic code that will eventually make refactoring a service out unbearable.
Being pragmatic
Note that I am not saying that you should always follow this practice of avoiding code reuse and thinking about how to make public-api resources independent for each part of your application. Software development is a constant rearrangement of where complexity resides, whether it be in a specific, isolated, and visible portion of code, or whether it lies in the complex relationships between too many abstractions. Therefore, as always use discretion as to what makes sense to keep in the internal macro-architecture of the public macro-service, and what should be split off into internal micro-services. What I am saying is that you should be aware of when you reuse code across resources and consciously make the decision to do so. Just try to think about whether or not a resource could easily explode in complexity (making the encapsulation of the complex service worth it), or whether it could easily become a performance bottleneck (making isolated performance monitoring worth it). Those two reasons, separating out complexity via encapsulation, and performance monitoring, are the big motivators for creating a micro-architecture.
In any case, if you’ve identified a resource that is a good candidate for becoming a micro-service, and has already been tested implicitly via the public macro-service api, then there is a convenient middle ground to take. Rack and its notion of middleware, in addition to bigger frameworks supporting it with things like Rails Metal, make this middle ground possible. I generally like using a class that inherits from Sinatra::Base as a micro-service, as the api does not contrain your resource design like bigger frameworks with more conventions do, and keeping the amount of files and directories to a minimum is possible. Sinatra provides just enough sugar to make building a Rack-middleware a pleasant experience (although the Rails 3.0 internals are shaping up to be flexible enough to be used in a very similar way, so watch for that as well). By creating your resource as a middleware service you more explicitly mark it as code to be isolated from the rest of the internal macro-architecture, but because of Rack you get the benefit of being able to test the entire public macro-service in one process. Doing this during the development phase of your application prevents you from being barred down by setting up the integration-testing infrastructure for running several rack-apps pulled out of the middleware stack but still consumed by the main application. In fact, if the separation is just for organization encapsulation purposes then feel free to deploy your monolithic app with the micro-services as middleware. However, if you have micro-services that were separated due to performance concerns that need to be monitored then be sure to pull them out and integration test them before your public deployment.
Common criticisms
The most common criticism that I have not addressed when it comes to using all these micro services is complexity introduced by latency issues when dealing with many http apis. To deal with this transparently and most elegantly, I recommend Ruby 1.9 + Neverblock. Other options include manual event handling with em-http-request, or explicit thread-based latency hiding with something like “need_later” futures in Dataflow.
Another common criticism is the complexity increased in an application because of all the new moving parts, which is indeed a problem and should be weighed against the two major motivations. Notably though, with the clear boundaries introduced via the separate applications, you can catch exceptions if one micro-service is not responding and handle error messages on the specific page that uses it. Compare this to a monolithic architecture, where partial system availability is much more difficult to design.
Final remarks
Please note that for simplicity I have been implying a ratio of one public api resource to one internal micro-service, but in reality a single micro service could be used in part by many public api resources, and a single public api resource could make use of several internal micro-services.
This concludes my summary of how to effectively manage and take advantage of an internal micro-service architecture. Micro-services are still in their young and “hip” stages compared to their macro counterparts, so expect best practices regarding them to evolve as we collectively gain experience. I think the general advice in this post is sane though, so hopefully it can serve as a reference to people not yet familiar with the topic. As always, comments/criticism welcome.
Benefits
For brevity I tried to not cover specific benefits of micro-architecture within the two major categories of encapsulation and performance monitoring isolation. Here is a list that I will update if anyone has more suggestions in the comments.
- library/gem dependency isolation
- choosing appropriately different servers for services (e.g. passenger vs thin)
- partial-site functioning when isolated services are down and error handling is in place
- full http reverse-proxy caching via ESI pulling in bits from disparate services

December 16th, 2009 at 5:11 am
Excellent writeup, Larry!
I’m venturing into micro-services as well. My first experience was with OSGi (Java), but that was microarchitecture in a single VM (microkernel), not through HTTP.
As of now I’m “increasingly reducing” usage of ActionView and helpers, and using more of AJAX frontend/SOFEA-style with Dojo. In some ways, with this setup, I begin to realize that Ruby on Rails conventions (esp with ActionView and ActionController) actually restricts what I’m trying to do instead of helping. I’m thinking it might be easier (and more performant) to do with closer-to-HTTP toolset instead of a high level MVC framework like Rails.
I’m still smoothing out with Dojo+Rails, and really looking forward to microarchitecture. However, support for this kind of development/architecture style seems to be still limited at this point. So I’m not rushing…
December 16th, 2009 at 5:13 am
For one, I still do not know (yet) how to call another Rails action from a Rails action handler (i.e using a microservice). Do you know how to do this? I can use Net::HTTP but this seems hackish.