Services are all the rage. Service Oriented Architecture, Microservices and the like all promise the delivery of true components. Just like the physical industries have been doing for decades, constructing complex systems out of ready-made, standardised pieces. Building a piece of software could then be reduced to picking and choosing the right service providers and crafting a process which consumes these services.
Currently a lot of architectures are pushing the idea of services not only for consuming ready made third party components but also for internal components. While this in general is a good idea, it should not be considered the only way of doing it. In this blog post, I’ll argue for reinstating the venerable library or jar file instead of blindly promoting services for internal use.
Libraries versus Services
First, let’s define a service. (Note: we are talking about a service as in an architecture, not a service as in OO sense.) A service is a component that manages some specific set of functionality, that can be deployed independently and exposes it’s functionality via an API that can be consumed out-of-process. Typically this means that there is a web based or RMI like API which can be consumed. The state managed by the service is kept in an isolated database. This setup has a number of advantages: each service can be developed in isolation, using its own technology stack and its own deployment demands. It just has to offer an API that can be used by another software stack as well.
Compare this to a library: a library also manages some specific set of functionality and has an API but it tends to be language dependent. It also typically does not provide any state management so if it needs some sort of state, a data source will need to be provided by the consumer. Using a library is thus much more limiting than a service but both encapsulate and isolate functionality at some level.
So why do I prefer libraries?
Standardisation of services
First of all, comparing services with physical components is a bit of a stretch. Software lives on a much higher abstraction level and as such the scope of functionality as well as domain models behind it can wildly different between two architectures. Slowly some standards are evolving for some subjects, but for example in the online payments industry, there is no such thing as a common API for performing a simple online payment. Even the functionality offered can differ hugely: multi currency, fraud detection, multiple partial capture possibilities etc etc. Thus, replacing such a service is not a simple plug-and-play action. It still make sense of consuming services if it is a third party. Forcing ourselves to do it for internal services just so we could be able to replace it later with a standardised service is expected to be as painful as with a library.
One of the premises of a service is that it is an isolated stack. This means that the API has to be exposed in some platform independent way. Typically this means exposing it over HTTP using a text based (or in some cases binary) protocol. Also, since it has to be able to be deployed isolated, accessing the API is supposed to happen over the network. In general, calling a Java function directly from a jar or via a web service has a huge latency difference, in the order of 5-8 orders of magnitude. That is, a web service call is at least 100.000x slower than a native java function call. Now imagine if you have to call multiple services in sequential order, all requiring network hops… And the cherry on the cake is: network connections can and will fail. The advantage of using a library here is clear: not only are they faster, transaction management is much easier.
Horizontal vs vertical scalability
An advantage often mentioned for services is that they can independently scale. While this is true for that individual service, it is not necessarily true for the architecture as a whole. Typically what will happen is that as the business grows, one of the services becomes a bottleneck. This one then is moved to a bigger machine. If it is possible, the service itself might be horizontally scalable and thus be distributed over multiple machines. Once the upgrade has taken place, soon another service will be the bottleneck. And so we keep on finding new bottlenecks. From an architectural point of view this is still vertical scaling; each functional component is scaled individually until we hit a wall somewhere as is always the case with vertical scaling. If the application’s services instead were build using libraries we wouldn’t have the luxury of scaling vertically. Instead, that step would have to be skipped and de whole architecture would have to be horizontally scalable. That is, divide the workload into separate partitions (instead of dividing the process into separate steps) and handle the workload on a processing unit containing the full functionality. This, while difficult to achieve, ultimately leads to horizontal scalability.
If all services are maintained and upgraded in an isolated way, integration testing becomes a lot more complex. If a service is upgraded to a new version, how can you guarantee that it doesn’t break existing processes? While a lot of this has to do with API management (and hopefully you can avoid versioning your API!), the concern is that if your service is consumed by multiple clients, upgrading your functionality is somewhat limited by those clients. You cannot simply do a complete overhaul without breaking the API. (Again, proper API design helps here, but only up to a point.) So either you have to internally refactor and use adapters to the existing API, causing additional complexity. Or you have to break contract and therefore breaking the isolation of that service and force all consumers to upgrade. From a consumer point of view, while in theory you should be able to rely on the API contract not to change, in practice any upgrade of the consumed service is something that has to be tested. So there has to be some central governance for these services. Of course, this holds for libraries as well. But here libraries are under control of the consumer: the consumer dictates when the library is upgraded, not the service provider.
Hammer and nails
While services do have their own benefits, they are not the solution for all problems. If you have a hammer, everything looks like a nail, and the same thing can be said for services. In many cases, using plain old libraries is a much easier and faster solution. For me, it even holds if the service or library needs to manage data: partitioning of data is then necessary but from scalability point of view, that might not be so bad.