The Death of microservices — Distributed Monolith 101
It does not matter the question; the answer is Microservices Architecture(MSA). Feel that way too? Are companies doing proper microservices at all? Would it be just everybody just calling “microservices” and doing all sins against microservices? Let’s adopt microservice now, okay. What problems are we trying to fix? I know I want to do it because it is everybody doing, are you sure? I think I know what I’m doing, are you? Well, “the road to hell is paved with good intentions.” The problem was happening before the SOA era; we had this problem before, microservices still have it, Serverless the same. OH, this is backend thing, buddy we are good at Frontend or in BigData, DevOps Wrong! It’s everywhere! If there are software and distribution, potentially, you will have this issue, which is not minor.
The Reality Mismatch
Very few companies are doing proper microservices. What companies are doing are not microservices. Microservices will likely stop to be a thing(to be honest if most of the company never did it — was they real anyway or just an illusion?). Do I believe in Microservices(MSA) for sure, do I think we should do it? Yes, but that will require discipline and effective learning, leadership and year, are we ready for it? Most of the cases no. It hurt a lot to admit, but there is one little thing called “Reality,” and we are not doing an excellent job with her. Should we give up? Hell no! Like everything in this life, it’s about education and discipline. Software is fragile, people come and go, so it’s easy to lose control, and things fall apart. I wish reality were different, but, unfortunately, it is not.
What problem are you trying to Fix?
If you are picking microservices, you SHOULD have the following problems:
Difficulties to Scale: It’s hard to scale up a monolith because of the coupling. It’s hard to work appropriately with specialized teams. It’s expensive to scale, and there is lots of waste.
Specialized Solutions: You want to work with different languages, different technologies, separate databases. Why? Because it is cute? No. There is the right tool for the job. Also, as you have more freedom, you can use people’s skills more productively and intelligently.
Freedom to Upgrade: Can you upgrade any lib, language version, server quickly in your solutions? If you have coupled, the answer will be no. Upgrading a lib could mean:
- Fixing a security potential breach
- Improving performance
- Reducing costs — Better code — using fewer resources
- Pure developer joy and productivity with a modern, up-to-date solution — a.k.a productivity.
- A simple bug fix
However, can you upgrade your libs(independent), wich out a mass migration? Often the answer is not.
However, microservices are not a free lunch. There are prices you need to pay. Here are the following prices you MUST pay:
- Infrastructure Complexity: Separate deploys, Dbs, Code bases.
- Complexity: in form, but not limited by Eventual Consistency, Distributed programming(failures).
- Duplication: Data duplication, Code duplication, DevOps code duplication.
However, naive managers have problems with code duplication. Even engineers think repetition is terrible. Well, it is awful, it’s a source of bugs, right? Yeah. However, simple is not that simple. You cannot eat the cake and have the cake.
You don’t want code duplication: Why did you pick microservices in the first place?
That’s it. It would help if you let it go. Code duplication is not bad as in comparison with coupling. Coupling is the trait of a monolith. Hold on; we have several services we don’t have a monolith, correct? Yes, you do not have a monolith. You have something far more worst them a monolith. You have a distributed monolith. Why is a distributed monolith worst them a monolith?
- It’s more complicated — you made it distributed buddy.
- It has more failure points — you made it distributed buddy.
Not only is worst but you still dont get:
Freedom to Upgrade — Because it is all coupled, thanks to you smart code reuse. Saved some lines of code but kill the main microservice benefit (ISOLATION). IF you are doing a poor microservice, stick with a monolith. That’s why I imagine Sam Newman often says, do not start with a microservice.
Botton line here: Code Duplication is fine. Go Type. Seriously. OH: but I have the same problems everywhere. It’s expensive, it sucks. Well, do you think a distributed monolith is cheap? I felt you moved to microservices because you want ISOLATION meaning code duplication.
Ways you can shoot-your self: Traps in Details.
There are so many ways you can kill yourself trying to implement microservices properly. It looks like this time; we did not make the same errors in SOA in the sense of tooling. We still have a long road ahead. What are the ways we can do microservices wrong, or how things can go wrong?
- You have too many microservices with the wrong boundaries — Go luck refactoring your distributed monolith. OH, Maybe a monorepo can help us — yeah hold that thought.
- Shared-Libs and Drivers: Yeah. It’s is how you create a distributed monolith. More to come.
- Lack of Encapsulation: Tables. People still share tables; you know the worst? It’s not only “tables,” you need to worry.
Is it a monorepo the solution or a part of the problem?
First of all, you are not Google. You do not have 2B lines of code. Second, if you already have a distributed monolith, yeah, a monorepo will make it easy to upgrade the best, It’s all or nothing. However, if you have proper isolation, proper design, no abuse of shared-libs, why do you need all code in one freaking place? You will likely start coupling things. Cripling you microservice! So dreamed isolation, and you might create a distributed monolith. Think twice. What problem are you trying to fix, and what difficulty you might create? Do no think decisions are free of side effects and consequences.
Shared-libs are evil
Don’t believe me? Look this video from a former Netflix Engineer who is on Facebook right now. Why are shared libs evil? Because of the 3rd-party dependencies, they introduce and coupling. Shared-libs start small, and like any addiction, they increase; you have hundreds of dependencies to make your software work. It won’t take time. When you have 1,10,100, 1k, or 10k services, you won’t be able to upgrade anything. Sounds familiar, which the monolith? Yeah, but worst. The issue is if you are a big product company, you know what I’m talking about, excellent. If you are in a service company might never see this issue as you leave the project after “deliver”(Good old waterfall), and it might take some time to see this problem to manifest.
Drivers are evil
There are only one thing wors the shared-libs, which is drivers. If to access a server, you need a driver, and this driver introduces lots of dependencies you are in deep trouble. Let me talk about an unpopular thing today called SOA. In SOA, we use to like to have: Services. Nothing else, nothing more. That’s why it is called Service Oriented Architecture. Services need to have a Contract and an Implementation. The contract should abstract a lot and be flexible and provide intrinsic interoperability. Which your custom driver just killed. Now no one that nows REST can talk yo you service you need that Jars of yours. What if you want to call the service with Python, Rust, V, GoLang? You need to have a driver per language.
It’s possible to have drivers and shared-libs that don’t suck, yes it is possible. But the implementation needs to be chirurgical. Meaning you do things you are not used to doing like:
- Mind your dependencies — Add as little as possible — ideally ZERO 3rd party dependencies. Be Lean on Dependencies.
- Copy Code: Why dont you copy some code instead of added 30 dependencies for five lines of code? Sound bad but isn’t.
- Use package explosion: Plenty of tools that can help you with that. More to come — will comment more soon.
- Encapsulation: Hide your code, have public interfaces(few), and do not leak your implementation; neither make it public.
In my previous post, I was talking more about coupling in the context of backward and forward compatibility, which is related to this post. There is only one thing worst than drivers, which is(often know as client-drivers or network-drivers) — corporate frameworks.
In the past, every company in brazil (10–20 years ago) had a corporate framework; these frameworks never worked well and often presented all these issues:
- Expensive: Often, they spend more time doing a framework than fixing real problems that the company needs to make money.
- Complex to use: Often based on a popular framework like Spring, Hibernate, or JEE but much harder to use.
- Lack of documentation: No Javadoc, no samples, no wiki, nada!
- Lack of abstractions: Often, make your life worst. Often do not provide the classical two levels of concepts to fix problems — level 1: Abstraction, Level 2: Low-level access.
- People hate it: They can’t use the web to search, they can’t tell their friends, they cant use it in other jobs.
Today corporate frameworks are less popular on their original “form”; however, they still exist — often called “shared-libs.” Just because it is the package like lib and you “call” a lib, it does not mean it is a lib. Today libs are the new corporate frameworks.
A Lib == framework is terrible when it falls in the following issues:
- Impose a specific programming model: Teams should have freedom of programming mode. Some people like Annotations, Mappings, ORM, Reactive others don’t.
- It has coupled with other libs: It doesn’t take time for your libs to depend on all your other libs. Coupling is the root of all evil. Them you can’t upgrade anything, can’t add anything in your service until all other services do the same.
- Hides to many things from you: This is one of the main traits of a framework, which means: “To frame the work.”
It does not matter how you can; it matters what it is.
It’s not only about Tables.
People still fail in isolating databases. Often companies have the same database cluster for multiple microservices(cost reasons, lack of automation, or simple corrupt practices); however, databases(when is your software of truth) is not the only thing you need to abstract and encapsulate. These are the other “things” you need to hide:
- All Datastores (SQL, NoSQL, NewSQL, In-memory) all of them.
- Caches (IMOC): Redis, Memcached, RocksDB, you name it.
- Configurations: XML, Json, Yaml, whatever.
Everything that is “public” is a contract. Kafka/Kineses/RabbitMQ queues are part of your agreement, You might not be able to hide them, but you should and need to treat them as contracts.
How can we fix this?
Not easy. However, there are things we can do to avoid or minimize these issues. To fix these problems, we need discipline, education, and other approaches. We also need to start accepting the penalty that every choice has, for this case, stop worrying too much about code duplication.
Do not accept jars from strangers.
We need to stop creating shared-libs for everything. We need to stop coupling shared libs together. We need to start leveraging: Protocols, Good Practices, Templates, and even looking at our work-mate microservice and copying the code from there. We need to stop pushing opinionated programming models on everybody like RxJava or Spring Boot. Do you want to use spring boot in your service, fine, do it, be happy! But don imposes on everybody else. Even if everybody wants the same capability or shared-lib right now, I bet you a beer that people will not want to upgrade at the same time as you will wish to in the future.
Currently, shared-libs are the number 1 cause of death to a microservices worldwide, right followed by shared databases. Protect your classpath.
Packages are your friends.
Packages provide isolation and abstraction. We need to use more of them. There are several libs and plugins for building tools that can get your 3rd-party libs and create a FAT jar or even explode the 3rd party dependencies in your classpath. OH, but Diego, it will use much memory, and that’s fine. Again accept the tradeoff.
Isolate your datastores, caches, and config-files. Do not let. They see beyond the service contract. Keep them internal to your service. Having proper isolation means you only need to apply backward/forward compatibility at your contract level, you are free on the database now if you need to provide back and forward compatibility.
Time passes fast. Companies grow, global refractories are not top priorities for most executives. You are protecting your data now will pay off in the long run.
It’s about Plataforms
Shared-libs, Frameworks, Drivers are not the only place for you to put your code. You have other better options like Services, for instance. Instead of creating a lib, you can create a service. However, not all problems can be resolved with services, especially if we go remote calls and pay the right price, for that can have at least four other options.
- Self-Service Automated Service
- Runtime Platforms
Tooling: It could be a build tool, simple script, UI. Any self-service tool your engineers can use in build time to fix their problem. For some use, cases will work, for others don’t.
Self-Service Automated Service: Instead of making a lib, you can make an internal self-service solution. i.g.: You could create a shared lib to do Cassandra Backups or create a transparent service that does not improve jars in people’s code.
Sidecars: Sidecars have their issues like hard to debug and not idiomatic. However, they isolate and abstract solutions without coupling with your classpath — another option instead of a lib. i.g: Lyft Envoy.
Runtime Platforms: Like JEE, Erlang, Spark, Kubernetes, where the abstractions are at the platform level and not at the code level.
There are options. We need to make our code pure and clean. Otherwise, we dont get the benefits we are advertising, and it will be a never-ending cycle of issues.
People need to get a deeper understanding of architecture and understand that choices(tradeoffs) have consequences which we must learn to accept(talking about duplicated code). We need to train people, and like in Lean thinking: “Learn how to see waste.” Education does not mean a simple training and game over. It is a continuous effort. Ideas need to be alive in people’s heads; otherwise, we won’t be able to him this war. Education is a must.