The Granularity Of A Microservice
The most frequent question I am asked about microservices is how small should they be. This is the wrong question. It’s not the lines of code that matter, but whether the service does one thing and does it well. A service that does one thing well can be developed quickly and is unlikely to change. If and when its purpose or technology becomes stale it can be easily removed or replaced. This is one of the key benefits of a micro-service architecture.
However just as low test coverage and broken tests may warn you of an unloved codebase, a high number of lines of code can indicate that a service has more than one job. So despite being the wrong question, it’s still useful to have an idea of the answer. Fred George, the father of micro-services suggests between 50 and 100 lines of code. I met Fred in 2012, and after going through his incredible two week bootcamp (which involved writing a relatively sophisticated application in just 22 lines of ruby), I was keen to try out micro-services in a real-world setting. Enter Campaign Manager, an application for deciding which ads to show on a 500 page/second web site.
Campaign Manager was a disjointed application mostly written in PL/SQL, with additional logic spread across multiple JSP pages and configuration beans. Its purpose was to control which ad slots were enabled by channel, sub-channel and page type. The replacement system had to do all this, but in addition consider region, device and orientation. It also had to incorporate a scheduling component and automatically activate / deactivate prominent slots when the commercial team’s ad platform (DFP) had appropriate inventory. Finally some slots had to be mutually exclusive in some regions, so for example a Billboard and an MPU Top could not both be active on the same page in the UK.
My approach was to develop a set of micro-services, whose design would be guided by the tenets of Unix Philosophy, with special attention given to doing one thing well. All but one service was written in JavaScript and deployed to nodejs, the remaining service was written in Groovy because it integrated with DFP via a java library. The services and their respective line counts were as follows:-
Service | Purpose | Lines |
---|---|---|
cm-slot-config | Served the ad slot configuration for each page request. | 96 |
cm-slot-admin | Provided the slot management API. | 154 |
cm-web-ui | User interface for managing slot configuration and admin operations (mostly client-side js). | 1168 |
cm-dfp-extract | Synchronised Campaign Manager’s slot configuration with the line items in DFP. | 419 |
cm-inventory-check | Monitored the real-time analytics feed and temporarily deactivated prominent slots if DFP was throttling adverts. | 134 |
cm-status | Provided a REST endpoint for displaying the status of each service. | 121 |
cm-heartbeat | Published a heartbeat message onto the ESB so we could quickly diagnose connectivity problems. | 21 |
cm-compact-indexes | Regularly compacted some Redis indexes for keys deleted using ttl expiry. | 35 |
cm-redis-logger | Persisted Campaign manager events published to the ESB and provided a RESTful API for retrieving them. | 78 |
cm-riemann-logger | Forwarded Campaign manager events published to the ESB on to Riemann. | 71 |
cm-smoke-test | Continuously created and verified a fake set of configuration (only visible from Bouvet Island). | 83 |
Of the services in the list there are two which breach Fred’s guideline of 50-100 line by a considerable amount, cm-web-ui (1118 lines) and cm-dfp-extract (419 lines). cm-web-ui broke the scales because we deliberately consolidated all of the web-ui functionality into a single service. We did this because sharing state and managing static assets hosted by different services is problematic. In the case of cm-dfp-extract, the DFP Java API is ugly, and the code interacting with it incurred some splash damage.
Just as interesting are the services which fall below Fred’s lower limit of 50 lines (cm-heartbeat and cm-compact-indexes). Do these services justify their own existence? Retrospectively I think not. They fail a second micro-service architecture guideline, they have no conclusions that are worth publishing. cm-compact-indexes should have been moved into cm-slot-admin and cm-heartbeat was superseded by cm-smoke-test and so should have been deleted.
Managing a small battery of micro-services is not without its difficulties, so it’s of no surprise that the second most frequent question I get asked is how to do this. In the spirit of doing one thing well, I’ll leave my answer for another blog post. Instead I’d like to round things off with a game of Devil’s advocate and challenge whether Fred’s 50-100 lines is too small. How would things have panned out if I’d have followed a more conventional architecture?.
Firstly I would still have split the Campaign Manager into distinct frontend and backend components. The frontend component needed to be horizontally scalable and it would have been a security risk to host the backend component on a publicly accessible server.
Service | Purpose | Lines |
---|---|---|
cm-slot-frontend | Served the ad slot configuration for each page request. | 96 |
cm-slot-backend | Everything else | 2234 |
With only two components, build and deployment would undoubtedly have been easier. Configuration would have been simpler too, since the application would interact via function calls instead of an ESB or remote API. However it would have been harder for a team of developers to work independently and releases would have been more risky – with fine grained services we never had to worry about accidentally releasing unfinished code or adopting strategies such as branching. With no fear of releasing to production, we delivered features as soon as they were ready, often multiple times per day.
I also think the resulting codebase would have been worse. A total of seven developers worked part-time on Campaign Manager over a period of eight months, but we never had more than four developers at any one time. The first release went live after three months and I rotated to a different project after four. I returned three months later to find that several new features had been implemented. Not everybody designs or writes software in the same way, and no one way is objectively best, however some of the changes were overly complicated and one feature in particular, a resource hungry analytics tool, was completely unnecessary. Because of the micro-service architecture we were able to systematically refactor service by service and I was able to remove the entire analytics service with one command.
In contrast single codebase applications have no physical boundaries. It’s far easier for poor code to spread ivy-like across it, and just like ivy, poor code can be surprisingly hard to remove. In an ideal world, organisations would only hire the best employees, who never made mistakes, who never needed to learn new tools on the job. In the real world micro-services can at least provide a degree of damage limitation and a controlled path back to sanity when things do go astray.