I first became familiar with Chris Richardson through his online resource, Microservices.io. Which, to be honest, provides an overwhelming amount of information - information that I've deferred consuming because I didn't really know how to approach it; especially with my lack of microservices experience. I'm more of a book person. So, when I heard that Chris was coming out with a book, Microservices Patterns: With Examples In Java, I was thrilled. So thrilled, in fact, that I couldn't wait for it be published. Instead, I purchased it through Manning's Early Access Program (MEAP). I've spent the last week reading through it and I've found it to be a fascinating, pragmatic, and holistic exploration of microservices development.
| || || |
| || |
| || || |
The whole book follows Mary - the CTO of Food To Go, Inc (FTGO) - as she attempts to grow her business by refactoring her old monolithic application into a microservices architecture. She's doing this because her development velocity has dropped and upper management is becoming increasingly frustrated with the engineers' inability to ship new features with sufficiently-high quality.
Mary is, of course, suffering from what Richardson terms, "Monolithic Hell". And, while a monolith represents a large, sprawling application, Richardson focuses on a thin-enough slice of the FTGO migration such that he can get his points across while keeping the content incredibly consumable. Most conversations about architecture tend to be either too simplistic or too complicated. But, Richardson manages to hit the sweet spot, conjuring up a domain model that is small enough to [almost] keep in your head; but, complex enough to require interesting cross-service workflows.
Right off the bat, I appreciated that Richardson is pragmatic in all things. No topic in the book was discussed as a "silver bullet" or the "one solution". Everything was a calculated choice - a set of clearly-defined trade-offs. For example, even the idea of moving to a microservices architecture was presented as one that should be avoided until it is absolutely necessary:
Another challenge with using the microservice architecture is deciding at what point during the lifecycle of the application you should use this architecture. When developing the first version of an application, you often do not have the problems that this architecture solves. Moreover, using an elaborate, distributed architecture will slow down development. This can be a major dilemma for startups whose biggest challenge is usually how to rapidly evolve the business model and accompanying application. Using the microservice architecture makes it much more difficult to iterate rapidly. A startup should almost certainly begin with a monolithic application. (Kindle location 416)
Richardson also approaches microservices from a holistic standpoint, talking about team dynamics as a completely separate concern on top of the technology. Of course he covers topics like Conway's Law and the "Reverse Conway Maneuver"; but, he also includes the fact that engineers are emotional, elephant-riding creatures who needs to be sold on the idea of microservices:
Adopting the microservice architecture changes your architecture, your organization and your development processes. Ultimately, however, it changes the working environment of people, who are, as mentioned earlier, emotional creatures. Their emotions, if ignored, can make the adoption of microservices a bumpy ride. (Kindle location 718)
And, his strategies also extend to the business as a whole, addressing managers and leadership that may be hesitant to take on big refactoring or divert resources away from the active development of new features:
An important benefit of incrementally refactoring to a microservice architecture is that you get an immediate return on your investment. That's very different than a big bang rewrite, which doesn't deliver any benefit until it is complete....
Another benefit of being able to deliver value earlier is that it helps maintain the business's support for the migration effort. Their ongoing support is essential since the refactoring effort will mean that less time is spent on developing features. Some organizations have difficulty eliminating technical debt because past attempts were too ambitious and didn't provide much benefit. As a result, the business becomes reluctant to invest in further clean up efforts. The incremental nature of refactoring to microservices means that the development team is able to demonstrate value early and often. (Kindle location 10769)
From a technology standpoint, Microservices Patterns covers a wide range of topics, from hexagonal architectures to testing and continuous integration to messaging patterns to the creation of observable systems. Because of this scope, some topics are discussed in more depth than are others. But, again, Richardson really finds the right balance, bringing in enough detail to facilitate meaningful conversation without overwhelming the reader.
In fact, he organizes the content in such a way that you can somewhat self-select into how deep you want to go. In each section he prefixes the deep-dive with a TL;DR (Too Long, Didn't Read) style pros-cons list. Then, he summarizes all of the points at the end of each chapter. This lets you access the high-level points without having to necessarily read every word.
And, to be honest, I did only skim the two chapters on "Testing Microservices" and the one chapter on "Deploying Microservices". While I am sure these topics are great, it was more information than I felt I could manage to absorb. Deployment is outside my day-to-day purview. And, I've written only a handful of tests in my life. As such, I wanted to make sure that I left enough room in my unfrozen-caveman lawyer brain for all the other chapters on topics like Domain-Driven Design (DDD), inter-process communication, and data synchronization.
One of the sections that had an especially profound impact on me was the discussion of which service should handle a specialized type of query - a query that could find available restaurants located near the requesting user:
However, even queries that are local to a single service can be difficult to implement. There are a couple of reasons why this might be the case. One reason is because, as I describe below, sometimes it's not appropriate for the service that owns the data to implement the query. The other reason is that sometimes a service's database (or data model) doesn't efficiently support the query. (Kindle location 5659)
.... If the FTGO application stores restaurants in [some other kind of] database then implementing the findAvailableRestaurant() query is more challenging. It must maintain a replica of the restaurant data in a form that is designed to support the geospatial query. The application could, for example, use the Geospatial Indexing Library for DynamoDB that uses a table as a geospatial index. Alternatively, the application could store a replica of the restaurant data in an entirely different type of database, a situation very similar to using a text search database for text queries. (Kindle location 5675)
.... Given that this service owns the data, it makes sense, at least on surface, for it to implement this query operation. However, data ownership is not the only factor to consider.
You must also take into account the need to separate concerns and avoid overloading services with too many responsibilities. For example, the primary responsibility of the team that developers Restaurant Service is enabling restaurant managers to maintain their restaurants. That is quite different than implementing a high-volume, critical query. What's more, if they were responsible for the findAvailableRestaurants() query operation the team would constantly live in fear of deploying a change that prevented consumers from placing orders. (Kindle location 5675)
This kind of blew my mind because it was the first time I can remember seeing a service whose sole responsibility was making another service's data more accessible for a given task. Though, as Richardson points out, this is really just a more generalized abstraction of the idea behind a full-text service like Apache Lucene (which provides full-text indexing on top of another data-store).
I suspect - or rather, I should hope - that seeing this separation of concerns will completely change the way that I approach drawing service boundaries. Instead of just thinking about "data" ownership, I need to start thinking about "business capabilities". A service should provide a "business capabilities" - a thing that provides value - rather than just be a repository for state.
One other thing that this topic underscores is the absolute importance of data synchronization, replication, and asynchronous messaging in a microservices architecture. That is a theme that Richardson weaves throughout the entire book, whether he's building microservices from scratch or incrementally refactoring a monolith into a microservice (a topic that receives its own chapter). He makes it clear that data synchronization is a foundational force that truly makes all the other things possible.
NOTE: For a book that focuses entirely on asynchronous message patterns, check out Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions by Gregor Hohpe and Bobby Woolf.
There's a ton of other stuff I could bring up. Like the fact that Richardson actually talks about authentication and authorization in a distributed system - something that I don't think gets explored nearly enough. Or, the fact that he explains Domain-Driven Design in a way that felt totally accessible. And, that demonstrated how even very simple classes can have behaviors.
To end this review, perhaps I can just share a few other experts that were meaningful for me.
One pain-point that I've felt deeply at work is the use of shared-libraries. Or, rather, the abuse of shared-libraries. As such I really appreciated that he advocates for shared-libraries in the case of relatively static functionality:
.... strive to use libraries for functionality that is unlikely to change. (Kindle location 992)
At work, we are currently in the process of decomposing our monolith into a distributed system. This makes the existing monolith a bit of a "moving target". We have to continue active development on it; but, do so in a way that doesn't over-leverage the team(s) building the new hawtness. He articulated these issues very well:
It's important to be sure that these problems are because you have outgrown your architecture. A common reason for slow delivery and buggy releases is a poor software development process. For example, if you are still relying on manual testing, then simply adopting automated testing can significantly increase development velocity. Similarly, you can sometimes solve scalability problems without changing your architecture. You should first try simpler solutions. If, and only if, you still have software delivery problems should you then migrate to the microservice architecture....
The most important lesson learned over the years is to not do a big bang rewrite.... A big bang rewrite is when you develop a new application - in this case a microservices-based application - from scratch. Although starting from scratch and leaving the legacy code base behind sounds appealing, it is extremely risky and will likely end in failure. You will spend months, possibly years, duplicating the existing functionality and only then can you implement the features that the business needs today! Also, you will need to develop the legacy application anyway, which diverts effort away from the rewrite and means that you have a constantly moving target. What's more, it's possible that you will waste time reimplementing features that are no longer needed. As Martin Fowler reportedly said, "the only thing a Big Bang rewrite guarantees is a Big Bang!" (Kindle location 10738)
One thing that Richardson said that gave me pause was that, "exceptions are rarely logged." As someone that has historically logged "all the things", I was piqued by his description of error aggregations services that will provide meta-data on top of the raw error content. This is something I should look into.
A service should rarely log an exception.... A better approach is to use an exception tracking service. As figure 11.15 shows, you configure your service to report exceptions to an exception tracking service via, for example, a REST API. The exception tracking service de-duplicates exceptions, generates alerts and manages the resolution of exceptions. (Kindle location 9311)
Of course, I love it whenever I see someone identify that the "Transaction Script" pattern is perfectly legitimate when your system is not overly complex. It allows me to feel less junky about my every-day activities.
While I am a strong advocate of the object-oriented approach, there are some situations where it is overkill, such as when you are developing simple business logic. In such a situation, a better approach is to write procedural code and use what Martin Fowler calls the Transaction Script pattern. (Kindly location 3709)
I really liked the way Richardson explained Domain-Driven Design (DDD). And, about how the domain-model dovetails with the transactional boundaries of a distributed system. This is something I've been thinking about just recently. And, it's nice to see some my thoughts reflected in Richardson's writing. Though, it was sometimes hard to reconcile his Aggregate transaction notes with the idea of an OUTBOX domain-event transaction (in so much that a domain-event and a domain-mutation must happen atomically together in a transaction).
Another rule aggregates must obey is that a transaction can only create or update a single aggregate. When I first read about it many years ago, this rule made no sense! At the time, I was developing traditional monolithic applications that used an RDBMS and so transactions could update multiple aggregates. Today, however, this constraint is perfect for the microservice architecture. It ensures that a transaction is contained within a service. This constraint also matches the limited transaction model of most NoSQL databases. (Kindle location 3901)
Also, did you know there is a "Law of Holes" (my inner-child is currently ROTFL'ing)? This reminds me of the conversation we have internally a lot, phrased as "throwing good money after bad."
The Law of Holes states that "if you find yourself in a hole, stop digging". This is great advice to follow when your monolithic application has become unmanageable. (Kindle location 10876)
And, honestly, I could just keep going. I highlighted an absurd number of passages while I was reading this book. Hopefully the ones that I have shared can shed some light on both the broad scope of the book and Richardson's attention to detail.
For the past year or so, I've been trying hard to wrap my head around distributed systems architecture and microservice patterns (which is particularly hard considering my daily work keeps me inside a monolith). Much of what I've read on the matter is too broad and shallow. Other things are narrow but excessively deep. Microservices Patterns by Chris Richardson really strikes a healthy balance between the two. His book covers a range of topics; but, manages to cover them in what feels like a very appropriate amount of detail. This book is definitely a recommended read for anyone thinking about (or struggling to) move from a monolithic architecture into a distributed systems architecture.