programming is terriblelessons learned from a life wasted

modules + network = microservices

Introduction

Microservices are a recent trend in software architecture, but the ideas behind are as old as the dawn of time (1 Jan 1970). To understand microservices, we need to understanding why we decompose software into services, and in turn, why we decompose services into modules.

The tradeoffs involved building modular software apply in both the large and the small, but we must not confuse the goal for the methods. We use modularity to reduce complexity, but often end up enabling it.

Modules

To save time i’ll skip straight to quoting Parnas’ “On the Criteria To Be Used in Decomposing Systems into Modules”–

We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others.

Parnas argues that the point of modularity is not one of reuse, but one of concealment, or abstraction: hiding assumptions from the rest of the program. Another way to look at this is how easily an implementation could be grown, deleted, rewritten, or swapped with a different system altogether, without changing the rest of the system.

Unfortunately, decomposition is genuinely hard: breaking your code into pieces does not always mean that the assumptions end up in different parts: it’s very easy to build a system out of modules that tightly depend on each other. Learning how to decompose software is a hard thing to do, and you will have to make a lot of mistakes before you start to get it right.

It is a tradeoff: a module brings extra overhead, and can be harder to understand where it fits into the larger system, but can bring simplicity and easier maintenance too.

Distributed systems

Decomposition, like many things in life, gets harder when you have more computers involved. You must decide how to split up the code, and also decide how to split it across the computers involved. Like with bits of a game world spread across a litany of global variables, spreading bits of state across a network is a similar world of pain and suffering.

Splitting things across a network means that the system will have to tolerate latency, and partial failure, and it is impossible to tell a slow component from a broken one. Keeping data in sync across a network while tolerating failure is an incredibly hard engineering problem known as consensus.

In my experience, all distributed consensus algorithms are either:

1: Paxos,

2: Paxos with extra unnecessary cruft, or

3: broken. - Mike Burrows

Although consensus can be avoided, the underlying problems cannot. Decomposing a system (into parts that run on different machines) is neither straightforward or easy, but far more treacherous. There are many techniques to make it easier, like statelessness, idempotence, and process supervision, and many others worth discovering too — but one technique stands out above all: uniformity.

It’s easier to handle talking to a bunch of machines if they can be expected to behave in a similar manner. Having a common interface was one of the major design principles behind Plan 9, which connected the operating system together through the filesystem.

Another distributed operating system, Amoeba, was built as a microkernel glued together from services using a common rpc mechanism. Once an interface for a service had been defined, client stubs would be generated to use the service.

Erlang is yet another platform for distributed systems, but unlike the former, uses asynchronous procedure calls to communicate—the code is forced to handle the possibility of latency, but can now achieve parallelism and other forms of concurrency. Similarly, twitter’s finagle library uses futures to achieve the same end: a uniform approach to connecting services together asynchronously.

Exposing the asynchronous nature of a network call can seem counterintuitive to Parnas’ advice on decomposition: surely the network is hard and likely to change and therefore worth hiding? Almost. The nature of the network protocol involved, and the particular machine involved are worth hiding, but hiding that the network is unreliable does not let code deal with it effectively.

A common interface, sync or async, allows easier interoperability between components of a distributed system, as well as being able to reuse code, code generation tools, and many other tools involved in deploying, monitoring, and debugging systems. Like with modules, the existence of a common interface does not guarantee a loosely coupled system, but it can be a step in the right direction.

Services

Once you have a distributed system built from modules, you almost have one built from services—your large program has been broken into smaller communicating parts. Even the most simple web app is often broken into a database, a file store, and a http server.

The real difference between a module within a distributed system and a service, is that a service runs separately and independently of the system that is using it. Like with a good module, a good service handles a hard or changing problem, and like any module, a service comes with maintenance costs.

Running one service is a burden, keeping more of them running is a full time job. Each new service must be configured to be able to find, authenticate and communicate with each other. Although splitting a system up allows the possibility of partial failure, it’s often just another thing that can go wrong.

Successfully deploying a system built from multiple services is both its own reward and punishment.

On the other hand, a service done well can allow extensive reuse, reimplementation, and better failure handling, but the real reasons for services are often social. There are two services because there are two teams building it.

Microservices

One good example for microservices is prototypes. A new feature can be developed alongside an existing system, without disrupting or changing the older code. Prototypes can often turn into bad examples of microservices too — the service is abandoned, or no-one knows how to run it any more — but prototypes can always be merged back in.

Really, It is more important to build a system that admits microservices, than it is to built out of them entirely. Once you admit something is running across the network, it isn’t much of a stretch to admit it running on a different service entirely. Without a common framework or ecosystem for microservices, the maintenance burden will outweigh many potential benefits.

A well engineered distributed system will likely have some elements of loose coupling, uniformity, and modularity, all essential for making microservices successful. The real question is not “should I write my system as microservices”, but “What sort of modules should I break my system into” and “what benefit is there from running it as a distinct service”.

Conclusion

Decomposition, be it into modules or services is a hard task, and often far easier in hindsight. There is no obviously right or wrong answer, but a series of tradeoffs that either work for or against you, which can change over time too.

Over time your problem will change and your software will have to follow, allowing loose coupling, and by extension, microservices gives your software more opportunities to grow, but it is up to you to work out if it is worth doing.