Granularity and Communication in Microservices Architectures - Neal Ford
About this talk
This talk outlines the speaker's exploration of software architecture, focusing on the concepts from his book 'Software Architecture: The Hard Parts'. He discusses the intricate nature of software design, particularly in distributed architectures, and emphasizes the importance of trade-offs in architectural decisions. The speaker elaborates on the challenges of granularity and communication, introducing various tools known as disintegrators and integrators to aid in managing service sizes and their interactions. He identifies five disintegrators, such as service functionality and code volatility, that help to break services into smaller parts, and three integrators, like database transactions, that guide when to combine them. The session culminates in a detailed examination of communication patterns, particularly transactional sagas, and the implications of synchronous versus asynchronous interactions in workflows.
Full transcript
Uh I did a keynote this morning talking about my upcoming book architectures code. This is about my book two books ago, software architecture, the hard parts. And the way that book came about, uh, Mark Richards and I wrote the fundamentals of software architecture. And as we were writing that book, we kept bumping into examples that were just too complex for a fundamentals book because as you dug
into the examples, it got deeper and deeper and we said, "Okay, too much for a fundamentals book." And so one of the times we were taking yet another perfectly good example and kicking it out of the fundamentals book because it was too complicated. Mark said, "Well, maybe we should our next book should be about all these things and we should call it the hard parts." And that's
exactly what it became because once we finished the fundamentals book, we looked at the big giant pile of difficult problems we had and said, "Okay, what's in common with all these difficult problems in software architecture?" and it's basically the subtitle of our book, Tradeoffs. They all involve serious tradeoffs and and how to do trade-off analysis. And that's really the theme of this book. So, what I'm doing
today is taking sort of the summary of part one and part two of the hard parts book. Part one is about static coupling and granularity and part two is about dynamic coupling and communication and talk about these two difficult problems in distributed architectures. granularity and communication. So let's talk first about granularity or services in a microservices architecture. And this is a great example of something that Mark
and I really like to talk about a lot, which is this idea of iterative design in software architecture. I don't know of any architect who's so clever that they can just look at a difficult problem and the perfect granularity of services just falls out of their head onto a drawing uh surface or whiteboard or drawing tool etc. What you need to be able to do as a
software architect is to create a candidate architecture but then iterate on that design. That's how good architects arrive at good solutions. They're not just born with the intuitive knowledge of what good architecture looks like. They create a candidate and they iterate on that candidate. To be able to iterate, you need tools. And that's where I come in today. I'm going to give you a bunch of tools
to allow you to iterate on the granularity of your services in a distributed architecture. Now, all of you at some point have had some crafts person come to your house to work on plumbing or electricity or something like that. And you probably took a sneak peek into their toolbox to see what kind of tools they had. And they had a bunch of general tools like hammer and
screwdrivers. But they also had some exotic tools that are only useful for their particular specialization. What I'm going to show you today are the software architecture equivalent of the generic tools like a hammer and a screwdriver, but you should be on the lookout for very specific tools within your ecosystem because they exist as well. And naming them like we have is actually a good idea because it
gives you a vocabulary, a design language you can use as you talk about design. And so I'm going to talk about two different classes of these tools. Disintegrators, which are forces that encourage you to break things down into smaller and smaller pieces, and integrators, which are things that encourage you to bundle them back up into bigger pieces. I'm going to talk about five disintegrators and three integrators.
But I want to stress this is not an exhaustive list. You are meant to take this list as a starter kit and add your own to it as you go along. So let's talk about the integrators first. When should I break a service into smaller parts? And the first one of these is maybe the most obvious which is based around service functionality or the behavior of the
service. So for example, let's say that I have a customer service that currently exists. Sorry, let's do notification first. We have a notification service that currently exists and it consists of three components text, email and postal letter. And I want to use the service functionality disintegrator to break that apart. I will break that apart then into three smaller pieces. SMS, email, and letter. That's the perfectly sensible
way to break that apart based on behavior. However, have to be careful here because of this scenario which I was about to talk about customer profile preferences and comments. If I break that up into profile comments and preferences, these are still customer profile, customer preferences, and customer comments. I'm not inverting my entire architecture and turning it inside out. So in other words, I'm not taking the customer
profile and turning it inside out and saying this is a profile for everything in my ecosystem that has profiles. I'm not trying to generalize it. I'm not using this to make that bigger. This is always making it smaller. And so this is still going to be part of customer. So I'm going to give you a physics analogy here for a second. In subatomic physics, there are two
nuclear forces. the strong nuclear force and the weak nuclear force. And basically the universe works the way that it does because of the weak nuclear force except when the strong nuclear force is present and it completely dominates the weak Why am I talking about subatomic physics? Well, disintegrators are sort of like the weak nuclear force. So concepts like domain driven design are disintegrators and tools like this
are weak nuclear forces which is why I say over here I'm not going to use this disintegrator to disintegrate parts of customer there's still a customer of this because that's the strong nuclear force which is another way of saying don't use these disintegrators to create something that's foolish because there are other overriding concerns turns. Remember this is always about trade-offs in software architecture and using these things
creates trade-offs as well. So for every one of these because software architecture is all about trade-offs for each of these disintegrators and integrators. I'll talk about the trade-offs that go along with it. So for service functionality the good side of that is you're getting further and closer and closer to single responsibility and philosophically that is the correct size for a micros service is something that has single
responsibility and the service functionality the disintegrator gets you closer and closer to that and it also helps you create really fine grain services which may be a plus but can also tilt into a disadvantage because the disadvantages of of this disintegrator is this antiattern that we call grains of sand. It is very possible in fact likely in microservices if you're not careful to get too small and
when you create really teeny tiny little services then the entire performance of your entire of your whole system is network latency of these things calling each other to do useful work. And so when you go too small it actually becomes an antiattern. That's where uh the uh if you go to the final degree of fine grainness, you end up there. Um and basically you're taking all of
your workflows and breaking them down into uh atomic pieces. So service functionality is pretty obvious. Uh there's even a menu item in your IDE to help you with this. The extract method refactoring. So let's talk about one that's not as obvious, which is code volatility. This is not even a characteristic of the behavior of the code at all, but rather a meta characteristic of the code itself.
So for example, let's say that I have my notification service with text, email, and postal letter and I want to use the code volatility disintegrator to break it apart. And I notice that text and email code almost never changes there. But in postal letter codes constantly being tweaked because reasons like postal letters in the US that travel across state lines or legal contracts and so we're constantly
having to change the wording to make sure we're compliant in all 50 states or some rule like that that causes that code to change all the time. And so using the volatility disintegrator, I would break these apart into an electronic notification and letter notification where code rarely changes in the electronic notification and changes all the time in letter. Now there are two things I typically say parenthetically
at this point. The first one of which is you may have heard of this concept called volatilitybased decomposition. This is something that Juval Lori talks about a lot. In fact, he talks about it uh in hyperbolic terms. Uh he will tell you that volatility based decomposition is the only tool that you'll ever need. And I don't think that's true. It's it's a great tool. It's very powerful,
but it's not the only tool you need. It's not a religion. It's just one of the tools in our toolbox. But there's a lot of thinking around volatility as a uh a good uh indicator which brings up the second point I usually make here which is the extra benefit of looking at in our fundamentals book we codified these what we call laws of software architecture these things
that seem universally true and there were only three of them. The first one is the most important one that everything in software architecture is a trade-off. And there's a correlary to that law is if you think you found something that isn't a trade-off, you probably just haven't figured out what that trade-off is yet. This is a great example of that because code reuse is purely a force
for good, right? As an organization, we want to try to reuse as much code as we possibly can because the rationale being the more code we can reuse, the less code we have to write from scratch. And so code reuse is fantastic, right? Well, it depends because code reuse has tradeoffs associated with it. And everybody gets the first part of code reuse right and everybody misses the
second part. The first part is abstraction. Oh look, we could call this code from a bunch of different places and it would be useful. So we should abstract that and reuse it. But the second part to be useful, it also needs low because if I start reusing that letter notification service a lot within my architecture, it's going to cause churn in my architecture because every time that
thing changes, I have to stop and coordinate around that change. Now, it probably didn't break me, but I have to check because it's highly volatile. So things that are highly volatile are terrible reuse candidates. And so this is a useful tool to help you identify the things in your architecture that are in fact good reuse candidates and ones that are not because they have high volatility. It's
also a great way to split things apart because it's not behavioral. It's a characteristic of how we're building the code rather than the behavior of the code itself. So the pros here is that exactly that it's not behavioral. It's a meta characteristic of the code. And so it makes splitting behavioral things easier sometimes. And it helps you identify reuse targets because if it has good abstraction and
has low volatility, then it's a good reuse target. And of course volatility you learn from version control by querying version control and finding out how rapidly it changes. And so this will reduce churn. The downside of this one, it's hard to predict. This is part of the problem with volatility based decomposition is it relies on you choosing and and figuring out which things are going to be
highly volatile. Sometimes you can, but sometimes that's a little unpredictable. And it is a tool, not an entire lifestyle or a There's two of them. Talk about a third one. We call this one scalability and throughput. But this is really about this whole family of operational architecture characteristics. Scalability, throughput, responsiveness, performance, elasticity. All of those belong in the same sort of family here. So let's say we
have our notification service, text, email, and postal letter. and we get 220,000 texts a minute, 500 emails, and one postal letter. And we want to use the scalability and throughput disintegrator to break that thing apart. So the obvious way to break that apart might be break it into three pieces. SMS, email, But then you look at that and go, "Well, wait a minute. Email and letter are
basically rounding errors compared to text. That thing is massive." And the magnitude of email and letter are basically the same if you look at it compared to u SMS. So maybe we should bundle those two together, letter and email. Okay, great. What would you call it? Naming things is hard in software. Would I call it the nons SMS messages? It seems kind of weird to call it
for the thing it's not. There's no real great name for this. And so you'd probably leave it as two separate services. Even though from a scalability standpoint, you could bundle them together. But remember the whole strong and weak nuclear force thing. Just because you're using this disintegrator, you're not building a puzzle for your future self because this might be some sort of non electronic communication that's not
SMS service and you'll come six months later and go, "What is this thing?" and have to figure it out. And so clarity will probably win out over consolidation here and you'd still have three services just because naming things is tough. So the pros for this, this is one of the drivers for going to a distributed architecture. Uh some things you can fix with design or architecture, but
some things require architecture and scalability. One of those things that requires architecture past a certain level of scale. You can't get a monolith to scale. And so it requires architecture and this is one of the drivers for that. Um the downside of this, this might break atomic things. This is a technical capability, not a domain capability. And so if you start breaking things apart around scalability, you
might break bounded context or other things you'd like to keep together from a domain because we are now dealing with technical partitioning, not domain partitioning. Our fourth one is sort of a specialized case of those operational ones which is around fault tolerance, reliability, availability and that family of uh capabilities. So my notification service let's say that for whatever reason the email code is very flaky in that
service and it will frequently cause uh fatal exceptions that will take down the entire process. And of course, if it takes down a within that process, it's going to take down the entire thing, which makes my notification service poor from a fault tolerance standpoint. One of the ways I can improve that is by breaking into three separate services. I can now use DevOps techniques like circuit breakers
or retries or mechanisms like that. In case the email service does in fact get flaky and die, it doesn't affect the other two. And so this gives me better fault tolerance by way of distributed architecture and spreading things around a bit. This is another one of those drivers for distributed architecture because single monolithic things have poor fault tolerance. They break and then they're broken. Whereas um I
can build DevOps practices into distributed architecture like circuit breakers or retries etc. um and uh redundancy. This is a way to add resiliency to your architecture by protecting against fault tolerance. The downside to this is this might break atomic things because again this is not a domain characteristic. This is a technical characteristic around availability. And just like scalability, this is an example of technical partitioning, not domain
partitioning. And so this might rest uneasily with the partitioning that your domain would like you to have around bounded context and uh design concerns like that. So the fifth of our disintegrators is the other one that's fairly obvious which comes up a lot security and access restrictions. If I have some code where some of that code is more sensitive than the other because it's personalized private information
or financial information or something like that, then I might break that apart and harden one of those services more than the other one. U the pros here is that this gives you physical isolation for security, not just logical isolation. And you can build slightly more robust uh characteristics around physical isolation. And this turns security into a first class concern in your architecture. It's not just being handled
by governance, but by the structure of the architecture itself. The downsides here is it might break atomic things. In my customer example there before, I probably wanted all that stuff together in customer and now I've broken it apart because of security concerns. And so that's a um a uh an atomic thing that I might want to keep together. This is a classic crosscutting concern because uh we
need to take it this into account. It needs to cross cut the entire architecture but it normally does not fit nicely within our design language like domain driven design or similar design tools techniques. So those are the generic disintegrators. As I say, I encourage you to look for your own, which may include thing like uh corporate structure or geography or um behavioral and the way that you
partition work around teams, etc. and disintegrators you'll eventually sort of come to one at a time. But the ones that are tougher to come around to at least the second two of these are the integrators which is an indicator that I've gone too far in disintegrating stuff. I needed to bundle things back together. And I have three integrators I want to talk about. The first of these
is the one that you've all heard about before in terms of microservices which are database transactions. In general, you want to try to avoid doing transactions across service boundaries. If I have a profile service and a password service and I want this to be added to my system atomically, that means that I'm trying to do an acid transaction across service boundaries here and I cannot. I can
fake acid, but I can't actually achieve acid. uh and in fact the entire second half of my talk is exactly how to do that because it's basically 225 pages in the software architecture the hard parts book to be able to do that. So you can if you absolutely have to. But the better solution to this problem in many cases just don't break it apart. Keep it as
one thing. Consolidate it back into one service. That's where the transactional boundary is supposed to be in microservices is at a service boundary. And so if I've broken it down so far that I now need to do cross-ervice transactions, that may be a mistake and maybe I need to bundle them back together and look for another disintegrator. And this is a great example of that iterative design
that I was talking about in software architecture. Maybe I take the service functionality disintegrator and try to break these two apart. But then I realize, oh no, that's going to force me to do a transaction across service boundaries and I don't want to do that. So I'll use this to bundle them back together, but look for a different disintegrator. Can I disintegrate it in a different way?
And now I'm iterating on the design of my architecture uh to try to come up with the best version of this granularity trade-offs for the database transaction uh integrator. It preserves atomicity. It gets atomicity back to the level that you want to be in microservices which is at the service boundary which is really nice. Uh and it's much easier to model these as microservices because microservices want
the transactional boundary to be at the service boundary and of course this is doing that which makes it um friendlier for microservices. The downside of this is this may prevent you from disintegrating something that has gotten too large. That is a common problem in microservices because that transactional context may be quite large and therefore your service is quite large. But one of the challenges in this kind
of architecture and for the other downsides to this see about 225 pages in software architecture. hard part the entire second half of that book is about okay you've decided you really need to do transactions what do you need to take into account and I'll do a an overview of that here in just a few minutes so it is possible but uh discouraged the second by integrators is
around data dependencies and this is one of those cases where breaking apart code is relatively easy because we have great tools for this in terms of idees and the way our platforms and languages work but breaking apart data dependencies is often a lot more difficult. So for example, let's say I have a existing service that has three bits of functionality in it, A, B, and C. And
I've decided I really want to do disintegrator to break this down into But of course, the single service also has data dependencies in a relational database. So I use the service functionality disintegrator to break the three bits of code apart into separate services. But now what do I do with the data? I could do this, but that's worse than what I had before because now I have
some of the disadvantages of microservices and some of the disadvantages of the monolithic database, but the advantages of neither one of those. So, this is worse than what I started with. So, I probably don't want to do that. I probably want to break the data up just like I broke the services up. But here's the problem with that. Function A writes to 1, two, four, and six,
and reads from five. Function B writes to three and reads from two and five. And function C writes to five and reads from 1, two, and three. So if I take the general rule of thumb in microservices that whoever writes to the database is the owner of the database, I can distribute things like this and say okay well A now owns one two four and six, B
owns three and C owns five. That handles the right dependencies. But what about all these reads? These used to be views in the single relational database, readonly views of data. But now, how do I reconcile that? Well, I could just let A read directly from the table on C over there and let B read directly from the tables on one of A over there and then let
A or C read directly from those tables. This is officially the worst of the three that we've seen. This is now officially a distributed ball of mud. This has none of the advantages of microservices and all of the disadvantages of microservices. This is not where you want to be. This is often solved in modern microser architectures using things like readonly caches, which is a really clever way
to solve this problem. But the other way to solve That's this data dependency integrator. And in fact, a lot of ambitions in microser architectures uh roll up on the shores of this particular integrator. I'd really love to break those things apart, but I can't because the data will not allow me to do that. So that happens So the pros here probably much higher performance if you leave
it all in a single database. One of the downsides of microservices often is poor performance and it's partially because the data is so isolated and we're having to do so much through messaging and network latency is killing us in terms of performance and responsiveness. Moving it back to a single database will almost certainly improve the data performance for the architecture. And it allows you to go back
to traditional data modeling like views and store procedures and all those things that you're accustomed to. And you have a clear idea of who owns this data now because it's all in a single database. The cons of course is now I'm not isolated like microservices wants me to be isolated. I'm not building bounded context. I'm talking to a single larger database which is not the uh plan
in microservices and it returns us to the big ball of data which is probably what we were trying to get away from with microservices to begin One last integrator which is overly complex workflow and choreography and this actually touches on something that I just mentioned before. Here's the scenario. I have a service that has over time grown to be quite large and has too much functionality in
it. And I've decided I'm going to use the service disintegrate that thing into four parts. And this time, miraculously, the data broke apart nice and cleanly as well. So, we got super lucky in that case. The data came apart with no sticky leftovers But here's the problem. That code was doing a workflow before and just because I split that out into individual services doesn't mean that workflow
has gone away. But now that workflow has to take place with network calls instead of method and that's way way slower. For example, if I make a call over here in my monolithic service and it comes back in 1500 milliseconds, when I make the same call down here, it has to make a call, but now I have to call another service and consolidate and aggregate and call
another service and get it to call and return and etc. Noticeably slower responsiveness in many cases. This is the Achilles heel of microservices is responsiveness. Particularly if you've broken your services down too small. All of the performance of your architecture is taken up with network calls and overhead and latency in your architecture. But you've also introduced some other issues like reliability and data consistency now because what
happens if I'm trying to do this workflow and one of these services is temporarily down. So now I have to worry about data consistency and mechanisms that allow me to preserve that or govern it or check on it etc. And so a lot more complications in my architecture to be able to handle that. So the trade-offs for workflow and it gives you if you bundle things back
together into a single service it gives you a clear transactional scope. Again, this is that rocks versus sand metaphor that it's easier to put um rocks into a jar before you put sand into a jar. The bigger pieces fit uh more easily with each other. Uh the cons here is you have operational coupling uh when you bundle things back into a single service uh and you're building
little mini monoliths in your architecture because you can't break them apart into smaller pieces. So those are the integrators and that is the family of disintegrators and integrators. As I mentioned before, this is the generic set of capabilities. I strongly encourage you to adopt these but then start building your own toolkit of these things and start looking for your own disintegrators and integrators and give them useful
names because it gives you a way to talk about these things um and gives you a vocabulary. Okay, let's talk about the second half of our topic which is communication and in particular transactional sagas. So let's talk about the origin of the term saga. It did not start with uh saga was originally introduced back in the distributed data days when we needed to pass information across multiple
databases. Uh that was given the name saga and when we needed to do the same thing in microservices we adopted that same name as saga. Um and in fact in Chris Richardson's book on microser patterns he talked about two different flavors of sagas. one that used synchronous communication and the other that used asynchronous communication. But in our hard parts book, we wanted to say something more deeply
analytical about sagas than just oh there are two possibilities here uh synchronous versus asynchronous. And so we did what we always do when we're trying to understand a difficult problem in an architectural space like this. And we started building examples and then tweaking little parts of the examples. And we noticed an interesting phenomenon. If you're familiar with the branch of advanced mathematics or fractals and this concept
of sensitivity to initial conditions, fractals very often have a small change to an input parameter after millions of iterations create massive differences in outcomes. This is the butterfly effect of a butterfly flapping its wings here in Bangalore will create a hurricane in the US in a week and a half of the small little changes that occur over time. And we notice the same kind of phenomenon in
distributed architectures when for example you change from synchronous to asynchronous. All the other things change a lot more than it seems like they should. And we realize the reason for that is because when you talk about workflows in a distributed architecture, this is actually a three-dimensional conjoined space. And you can't just change one of these without impacting the other two as well. One of the interesting sort
of side effects of this insight. So the three dimensions here are communication synchronous or asynchronous, consistency which is either atomic or eventual and then coordination which is either orchestration or choreography. Sort of an interesting sidlight of this is if you look at monolithic systems that are not distributed architectures at all. They are orchestrated synchronous and atomic which means that if you graph them here they would be
exactly the origin 000. It's only when you start talking about that the little ball moves out into space, but then you can't move it freely because some of the the uh the axes are stickier than the other axes. So having figured this out, what we decided to do is say, okay, well, if you look at that diagram, basically what that's saying from a mathematical standpoint is I
have three different things that have two different possible binary outcomes for either end of their spectrum. Which means if you understand combutations and permutations, there are eight different possible combinations of those three things. And so Mark and I said, "You know what? Here's what we'll do. We'll model all eight of them, all eight different possible combinations of those three dimensions, and probably half of them or more
will fall away as being ridiculous. And that'll give us some insight into the other ones." And that didn't happen because it turns out all eight of them are either legitimate patterns or anti-atterns in distributed architectures. And so we gave them all names. They're all sort of whimsical names based on what kind of saga it is because they're all sagas. They're all doing transactional workflows in distributed architectures,
but they all have very different characteristics. And I'll talk through each one, show you the happy case and the unhappy case, where it's applicable, and where it should not be used in a distributed architecture. All of these are just three different combinations of these three what we call primal forces. Communication, consistency, and coordination. So let's look at the first of these. We call this the epic saga.
So the dotted line there is the scope of transactionality. So it's at the entire workflow level. And you can see the sliders up there. This is using synchronous communication, atomic consistency, and orchestrated coordination. So you see our orchestrator there. Let's see what happens in the epic saga. So in the happy case, I make a call to the orchestrator and it makes a synchronous blocking call to the
first domain service, updates it. synchronous blocking call to the second domain service updates it and then a synchronous blocking call to the third one updates it and then it returns to the customer congratulations your workflow is complete that's what the happy path looks like let's look and see what happens when an error occurs call my orchestrator updates the first domain service updates the second domain service but
when it tries to update the third domain service it cannot for it's not available, there's a data problem, whatever it is. Now, the orchestrator is going to go back and issue compensating updates to the previous two to put them back in the original state they were and then eventually return to the customer saying, "Sorry, we tried to do your workflow, we couldn't, but everything's back the way
it was." This is what's referred to as compensating updates in the microservices world. But there's a problem here. It is at this point every presentation on microservices you've ever seen waves a magic wand at this point then says and then a compensating update occurs and then everything is back the way it was But what happens if the compensating update fails? So, there are two error scenarios you
have to worry about here. One is I tried to do the workflow and I couldn't for some reason, but I put everything back the way it was. The second one is I tried to do the workflow, it failed, and I also failed to put things back the way they were. Compensating updates can also fail. And you have to take that into account. The reason I bring this
up now is that notice that because of the way this particular topology works when I hit this orchestrator, I know what the outcome was. I know whether I was successful in putting things back together or if I'm in an unknown data state. That's one of the key differences in these workflows is when do I know that and can react to it. That's the epic saga. Notice that
when I reach this point in this workflow where I have now updated the first two domain services, realized I have an error in the third one, but have not yet had a chance to do the compensating update. If another service queries this service around that change, they're not going to get that change. They're going to get a correct answer back and it's going to be delta minus
one. And so this workflow is protecting that change until it happens everywhere. That's part of faking atomicity at the workflow level. So this is a longunning heroic story which is why we called it an epic saga. It mimics a non-distributed transactional interaction but it is highly highly coupled and we will see the impact of that as we go along. uh easy to understand but difficult to implement
because getting all that compensating update workflow stuff can be tricky particularly if you've never done it before each of our workflows we create a grade what we did in the book is a much more extensive scorecard but we've just collapsed it to the ones that change the most for the purpose of this presentation how decoupled is this solution and this one is the most coupled one of
all of them so it is not decoupled. How simple is it to understand and implement? This is uh a little of both. How responsive is this architecture generally and how scalable? And it scores poorly on both of those. So let's look at so why would you use this? This is often the first attempt by teams who don't understand a lot of the the forces at play here.
The legitimate use for this are environments where every step has to be completed before the next step starts. So if the input of the B service is the output of the A service, I have to wait until A is finished before I can call B or where absolute transactionality is much more important than responsiveness because that's the big Achilles heel here is responsiveness but it gives me
absolute transactionality. So things like wizard style uh user interfaces for example. So let's change something. Let's change Notice dotted lines here for communication, but leave everything else the same. On the happy path, now I can do multiccast and do all those in parallel. So, this is going to be a lot faster than the epic saga because all this can happen in parallel for the happy path. for
the unhappy bath. I can also do this in parallel, but notice that one of these has an error. The orchestrator again can go and once it's gotten the answers back, go back and do compensating updates, but do this in parallel as well and then return the possible error state back to our customer. We have the exact same issue here and we have the exact same advantage here
because this is orchestrated. we know exactly what the outcome of this is going to be. It just comes back a lot faster because of um we can do this in parallel rather than synchronously. We call this a fantasy fiction. It's a complex story that's hard to believe in the end because as soon as you go to multi-threaded, it's hard to consolidate information at the end because now
all those threads of execution are building their own kind of thing. Moving to asynchronous will improve performance and responsiveness, but now you have concurrency monsters that come live in your architecture, and that may be a worse problem than the performance problem you're trying to fix. Notice the star charts have gotten a little better here, a little better in coupling, less in simplicity because of the multi-threaded stuff.
This is often the first attempt at fixing an epic saga because it was too slow. You get noticeably better responsiveness by going to async, but not a good fit if order of operations is really important. This is the first sort of non-intuitive result of this analysis that we did because everybody knows that the way you improve performance in microservices go to async, right? That's how you get
better responsiveness. And it did make it a little better, but not a lot. It turns out it's the combination of communication and atomicity that's chilling our performance here, not just communication. And so you can't just make communication better by tweaking one thing. You have to tweak more than one thing. But we'll see more examples like that in a second. Let's change something else. So now I'm going
to move communication back to synchronous but move to eventual consistency. So notice my scope of transactionality has now collapsed to the individual domain services not the entire workflow still orchestration. Let's see what happens now. So now I'm in the happy path. Call the first domain service. Call the second one. Call the third one. From this standpoint this is no different from the In the happy path is
these two basically work the same way. Here's the first divergence. Then the unhappy path. second domain service. When I call the third one and I realize I'm in an error state, I have the option if I want to to return the error state now and then do the compensating update in the background. Why can I safely do this here? Well, the mandate here is eventual consistency. I'm
not trying to impose consistency at the entire workflow level. So, it's okay if those two domain services are out of sync with the rest of them temporarily because the only requirement for eventual consistency is at the service level, not the entire workflow level. Now, the hazard of doing this quick return is the exact hazard I talked about before. What if the compensating update fails? So, there has
to be two different ways to return. If you're taking advantage of this early return, there has to be an an emergency path to say, "Oh, and the compensating update failed." But this gives you significantly better because I'm not waiting for all that background stuff to have happened. I can now make the entire workflow noticeably more responsive by handling that stuff uh in the background. if I reach
the point where I'm in an error state but not yet done the compensating update and somebody else queries that domain service, they're going to get delta back because eventual consistency says that I'm only consistent at the service level, not the entire workload. So fairy tale is an easy story with a pleasant ending. And in fact, this is the easiest combination of these things to reason about. Synchronous
is easier to reason about than asynchronous. Eventual consistency is easier to reason about than atomic. And orchestration is easier to think about than choreography. So that makes this a very attractive option. Uh you'll notice that it has really high scores on simplicity and scalability and better and better scores on responsiveness. This is well suited to most medium or complex workflows that don't require extreme scalability or The
orchestrator makes it harder to scale, but it's also fantastic for complex workflows. And so that's where the sweet spot for this one is. I mentioned Chris Richardson's book. This is the orchestrated version in his book. He did two workflows, an orchestrated one and a choreographed one. This was his orchestrated one was what we call a fairy tale. If one of these is the default, it's probably this
one because it has the easiest combination of all these things up here and really good uh scores in terms of uh the star ratings down there. So, if there is a default, it is probably this one. So, one more orchestrated one. We sort of organize these with all the orchestrated ones together and then all the choreographed ones together sort of arbitrarily. And so here's the last orchestrated
one parallel saga which is asynchronous and eventual consistency. So everything here can operate at multiccast speeds. Everything can happen in parallel. And I have fewer restrictions on uh atomistity here. And so I can uh for the unhappy path I can do the same early return if I want to. That's never mandatory but I have the option in this architecture if I want to do that early return
when I know there's an error state and then go and fix up the uh the problems in the background. So multiple stories running at the same time. This is aggressively asynchronous. Uh the orchestrator still allows you to have complex workflows here. But then all the asynchronous gives you good u and so you'll notice here really good responsiveness and scalability particularly for an orchestrated workflow. Simplicity takes a
big hit there because of all the concurrency and multi-threaded business in this architecture. This is a really good option for complex workflows at high Difficult of ordering of updates is important because of the asynchronous. Now you got to have joins or semaphors or mutxes or something to join that information back together at the end. Two of the three things that drive coupling up are minimized here. So
it has the second best decoupling uh of any of our uh patterns and the complexity here is due to concurrency because going asynchronous gives you concurrency monsters. So now let's switch to choreography. We've been talking about orchestration up until now. Let's talk about choreography and get rid of the orchestrator and see what effect that has. And so that last slider is now going to be permanently pushed
to choreography. It'll pull the other two sliders back to synchronous communication and atomic consistency and create what we call the phone tag saga. Phone tag. Whoop. I went one too far there. Let me go back to my happy path. Here's the happy path for phone tag. I got no options here. There's no orchestrator. It's all choreography. So, the first one has to call the second one, which
has to call the third one. And then the responses have to go right back up the chain that it came before. I have zero options here because my uh the architectural pattern here gives me no options. And exactly the same is true here. The first one has to call the second one because these are synchronous blocking calls. And so, I'm calling and blocking, calling and blocking, calling
and blocking until it uh replies back along the same path that it took. This is sort of like the kids game of phone tag where you whisper a story in one ear and it travels around the room. Uh it is the epic saga without This is perhaps a way to add more scalability to a workflow when the orchestrator has become a bottleneck. But this is not common.
In fact, this is the least common of all of them. We're expecting more sort of odd u characters like this, but this is only the really super odd one that we found. Um you can see why this is not super common. It does not score great on any of these criteria down here. Um and it's a weird combination to have synchronous communication and choreography because if you're
going to choreography, you're almost always going to asynchronous communication. And so this is a a bit of a weird case. There are some use cases for this though. If you have something that is absolutely um needs to call in order, then the uh the synchronous here is a feature, not a bug. Uh but then choreography would be very strange. One of these has to be objectively the
worst one, right? And here it is, the horror story. asynchronous communication, atomic consistency, and choreography. So, the happy path for this thing, a million options because I could call from one to the other sequentially like this because it's all asynchronous. I've got options and complete the circuit like this and then return from the first one or I could send as part of the request who to respond
to and the last one in the chain could send the response back versus going back to the first one to send the response back. That's an option and of course because it's asynchronous I could do multiccast and any variation of multiccast that I want here. So huge number of options in the way that I end up implementing this which adds to the complexity. Same is true in
the unhappy path. I have a million different ways I can implement this in terms of synchronous and asynchronous communication etc. Multiccast all the different options in the world here which is another thing that adds to it complexity. This is an example of multiccast for the uh the compensating updates certainly a possibility here. But what makes this thing so bad? Well, here's the problem. Let's say that a
request comes into this architecture and it is for let's say a green request but everything is operating asynchronously now and that service has to hold on to the previous value of green in case I need to do a transactional roll back because the whole workflow is atomic but everything's operating asynchronously and so now in the meantime a brown request comes in and then a blue request comes
in so I'm holding on to previous values for all of those and then Another request comes in for green, but it can't execute until the first screen request is executed and it's still in flight within my services. And it is at this point that your head will explode. All the different ways that this can break, all the variations for this make this a nightmare. This is the
job security architecture pattern because if you implement this, nobody else in your company will ever get anywhere near it and it will become your job for the rest of your career babying this thing. And it's great because you come in on Monday and it's broken in a brand new way that you've never seen before. And you can spend Monday and Tuesday figuring out how it broke this
time and fix it by Wednesday. So you have a couple of days off until Monday, the next Monday it's broken in a brand new way you've never seen before. and just rinse and repeat for the rest of your career. This is very much an antiattern. It's trying to achieve atomic workflows without a coordinator and concurrency layered on top of this. Now, unless you think this is not
common, uh, literally within two months of Mark and I coming up with this taxonomy, the project that I was doing advisory work on, the architects brought me a workflow and it was the horror story. We did not implement that, but this allowed us to identify, oh, this is going to be bad. We need to figure out a way to avoid doing that. This is likely a well-intentioned
but flawed attempt to achieve high performance and atomistity at the same time and it is unfortunately not uncommon because the the way we got there in the case I was talking about it was high volume credit card transaction processing. We needed choreography and asynchronous for the scale but the business said it needed to be transactional. Congratulations, you've landed in a horror story. We did not implement that.
We ended up using actually the uh transaction disintegrator and bundle it back into a single service uh to solve that problem. And the star charts over here are terrifying. The last two choreographs are perfectly legitimate. Time travel, which uses synchronous communication but eventual consistency. So this is sort of like the phone tag but with eventual consistency. Again, because I'm doing all synchronous calls, I got no options
here. I have to call from the first one to the second one and I have to call back in the same chain as we saw before. Just like the phone tag we saw earlier. Same is true for the unhappy path. It has to follow the same chain of events because I'm doing all synchronous calls. That's the only option I have is synchronous blocking to each and the
other and then follow this back up to do the compensating updates that we saw before. The only option I have now is to pass who to reply to as part of the request and have the last service do that. But that's a minor optimization at best. This is a problem moves atomically through time. Uh there's no orchestrator here. So complex workflows are tricky. This is mostly the
synchronous chain of event kind of problems because the synchronous now is makes it creates a staged kind of a workflow. So stage workflows fit into this kind of workflow. So document transformation or pipeline problems end up using this particular pattern and it has a lot of fairly attractive features um because where the sliders are. The last one what I call the anthology saga. All the sliders pull
all the way to the right. This gives you multiccast capabilities of course in any of the two cases the happy path and the unhappy case. And I've got every option possible here. Uh I don't have the horror story problem of trying to make the entire workflow atomic. And so I can use a visual consistency here which gives me a lot of flexibility in terms of error handling
and uh other workflow parts of this architecture. This is a loosely associated group of short stories like an anthology of stories. This is Richardson's choreograph saga. So our fairy tale was his orchestrated one. uh anthology is his choreographed one. So that's how ours relate to his. This is the complete opposite of the uh the um Really good for non-transactional pipes and filters styles and really really scalable
due to a lack of coupling. In the couple of minutes I have left, I want to highlight something that Mark and I have used throughout this book and are actually using in our upcoming book on software architecture patterns, antiatterns and pitfalls. And it's this idea of qualitative analysis or analyzing architectures. Let's say your boss has come to you and said, "I need a new accounting workflow." and
you can come back to your boss and say, "Boss, I have good news and I have bad news." The good news, I'm going to give you the objectively best possible accounting workflow. The bad news is it's going to take me eight times longer to build it than you have budgeted for and eight times the budget because I'm going to build it eight times. Take the seven that
are not as good as the eighth one and throw them away and we'll keep the best one. Nobody has time to do that. That's where this idea of qualitative analysis comes in. That's where these star charts come from. I don't know exactly how much more scalable one of these than the other, but I know what creates scalable systems. And I can use this to say, well, I
know this one will be more scalable than that other one. So, for example, if I decide that responsiveness is my number one criteria for this architecture, I can look at this and go, well, two of these have really good responsiveness. What's the real difference? One is orchestrated, one is choreographed. Okay, how complex are my error conditions and uh boundary conditions. Do I need an orchestrator or can
I get away with choreography? This is a way to narrow problem spaces down so that you can make decisions without having to build everything from the ground up. That's this idea of qualitative analysis in a software architecture. And it's a way to uh come up with decisions without having to build everything from scratch uh every time you build something. We did this in our fundamentals book uh
comparing architecture styles. We do it here for uh these saga summaries and we're also doing this for patterns in our upcoming patterns and anti patterns book. So that wraps up uh and looks like I'm out of time. So thanks very much for coming. I hope you enjoyed it and I hope you found something useful you can take back to your office. Thanks. [music]
More from this event
See all 126 talks →
AI Is Not the Risk. Architectural Drift Is - Sunil Kalkunte
17:39
Breaking the Monolith: Tesco’s Journey to Federated GraphQL with xAPI - Vishwas Chandrashekar
29:13
A Practical Introduction to LangChain4j - Venkat Subramaniam
1:01:28
Beyond the AI Models: How Lowe’s is Building the Store That Knows - Swaroop Shivaram
13:59