Valuable lessons to read if you have ever heard about microservices.
Building Microservices by Sam Newman - Think about it carefully before using it
This post summarizes points that I feel interesting and worth remembering. Contents are copied from the book.
“The Microservices architecture has many appealing qualities, but the road towards it has painful traps for the unwary. This book will help you figure out if this path is for you, and how to avoid those traps on your journey.” - Martin Fowler
Microservices are small, autonomous services that work together.
Ease of deployment
Optimizing for replaceability
Microservices are no free lunch or silver bullet and make for a bad choice as a golder hammer. They have all the associated complexity of distributed systems.
2. The evolutionary architect
Architects can have a direct impact on the quality of the systems built, on the working conditions of their colleagues, and on their organization’s ability to respond to change.
Our architects need to shift their thinking away from creating the perfect end product, and instead focus on helping create a framework in which the right systems can emerge, and continue to grow as we learn more.
Making decisions in system design is all about trade-offs, and microservice architecture give us lots of trade-offs to make.
Great software comes from great people. If you worry only about the technology side of the equation, you’re missing way more than half of the picture.
3. How to model services
We want to find boundaries within our problem domain that help ensure that related behavior is in one place, and that communicate with other boundaries as loosely as possible.
Loose coupling: A change to one service should not require a change to another. A loosely coupled service knows as little as it needs to about the services with which it collaborates.
High cohesion: We want related behavior to sit together, and unrelated behavior to sit elsewhere.
When starting out, however, keep the new system on the more monolithic side; getting service boundaries wrong can be costly, so waiting for things to stabilize as you get to grips with a new domain is sensible.
Premature decomposing a system into microservices can be costly, especially if you are new to the domain. In many ways, having an existing codebase you want to decompose into microservices is much easier than trying to go to microservices from the beginning.
Build if it is unique to what you do, and can be considered a strategic asset; buy if your use of the tool isn’t that special.
Keep your APIs technology-agnostic: Avoid integration technology that dictates what technology stacks we can use.
Avoid shared database at (nearly) all costs.
Two different idiomatic styles of collaboration
Request/Response: a client initiates a request and waits for the response.
Event-based: says thing happened and expects other parties to know what to do.
Highly decoupled: the client that emits an event doesn’t have any way of knowing who or what will react to it.
Don’t Repeat Yourself (DRY)
DRY more accurately means that we want to avoid duplicating our system behavior and knowledge.
Don’t violate DRY within a microservice, but be relaxed about violating DRY across all services. The evils of too much coupling between services are far worse than the problems caused by code duplication.
5. Splitting the monolith
Seam is a portion of the code that can be treated in isolation and worked on without impacting the rest of the codebase. Identify seams that can become service boundaries.
There is no need for this to be a big-bang approach. It is something that can be done bit by bit, day by day.
We can - and will - make mistakes, and we should embrace that. What we should also do, though, is understand how best to mitigate the costs of those mistakes.
With Continuous Integration (CI), the core goal is to keep everyone in sync with each other, which we achieve by making sure that newly checked-in code properly integrates with existing code.
Build pipeline: having different stages in our build. We build our artifact, and that artifact is used throughout the pipeline. As our artifact moves through these stages, we feel more confident that the software will work in production.
Continuous Delivery (CD) is the approach whereby we get constant feedback on the production readiness of each and every check-in, and furthermore treat each and every check-in as a release candidate.
CI builds artifacts, CD validates artifacts.
Embracing a culture of automation is key if you want to keep the complexities of microservices architectures in check.
Automate everything, and if the technology you have doesn’t allow this, get some new technology!
Optimize for fast feedback, and separate types of tests accordingly.
Avoid the need for end-to-end tests wherever possible. Focus on a small (very low double digits even for complex system) number of core journeys to test for the whole system. Any functionality not covered in these core journeys needs to be covered in tests that analyze services in isolation from each other.
Monitor the small things, and use aggregation to see the bigger picture.
We can never know what data will be useful.
Monitoring is one area where standardization is incredibly important:
Write your logs out in a standard format
Have all your metrics in one place
Have a list of standard names for your metrics
Use correlation IDs to track call chains across multiple services. With the right log aggregation tooling, you’ll then be able to trace that event all the way through your system. You can picture the whole cascade of calls.
Authentication is the process by which we confirm that a party is who she says she is.
Type in username and password.
Use fingerprint to confirm.
Use face to confirm (Face ID)
When we are talking abstractly about who or what is being authenticated, we refer to that party as the principal.
Authorization is the mechanism by which we map from a principal to the action we are allowing her to do.
Go with the well known: The easiest way you can mess up data encryption is try to implement your own encryption algorithms, or even try to implement someone else’s. Badly implemented encryption could be worse than having none, as the false sense of security can lead you to take your eye off the ball.
Don’t write your own crypto. Don’t invent your own security protocols. Unless you are a cryptographic expert with years of experience, if you try inventing your own encoding or elaborate cryptographic protections, you will get it wrong. And even if you are a cryptographic expert, you may still get it wrong. Reinventing the wheel in many cases is often just a waste of time, but when it comes to security it can be outright dangerous.
10. Conway’s law and system design
Melwin Conway’s law:
Any organization that design a system (defined more broadly here than just information systems) will inevitably produce a design whose structure is a copy of the organization’s communication structure.
Eric S. Raymond summarized:
If you have four groups working on a compiler, you’ll get a 4-pass compiler.
11. Microservices at scale
Systems that just act slow are much harder to deal with than systems that just fail fast. In a distributed system, latency kills.
A planned outage is much easier to deal with than an unplanned one.
At scale, even if you buy the best kit, the most expensive hardware, you cannot avoid the fact that things can and will fail.
The architecture that get you started may not be the architecture that keeps you going when your system has to handle very different volumes of load . You should “design for ~10x growth, but plan to rewrite before ~100x.” You need to do something pretty radical to support the next level of growth.
There is a danger that people will see the need to rearchitect when certain scaling thresholds are reached as a reason to build for massive scale from beginning. This can be disastrous. At the start of a new project, we often don’t know exactly what we want to build, nor do we know if it will be successful. We need to be able to rapidly experiment, and understand what capabilities we need to build. If we tried building for massive scale up front, we’d end up front-loading a huge amount of work to prepare for load that may never come, while diverting effort away from more important activities, like understanding if anyone will want to actually use our product.
The need to change our systems to deal with scale isn’t a sign of failure. It is a sign of success.
In a distributed system, we have three things we can trade off against each other: consistency, availability, and partition tolerance. Specifically, the theorem tells us that we get to keep two in a failure mode.
Consistency is the system characteristic by which I will get the same answer if I go to multiple nodes.
Availability means that every request receives a response.
Partition tolerance is the system’s ability to handle the fact that communication between its parts is sometimes impossible.
If our system has no partition tolerance, it can’t run over a network. In other words, it needs to be a single process operating locally. CA (Consistency and Availability) systems don’t exist in distributed systems.
Getting multinode consistency right is so hard that I would strongly, strongly suggest that if you need it, don’t try to invent it yourself. Instead, pick a data store or lock service that offers these characteristics.