API microservices, the Unix philosophy, and the Richardson Maturity Model
a practical perspective
After some time in the trenches I have come to believe that the engineering community needs to eliminate some FUD (Fear, Uncertainty and Doubt) and NIH (Not Invented Here) thinking regarding microservices and API design.
I have not conducted any statistically valid surveys so all of the following is personal opinion formed while on the job.
Building APIs with a microservices approach allows teams to avoid several common pitfalls:
- Becoming encumbered by a monolithic centralized API that is at risk of reaching its maximum performance and hosting limits. This is when “vertical scaling” (e.g. throwing more RAM at it) can only go so far. Unless the monolith is a beautiful bonsai garden of multithreading or a romantic symphony of processes and queues, the crunch will come and it will not be pretty. Furthermore, the bigger and hairier a codebase becomes, the more expensive it is to develop due to reinvention of mundane things and a hockey stick-shaped learning curve for new teammates.
- Inflexible rules and restrictions on developer freedom and creativity. In order to rein-in an increasingly daunting beast of a monolith, standardization of practices is an excellent, if temporary bandaid fix. I’m not arguing against standardization. As a Python fan I love its culture of pragmatism and plainness. The pitfall to beware of is at the architectural level. The reason for this is that the right level of abstraction for “web service API” can be a moving target. When a team inherits a monolith in a system that does not already enforce common patterns, and is under pressure to scale the API to lower hosting costs or prevent additional outages, desperation can easily cloud judgement. When under pressure it can be tempting to go on “lock down” mode where past bad decisions become more entrenched to defend the pride of those who made them, at the expense of the product. Lock down mode is the definition of crazy, of valuing dumb and slow hard work over fast smart work.
- Lack of access to new engineers in the exact, arbitrary stack of choice. I recently witnessed a disaster waiting to happen in one team (whose company will remain nameless to protect the innocent). This team suffers from both pain points illustrated above. They brought me in to advise on architecture and after observation I prescribed microservices. There was intense interest and excitement from the engineers and founders who had never heard of this pattern before. The company has less than 18 months of runway before doing another funding round or dying. However, instead of entertaining a development model which would allow decomposing the monolith into separate services, they inexplicably decided to curl up and attempt to crawl back into the womb of Theoretically Appealing Concepts. The same concepts that resulted in a beast too burdensome to scale or improve, except by the two engineers who had been masochistically refactoring it for months. Examples are all in the form of “X all the things”, where X may be “actors”, “non-blocking async”, “VM tuning”, “stronger typing” etc. Those things may all be good in time, but they’re also all going deeper into the abyss of risk right now.
- Inability to add new features in parallel to the permanent refactoring efforts that tie up the only senior engineers on the team. To me this sounds like a death knell. Refusal to increase traction with customers because of pride or delusion, especially when it’s possible to do so while simultaneously reducing technical debt, is inconceivable. If only the non-technical cofounders or investors knew what was really going on.
The microservices design approach is not a Theoretically Appealing Concept like “better multithreading will make an I/O-heavy web service scalable”. Decomposing monolithic services into potentially many smaller ones is an architectural style that seems to work well in the real world. Kind of like REST. As with all software, abstractions are important, but they need to be the right ones for the problems being solved.
From Eric S. Raymond’s The Art of Unix Programming:
“Developers should avoid writing big programs. This rule aims to prevent overinvestment of development time in failed or suboptimal approaches caused by the owners of the program’s reluctance to throw away visibly large pieces of work. Smaller programs are not only easier to optimize and maintain; they are easier to delete when deprecated.”
Ramses Executable API Specs
a different way to avoid complexity with microservices while optimizing for developer experience
There are now several great spec projects to help developers think about API development in a way that does not require reinventing the conceptual wheel. These include RAML, Swagger, API Blueprint etc. Each of them provides a DSL (domain specific language) for building APIs. RAML claims to be simple, Swagger popular and API Blueprint powerful.
Consider the following rhetorical questions:
- If using a DSL/spec allows one to eliminate boilerplate code without reducing flexibility, is that a good thing?
- If it’s possible to get documentation, tests, and client libraries “for free”, would that be good?
- What about if it’s possible to hook into the high-level DSL to add custom things when needed?
I have met engineers and founders who hum and haw at the validity of, or answers to such questions. It doesn’t matter if the DSL is provided by one of the slick new spec formats or from high-flying macros, or from brutish mappings of interfaces and objects, or from rigorous type definitions. Each language and stack has its own capabilities, and anything that’s Turing complete (all programming languages including CSS) can technically provide the same level of control under the hood. Given that, why would any engineer feel more productive wallowing in verbosity than by understanding their whole program at a glance with the right level of abstraction?
The only excuses I can think of are irrational fears of the robot apocalypse, or sociopathic and territorial “defense” of job security. There is no need to fear death or the gods! Your social media expert MBA boss isn’t about to sully themselves by actually creating something, even if you made it incredibly simple to read and understand. Plus, you want the robots to be on your side, so better to start commanding them now.
For teams in the midst of monolith hell, it may be worthwhile to consider the Richardson Maturity Model (RMM for short) as it relates to microservices. This model has been a guiding force for the development of Ramses. The actual RMM says that there are three levels of maturity when it comes to leveraging the natural capabilities of HTTP to build APIs.
- Level 0 is remote procedure call spaghetti.
- Level 1 adds the concept of resources (e.g. collections and their items).
- Level 2 adds consideration for the meaning of HTTP verbs (i.e. making GET get things, POST post things, PUT put things etc.).
Then there’s Level 3. It adds hypermedia links to the mix so that clients can dynamically browse resources à la HTML in a browser. This is where Ramses departs from all the awesome work that’s being done in the world of hypermedia. Two reasons that a static client generation approach seems to be fine at the moment:
- Webhooks, federated authentication, and specs already exist that allow APIs to both drive and be driven by external systems in production.
- Generic and dynamic client libraries that know how to browse hypermedia automatically are still very new. Instead of speculating, why not generate static client libraries directly from the exact same specs that are already being used to generate the APIs that are being consumed?
I’d like to think Ramses is at Level 2.5 of the RMM. Level 3 is on the horizon and I’m excited about the prospect while simultaneously wondering if the dream of dynamically generated UIs will ever become a reality. Look at what happened with HTML. We like to keep the interface separate and purpose-built because it’s better for human use.
Varieties of API spec usage in the wild
In looking into the stacks of numerous companies, I’ve noticed the following levels of maturity in spec utilization, which seem to be relatively analogous to the RMM insofar as there are levels of declarativeness involved:
- Level 0: no spec, APIs are defined willy-nilly. Sometimes there are RPC endpoints, sometimes there are REST endpoints, or they’re mixed together. Even if frameworks or libraries are used to ease the pain, all endpoints need to have at least the following boilerplate things custom written and maintained for them: serialization, URL mapping, validation, authentication/authorization, versioning, testing, database queries in views, etc.
- Level 1: Standalone spec. For example, having a Swagger file maintained in parallel with the rest of the custom code from Level 0 in order to generate docs or act as an ideal source of truth for discussion. Not bad but much more can be done.
- Level 2: Spec plus code generation. Using a high level spec file as a DSL to generate methods, classes, types etc. which are exposed openly in the codebase and subsequently modified for the problem domain. Pretty cool but why introduce additional maintenance if it’s avoidable?
- Level 3: Fully executable spec. Since the maturity model is just extending the metaphor of HTTP as far up the stack as possible, it’s a small step to think of code that would be generated at Level 2 as if it were data that’s actually part of the underlying protocol. Resources and their corresponding behaviors that are mapped all they way down “just work”. Do Level 2, but at runtime. Don’t modify the generated code. Instead, hook into it as needed.
One more thing from the Unix philosophy:
“Developers should avoid writing code by hand and instead write abstract high-level programs that generate code. This rule aims to reduce human errors and save time.”
Customizing opinionated HTTP API instances
The holy grail of API design is making the robots do more work so the humans can be free to think about features and not about plumbing. Surely there’s no way that we can think of everything in advance of implementing an API for the next most amazing and special disruptive product. It’s true!
What about async needs or failover techniques like message passing and supervision? Those can be a matter of the server environment or the language itself. I’d personally like to give these these capabilities first class support in Ramses and see similar projects or ports for other systems that might already have these things built in.
to the future!
One thing I did not cover specifically: polyglot scenarios. There is some FUD/NIH around “not my language” or “not my stack” that can still seem daunting. I think containerization is a big help here. Plus, if new services are made with executable specs and are easily hackable, the cost of adding a different stack to the mix is nothing compared to the cost of maintaining boilerplate spaghetti.