Microservices are not Architecture
Uncle Bob posted a bathrobe rant on Xitter today and I really don’t know what to make of it. For those uninitiated, Uncle Bob Martin is one of the titans of the software industry, having written more software and seminal books than any of us could dream of. I’ve learned a lot from him though his writing that has made me a better engineer. In the rant, he makes the argument that microservices are not architecture. It’s an argument he’s made before in Clean Architecture, but here he does it with the clarity and flavor of spoiled milk. That said, I think he’s right. So, in a bout of personal arrogance, I’m going to try to make his case for him.
Before we start, let’s get a shared understanding of what a microservice is, though. We’ll define a microservice as a small independently-deployable unit of software. The exact implementation doesn’t really matter at all. The exact scale of “small” varies. I’ve seen some “microservices” that are a single AWS Lambda function and I’ve seen some “microservices” with half-a-million lines of code. This ambiguity in size is the first part of his argument. They’re not “microservices” they are just “services” and they come in a variety of sizes. There’s nothing about the definition that makes them “micro” just they are independently deployable.
We’ve been taught that microservices are where your architecture lives. When we draw out architecture diagrams on the whiteboard, our boxes are microservices with cute names like Galactus. This is deeply ingrained in the industry at this point and we’ve forgotten what software architecture actually is. Your architecture lies not in the microservices, but in the seams you define in your software.
Seams are the places where different pieces of software are joined together - clear integration points with defined contracts and behavior expectations. This concept goes by a few other names like “interfaces” and “ports” but they are all the same thing. Well-defined seams must be severable - I can take the software on each side of the seam and utilize it independently. Notice that we are still really abstract here. We’ve not talked about services, deployments, processes, or anything else. Just the abstract concept of a boundary between units of software.
Monolithic software has seams, operating systems have seams, embedded software has seams. A great example is in file systems. Linux can run on a variety of file systems because they all implement the same interface to the kernel (VFS). Do you need a basic fast file system? EXT4. Do you need high data integrity? ZFS. The seam allows you to choose the right tool for the job. As long as you abide by the behavior contract in the seam.
The abstractions you use as seams are your software architecture.
Well-defined seams offer you a really neat opportunity: you can split along them in a variety of ways that each confer different advantages and introduce different downsides.
You could simply split the seam as code packages. Create a different maven or npm module to keep the code isolated to prevent anyone from breaking the abstraction. The code still compiles into the same binary, but you get some measure of isolation without sacrificing performance.
You could split as individual processes on the same host using inter-process communication protocols. This allows you to have different modules in different runtimes or languages, with different OS-level configurations.
You could split as individual containers composed together as a single unit, using REST APIs to communicate between containers, commonly called sidecars. This enables each piece of the software to run independently to take advantage of differing container needs.
You could split as completely different services behind distinct load balancers, using REST APIs to communicate. This allows for independent scaling of each component, but incurs overhead in network transit time and request serialization/deserialization.
Regardless of how you split it, the seam is there. The contract is maintained.
A “microservice” is really just the last one. You’ve decided to split a seam using the highest level of separation you could: completely different services. This comes at a cost, but confers advantages.
In a high scale system, being able to independently scale different pieces of your software up or down is invaluable. Load is often inconsistent and unpredictable. Some pieces may be more popular in the morning, but less popular in the evening and vise-versa. It allows very clear boundaries on fault containers - you don’t have to worry about a botched process deployment killing a different process. You get memory and compute isolation - no more greedy processes taking all the host’s memory and CPU time.
But network traversal takes time, as does serializing your requests to and from JSON, as does the load balancer. It’s not a huge amount of time nowadays, but it’s still time. It also makes the seam behavior harder to enforce since there is no compile-time testing that the contract is being followed correctly. You have to deploy and pray or write massive suites of “contract tests” that assert the behavior at deploy-time. I’ve worked on teams with literally thousands of deploy-time tests to assert the behavior of a single microservice.
Microservices became popular because they solve a lot of the software challenges that come with monolithic single or multi-process software, but they do so at the cost of latency and contract enforcement. They have their place still, but they are not the one solution to rule them all. They are a tool by which you can split seams, but they are not your architecture. Your architecture is the seams.