Old school enterprise architecture and cloud architecture have different service concerns and value different ReST framework features. Microservices allow you to decouple language choices, so a move to the cloud is a great time to re-think your ReST framework selection.
Developing a justification for microservices takes a while, so I will assume you have already felt the pain of living with an old school enterprise, or monolithic, application and understand a bit about cloud or web scale applications, microservice architectures, or fine grained SOA architectures. If not, see what James and Martin have to say to come up to speed: http://martinfowler.com/articles/microservices.html
In our context, the root problem with monolithic applications is complexity; the core benefit of a microservice architecture is simplicity. Many people are finding that managing one complex application is much more difficult that managing many very simple pieces - why microservices have become so popular in just four years. Each of our ReST framework selection criteria simplify some part of the develop, deploy, support, maintain cycle.
Adopting microservice archtecture requires some mind-shifts:
When you make these shifts, you will spend more time in design to simplify and eliminate code. When you no longer need complex domain logic in your middleware and complex transaction logic because of unnecessary data store complexity, what you value in a ReST framework changes dramatically - exactly what we are exploring.
At a high level, we want a framework that can produce a least cost implementation of a ReST service, from both a development and operations viewpoint.
Beyond that, goal definition becomes a bit circular, with the criteria helping define what is a least cost implementation. As we develop each of the criteria, we will identify what is least effort from a development, maintenance, and support perspective.
Each framework should provide a bare minimum of
The driving concern here is ease of use. Some frameworks are very plain and simple to use. Some have many ways of doing the same thing and it can be difficult to piece together the best strategy for a given situation. Familiarity with a language or framework may weight decisions in another direction. This is simply too subjective to compare meaningfully. Instead, we can compare the supporting documentation and you can decide how important that is.
If the framework does not have good documentation and community support, the time you save in advanced features will be wasted on figuring out how to use them. The only thing more frustrating than tedious work is digging through documentation trying to figure out how to use the framework.
Robust frameworks generally provide base documentation, tutorials and example code, and a community support forum.
I value examples over base documentation because the examples show context – how to glue the bits together, and base documentation is a bit like a large picture puzzle. Next most important, a knowledge base of existing questions and answers where others tried to extend the examples or use the base doc – lessons learned from the real world. Finally, an interactive support forum. Why last? Because in general, I solve my problems before I get responses. The existing forum provides immediate answers. However, when remediating a difficult to identify bug, the support forum can save the day.
Non-blocking services are not new; Java 6 introduced the Servlet 3.0 specification and Spring 4 made it easy to code in the Java world and Node.js derives a lot of its prestige from its high capacity due to its non-blocking model.
Although writing non-blocking code has become considerably easier in recent years, it still requires extra code and attention to introducing blocking calls that will destroy that performance. Once your service starts to hit, say 500 req/sec, this extra work starts to pay off.
You may start writing your service having a realistic expectation of load and know that the simplicity of a blocking strategy is fine, or that non-blocking will be required. You may also want to hedge your bets by choosing a framework that provides both.
Input validation is the ability to qualify request variables and reject them if found lacking. Qualification could be required parameters, field length constraints, composite parameter constraints (either this field or that specified), etc.
In addition to rejecting invalid requests, input validation should generate rich error information, describing each of the fields that failed validation and why.
Finally, the validation system should be easily extendable; you should be able to create your own validation rules for edge cases.
Validation should be through configuration, templates, annotations, or something similarly simple.
Writing homegrown validation is tedious, usually results in uneven application, and should be unnecessary now that Hibernate has provided a solid standard that has been implemented/copied in most modern frameworks.
Authentication is the process of proving someone is who he claims to be. Authorization is the process of restricting access to appropriate callers.
Authentication should be controlled from your front door. This reduces load on your authentication server and allows changing authentication schemes, or supporting multiple schemes, at one place instead of throughout all of your code bases.
Offloading authentication to a web proxy server assumes that the proxy exists on your application/service stack boundary and that boundary is secure. Much the same assumption you make for SSL offloading.
Consider the alternative; requests are authenticated in your code. Where is it authenticated? At the application layer? At the mid-tier? At the service layer? All three? You start to see the problem: duplicate code at every layer requiring coordinated code base changes and a 3x increase in authentication calls.
Authorization, on the other hand, must be done at the access point. The application layer must restrict access to features based on authorization. The services may have to authorize as well, if there are clients other than the application that can access them. If you know that the only access is through an application and it can appropriately restrict access, you don’t care about authorization, but be wary of what you “know”.
Authorization should be implemented as simply as possible, through annotations, schema, configuration, etc.
How is data is handled from the wire to the service code to the upstream provider and back again? Since all modern frameworks have transparent serialization and deserialization, this leaves internal data handling to compare.
Some frameworks serialize inbound parameters into an intermediate representation. In Java, this is an annotated POJO. Others serialize data into a hierarchical map representing the inbound JSON. The difference is usually the language: traversing an object map in Java is tedious while in Groovy, Ruby, Python it is trivial. This begs the question: How does the intermediate representation serve us? Is it simply a consequence of a language choice?
POJOs offer some code clarity but is that worth the effort required to create and maintain them?
How simple is it to create a new service?
Spring + Jersey service implementations separate concerns into
One of the big advantages of the Service layer is the ability to wrap multiple database calls in a transaction. If we optimize for speed, we make those operations primitives in the data store, effectively eliminating the need for a Service layer.
Other categories have addressed how to simplify input validation, authentication, and eliminate intermediate representations. We have just eliminated the need for a service layer, so what is left? Nothing that requires code to implement.
Templating and configuration are clearly simpler than writing code.
Unit and integration testing are essential to most coding efforts. Even if we write zero code, we want guard rails to highlight problems in our configuration/template, mappings to upstream providers, or the framework itself.
If our service implementation approaches the zero code case, we can test sufficiently through an external driver and use facilities like Docker for upstream provider mocking to run these tests locally and during CI builds.
Testing frameworks are generally tied to languages, but some have ReST framework integrations that significantly simplify testing within that framework.
If you have to add business logic to the service, you begin to need unit tests and mocking facilies.
What testing support is needed must be evaluated in the context of the ReST framework and the code base.
Any framework we choose must have connectors for the upstream providers we will use. This collection should be as robust as possible because we cannot anticipate future needs. We don’t want framework limits to dictate infrastructure choices.
Flexible infrastructure decisions allow us to eliminate code.
There are two difficult to define categories here: everything you know you need, and everything you might need in the future. Let’s set a base criteria of a document store, a relational store, a ReST provider, and a SOAP provider. And at a higher level, a large number of connectors
Rich error responses include tight control over HTTP response codes and the ability to attach additional data to help the client understand what went wrong and how to fix it.
Meaningful response codes clarify the API and its behavior. If, for example, you can return both 500, and 503, the client can tell the difference between a request that will never succeed due to a server logic fault, and one that they should retry later because an upstream provider was temporarily unavailable.
Fine-grained response codes also support rich monitoring dashboards providing error counts and rates and better indicate the actual problem.
Rich error responses improve the client experience, allowing clients to understand the root cause and possibly fix it immediately or create a bug report with details that will allow it to be located quickly.
Rich error responses are required to monitor and manage the service network. We want to be able to configure the framework to return errors that the client can use to develop retry strategies, identify root causes of faults, etc.
Error responses must be configurable, from setting custom HTTP response codes to filling in error details to enable fast fault remediation at every downstream client.
API documentation consists of domain knowledge and API details. There is no way around describing your domain and how to work with it. This documentation forms a foundation the client should easily be able to apply the API to.
The API details documentation is the tool the developer will use to understand manipulating the domain and will come back to time and again.
We have identified good documentation as a key factor in selecting a ReST framework. Likewise, it will be key to your client’s adoption and success.
Frameworks like Swagger have set the standard for API detail documentation. There are competitors, but to be considered, they should provide similar functionality:
Swagger integrates with just about everything. Integrating it with a Spring + Jersey service means a lot of annotations to identify paths, types, parameters, response codes, and narrative. It gets pretty ugly so I have hidden it in the interface files in the past.
If you have never seen Swagger in action, check out this live demo: http://petstore.swagger.io
Frameworks like Node.js loopback generate Swagger doc from the schema you define, leaving only narrative and response codes to write. This eliminates a lot of annotation writing and code clutter compared to the Spring + Jersey case.
Systems like Swagger can generate documentation in the form of brief descriptions, resources, URIs, input and response parameters. This is presented in a slick web app served from your service. It allows clients to read, experiment with your service. Swagger documentation can be generated from a schema/template or generated during a build step by introspecting several languages and frameworks.
If you can automatically generate documentation in this detail, you can also generate client stubs. Clients that use intermediate representations will find this a great benefit. Some manner of client stub generation should be provided whether it is Swagger, WADL, or RSDL.
Great APIs take good care of their clients. A robust system like Swagger also provides an ad hoc test bed and client stub generation. Swagger already covers many languages and frameworks so the question is likely: how much do you have to do? In Java, you have to provide annotations on your interface classes and a separate build configuration. Systems like Node.js Loopback infer all that is needed from the schema – which you can augment with domain knowledge.
What does it take to deploy your service? If it is run from a servlet/application container, then you have to maintain and manage that container. Even if you containerize the servlet/application container, you still have manage that container as you build the Docker image. Ansible would suffer the same need. What if your framework built a single deployable package that could be run on its own.
Most ReST services don’t really take advantage of any of the advanced features of servlet/application containers – they are overhead. New frameworks like Spring Boot, DropWizard, and Node build a single deployable package.
Monitoring is essential for
Collecting and reporting these metrics is a bit tedious and is frequently not implemented. This is a catastrophe for operations leaving your customers to identify errors, no way to anticipate approaching capacity limits, little ability to analyze SOA faults.
If your framework does not provide monitoring, you must build your own. On the plus side, if you build your own, it should work on all services produced with the framework.
What are the essential metrics?
If your monitoring system also logs request and response parameters, you have eliminated the need for logging.
Logging is essential for monitoring, support, and debugging. It will be provided by the logging facility, the framework, your custom code, and your custom entries. As with other concerns, the less you have to do, the better.
Key log entries are:
If the base functionality is not provided by the logging facility or ReST framework, one or the other must be extensible or you will have to manually add log lines – more needless work.
The least amount of logging required is the highest goal. This is determined by your ability to choose data stores and monitoring integrated into the system that will make boilerplate entries for you like call duration and frequency. When you have to log, it must be consumable by the log aggregator (ELK, Graylog, Splunk, etc.) This is really a go-no-go choice: the framework provides all facilities or it does not.
These features simplify so much that I consider them essential
These features are all desirable but, if missing, don’t have quite the impact of the first group