Modules, Monoliths, and Microservices
tailscale.com
tailscale.com
In theory microservices are technical artifacts, but what they tend to be are cultural artifacts.
Microservice adoption is often driven by cargo culting, or (better) a considered decision to work around a lack of organisational cohesion.
What microservices let you do is ship your org chart directly, and also map back from some functionality to an owning team. You know who to page when there's a problem and it's easier to tell who is late delivering and whose code is slow. In cultures with "lax" technical leadership (aka no everyone uses same thing mandate, I'm not judging) it lets teams use their favourite tools, for better or worse.
Other org related things are the ability to have independent (per team) release schedules. Separable billing. Ability to get metrics on performance, cost and failures that can be used as indicators of team performance and promotion material. Microservices can also act as "firewalls", limiting the impact a bad hire or team can have across your codebase.
None of this is intended to be negative judgement; microservices can (among other things), help teams feel a sense of agency and ownership that can be hard to maintain as org size scales up.
NB: one may also just call them “services” with no practical loss of generality and sounding less like technobabble to everyone else.
Problem is that SOA has been around for decades whereas “microservices” sounds hip and cutting-edge
Maybe 8 years ago it did.
This is completely untrue, and I think it's part of the problem with discussions about microservices. People think it means "lots of small services". That is not what it means. That would be closer to SOA. Microservices is not SOA, and it is not "technobabble".
It has defined constructs, methodologies, and patterns.
This is not true at all. Microservices are a Service-oriented Architecture:
From section 2.3 in "Microservices: yesterday, today, and tomorrow"
" Microservices are the second iteration on the concept of SOA and SOC."
https://arxiv.org/pdf/1606.04036v1.pdf
There's probably other links I could dig up to prove the point, but I think that one's good enough.
I see it all the time. It's simply wrong to say that microservices is just "services". Many people being wrong changes nothing.
The earliest things on microservices seemed to be identical to early SOA stripped of lore, usually of specific-context origin, it had accumulated in a decontextualized way, and which was often, especially once decontextualized, not particularly in-line with the original principles.
It's now gone on and attracted a whole bunch more of its own lore the same way, arising out of somewhat different contexts.
I feel like you could just google SOA and Microservice architecture, pick up a book, and answer that for yourself.
Your link:
> Sam Newman provides a succinct definition of microservices in Building Microservices: “Microservices are small, autonomous services that work together.”
I even see the reverse happening, org chart changes made based on service boundaries, due to this friction.
Microservices or not, above a certain headcount there's no escaping this reality, there's an overhead due to communication, organization, etc. As long as the throughput is greater this way (than without organization, or without the increased headcount) it makes sense to do it.
I don't believe that's true in practice.
Having many services moves a lot of complexity from 1 program to "the system" as a whole. All the stuff outside the services themselves.
Who do you call when the incoming requests are failing, but each service is working fine in isolation?
So you get adventurous and open up XRay or Application Insights or whatever the fuck. After a few hours you realize that the erroneous response code that you thought was the culprit is there by "by design" "intentionally".
So you say fuck it and add another microservice or cache layer that "fixes" the problem and call it a day. You can hear screams of agony from the infrastructure team, but who cares.
When you leave for the day, the boss pats you on the shoulder. "Nice work today, handling that incident", he says.
Very few people, if any, have a complete understanding of the system and this is often by design which may not necessarily be a good thing. Reasoning between microservices feels harder to me than reasoning within a monolith.
The overhead of network calls and its attendant problems can be a worthwhile price to pay if you need to loosely couple your teams.
This is why my yardstick of "should it be a micro service?" is "could it work as an entirely separate business?"
I was a massive skeptic of micro services until I saw that it actually solved some people problems pretty well despite incurring a technical cost.
I'm pretty convinced it's an anti pattern in small startups though.
They do often follow Conway's law, but that's not a desirable thing. Most modules are outside of that kind of context -- they're in libraries written by people you don't communicate with at all!
I'm interested in people's opinion here. I'm also planning an article on the relationship between services and modularity.
You are restricting the scope of what we call a “module” to language runtime modules while the OP has expanded the scope to include things like network tiers and processes with an IPC interface.
I found the OP's perspective both new and useful.
This is fine to talk about, of course. It's just confusing to reconcile the article with my definition of modules, which I thought to be the common one.
You just don't group multiple modules into a single library.
Well you kind of can with sub-modules, but at the end of the day the binary library is generated exactly the same way.
Software threats arise from the fact that coders don't in fact fully understand everything the software is doing, how it makes it impossible or possible for hackers to gain unauthorized access.
If we understood the software perfectly we could easily and quickly remove vulnerabilities from it. And if we understand its limitations we wouldn't let it run a commercial airline on auto-pilot.
I prefer to think of security as the opposite of functionality: increasing functionality makes more things possible, increasing security makes fewer things possible.
I like this view because it forces us to acknowledge the tradeoff: the most functionality we can provide is a root shell with no passwords; the most security we can provide is an inert brick; we need to be specific about what functionality should be provided, what shouldn't, and design a system which sits between the two.
From this perspective, modularity can increase security by preventing one module from accessing/executing unexported parts of another module. Yet this implies that modularity also reduces functionality, for exactly the same reason. Again, we need to specify what should be exported, what shouldn't, and implement something between the two.
Conway's law might as well be, like democracy, the best of all the bad worlds.
Importantly, this is completely independent of team structure. The boundaries between teams are not necessarily the minimal interface.
No, but they are the inevitable interface.
Is following the second law of thermodynamics a bad thing? I say so jokingly because thermo is a little more rigid, but I still think it's futile to fight Conway. After all, if you change your org structure to try to beat it, you're actually proving it.
It's not something to beat as much as it's something to inform decisions. Code should be architected with knowledge of how the organization is broken up, and orgs should be structured with knowledge of how that affects the technical side.
I guess all this is to agree with you and say...if breaking up modules in a certain way for small teams is effective for your organization, then it makes sense to do it.
This is why I say Conway's law is a bad thing for modules (regardless of how inevitable it is): technical considerations are often not taken into account, and technical design is ossified by team structure.
Microservices and the like are not about isolating code, are about isolating responsabilities within code first.
The concerns expressed are valid, but the premise they move from are weird. Privilege escalation is a problem both in monoliths and micro services. People figuring out encryption keys from timing attacks is about the same threat both for micro services and monoliths.
also, for such a long article, I find it weird that not once the session management, authorization, and relation integrity topics are mentioned, while schema versioning gets touched but not interface versioning, as if the suggestion is to make modules talk to each other by touching each other data.
A. Privilege isolation is the hardest to refute benefit of microservices in theory, but you have to keep in mind that we need hardware virtualization to achieve a reasonably strong form of that (i.e. your k8s cluster isn't helping you out here, modulo Fargate perhaps)
B. This is rarely the primary concern when we look to microservices (or generally, modularity) to cure what ails us.
C. If we have less concern about trust boundaries, there are easier ways of modularizing code that probably solve the things we're really worried about (i.e. libraries or any other way of splitting code that is built into a single artifact).
> at this point security people simply don't believe that any of the following (each one the very best technology available at the time) is totally safe
To be fair, we don't think anything is totally safe :) that's the point. Actually a lot of those things listed are why Chrome is so secure. I think this is a bit pessimistic about process isolation, which is actually quite powerful, but I get the point - there's no gap like an air gap.
I prefer to think of monoliths and microservices as being completely unrelated ideas - they aren't on a spectrum of "big to small", they're just two totally different ways of approaching software development. I might put "monoliths" closer to SOA, in that they both describe a very basic approach to a problem, whereas Microservices is not "small services" it's about bounded contexts, patterns, organizations, etc.
Anyways, I appreciated this post.
[0] https://www.graplsecurity.com/post/architecting-for-performa...
I personally like microservices a lot because they allow you to do one more thing (a variation on the isolation theme, sure), to limit the scope of what you, the programmer have to deal with at any given time. Your specifications, tests and mental activity all focus on a much smaller chunk of code, which is much easier to deal with.
I don't think I've ever built a bug-free monolith, but I'm fairly sure I've been able to repeatedly produce bug-free (or nearly bug free, you can never be 100% sure) microservices. And that to me is the bigger advantage, the scope limitation from the programmers perspective.
Good software architecture is hard.
Message orientation, in isolation for each message handler, is wonderful, easy to test, decoupled etc. But the overall flow is harder to understand and coupling at the business level still exists. You still need to remove stock from inventory after a sale even if the two parts don't communicate directly. Messages are COMEFROM.
The other thing is reliability. Monoliths can usually rely on the ambient availability of resources - a synchronous in-process call will have a CPU available because that's how in-process calls work - but services across the network might not have resources to respond for some time. It cuts the other way too, though; when the monolith is down, everything is down. There's tradeoffs everywhere.
[1] https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers...
[2] https://medium.com/dartlang/dart-asynchronous-programming-is...
[3] https://patterns.ponylang.io/data-sharing.html
[4] https://doc.rust-lang.org/book/ch16-02-message-passing.html
I think that's only true for very specific kinds of architecture which are only tangentially related to coroutines: stateless data transformations or actor-inspired architectures. Things like shared atomic data structures or mutexes translate pretty poorly to microservice architectures (they're doable, but really painful, bug-prone, and you give up a ton of performance).
EDIT: Hmmmm... on reflection, I suppose if you think of the actual stateful set of yield and continuation operations of coroutines they are one particular possible microservice architecture, but it strikes me as a bit messy? Although I guess you can retrofit the usual set of other concurrency operators on top.
Basically the crucial point being getting concurrency without the need for actual parallelism?
Seriously, I've seen a monolith broken up into a microservice using Redis as shared memory. Didn't know it was an anti-pattern.
You really just have to be clear whether you are building a distributed monolith vs a set of microservices. In the former case, you are optimizing entirely for performance and scalability concerns. It just so happens if you have things that require scaling of the underlying hardware, our current state-of-the-art tooling handles scaling hardware resources (number of cores and memory) better when you have separate services rather than bundling it all in a single program.
However, you get none of the operational/human-level process changes of microservices. Teams are still coupled to the changes in the shared state. Anything that changes the shared state cannot be deployed independently. In fact from a process perspective things are probably even harder to coordinate now since the coupling is still there, but not explicitly expressed in code anymore. And from a site reliability perspective, you still have a single point of failure: the shared memory. Any mitigation strategies you use for that you could've used for a monolith as well.
If all interactions are synchronous, you're often adding complexity at the IPC layer, and need more boilerplate on either side since you need the presumption that the service you depend on may not exist.
Even so, if the services are fundamentally different in their scaling needs, security demands, availability, etc splitting services is a consideration.
Maybe, over time, the ideas behind k8s and Service Meshes will start to close this gap of complexity? Or is it a language problem that will create the solution, ala Erlang?
How does one manage authentication/authorization in a distributed system? Is that on the service mesh also?
Complexity is a bitch!
I'd say simply having different scaling needs may not suffice to isolate services, especially if all the other parameters I mentioned stay the same. Even so, some cases for needing to isolate purely based on scaling - in my experience - have been when the scaling requirements are drastically different, with completely different geographical needs, scale ceilings, etc.
We try to keep authorization within the system itself, with additional hardening on the service mesh/container/vm/network level as needed. I don't know that this is the right way to do it, but it's worked for us thus far.
Biggest problem with isolating services is that it reduces predictable errors by giving you smaller chunks you can reason about, but raises unpredictable, unreproducible (aren't those a bitch) errors down the line because the overall complexity has grown.
We have still some monoliths, like some EARs in some Jboss on-premise but the cloud+kubernetes+microservices trend seems the obvious way to go for everybody. Those monoliths are minimally maintained, new features go to microservices, and the end goal is to throw them out, it take years though.
It is very likely than microservices are way too small IMO, I've seen microservices just doing an IF, just doing an SQL select, or just adding some credentials in a HTTP header, stuff that could work in one service are sometimes splitted in tens of microservices.
Althought these microservices are Java/Spring Boot or Node/Express ones most of the times, despite the fact that you could potentially use the best tech for each microservice, managers want to maintain a limited tech stack where they can easily find cheap developers, basically Java and Javascript, some Python or Go maybe but not so much.
What strikes me the most is that it seems to work, till now at least, each developer have to maintain like between 3 and 10 microservices, but very simple ones.
Merging two microservices into one never happens.
Writing a new microservice is almost more frequent than evolving an existing one.
Is it a worldwide trend or am I living in a particularly excessive bubble?
I don’t understand what he means here - a website where every page is a separate CGI script and state is managed by the client is the very prototype of microservices.
A microservices approach would separate the state related to logins from the state related to other features. So there would be a login services, that accepts usernames and passwords from end users, and provides internal services like "given a session cookie, tell me if it's valid and what the user's name is" or "store this key/value to the session" or "read this key/value from the session".
Isolating concerns from each other is common in both monoliths and microservices. It's just how people tend to design ANY computer program or distributed system.
Obviously it's hard to draw the line between "function that does this" and "service that does this", and it's possible to have microservices that you call over an RPC that just reads/writes the same database the caller uses. It's messy and indistinct, and is probably too general a word to attach a ton of meaning to.
(My followup is to read deeply into critiques of the patterns. The HN commenter that says "microservices suck" will be perfectly fine that Apache sends a WSGI request to a Python app, which speaks TCP to a database. Those, they say, are services, not microservices, so their rage does not apply in that case. But... it's a pretty arbitrary line, so you have to read and understand carefully to identify the problems they have and what advice of theirs to take.)
Lambda helps but is limited. Dealing with existing code is hard. If this is a problem that keeps you awake at night, I want to talk with you. I want to figure out what can be done to remove single points of failure/compromise in production software systems.
Email is free at refinery dot io
My current take on this. The "business logic" should not be written in Turing complete languages, but rather composable languages based on simple type theory. So the "business logic" would be limited in what it can do to compromise the system. And then it should be deployed on the execution platform, which is a different concern altogether.
I think using TC (like Java or Javascript) languages for everything is a wrong direction in SW engineering. I think we want to have TC languages for the low level tools (like parsing data, algorithms), and for the execution engine, but we want to compose these in languages that can easily compose, and thus are restricted.
IMHO there are at least two different categories of compositional restricted languages. One is data manipulation languages, that describes how to manipulate data in the schema, regardless of the platform or physical data representation (I really like what David Spivak is doing in this area with categorical databases). The other is system architecture languages, which shows how are these data manipulations executed, and how they compose into transactions, and more broadly, software systems. The former category concerns with how the schema of the data changes, and the latter category concerns with how are the data in the same schema exchanged at the boundaries of different SW systems. (There are probably also useful categories of restricted languages to describe UI and visualizations.)
"Workflows" have been an area of focus for a product we've been building for a while now. A big part of that goal is to remove the "Turing completeness" from the system and keep each "block" of a workflow as a simple blackboxed data transformation. And "conditional transitions" are used to emulate business logic.
It's a bit clunky though and, after reading your text here, it makes me want to sit down and think for a few hours. Thank, internet stranger, you have made a material difference in my day over here! It's a nice feeling to hop out of bed early and excited :)
I think this is an excellent point. I've been coming to this same conclusion for some time.
For a lot of use cases a less powerful but more predictable language is probably what we need. State machines and DSLs go some way towards this but I don't think that's the full solution either.
Maybe something based on a restricted APL language, either way it sounds like a good PHD project to me.
Why do you think DSLs are not the full solution? My main thought was that having DSLs for specific areas of programming domains is the way to go. But they should have interop with Turing complete languages to implement specific sub-parts. That way DSLs can capture a lot of knowledge and best practices that we learned through years in that domain while still facilitating innovation being flexible. Such language would be very limited, more like "smart configuration languages". Some of them I have seen around are Terraform, Nextflow, Meson-build.
APL still sounds too general and self-sufficient to me to be a solution to this (but I don't know much about it so maybe I am wrong, I just checked it out on the wikipedia).
Microservices solve many problems and cause others.
People pointing out that they aren't a silver bullet are preaching to the converted. Anyone who honestly believes that anything in engineering is always better than something else haven't been listening in class!
It was a very good read and I'd encourage you to grind through it all (and not skim ;) ).
This might sound a bit mean but the author hear lacks clarity in thinking and therefore really doesn't know what he's talking about.
We 100% know how to isolate code. In fact the primitive that guarantees this isolation is part of the name of this site: combinator. Isolated logic is literally the definition of a combinator.
If every line of your code was written as a combinator then all your code would be reconfigurable and modular to the maximum amount theoretically possible.
The hard lines drawn with physical hardware are largely irrelevant and only obscure the main point. It's not relevant whether your code is isolated by a combinator or hardware acting like a combinator. There is no theoretical difference between using a virtual folder to group things or using a computer to group things.
The main issue with 'isolation' then comes down to IO, because all IO related primitives cannot be part of a combinator by definition and IO is a required part of computation. (by IO I mean input/output and shared mutable memory access)
The article doesn't even mention this. The main problem with modularity is how to deal with it in the face of IO. Hence he's not clear about what he's talking about.
IO breaks modularity & isolation and so does the use of anything that is not a combinator.
In Scala, for example, when using functional programming, even IO is represented with an object (take ZIO for example, or Cats effect), does this IO still breaks isolation and modularity?
I have seen code that looks extremely modular, reconfigurable, and isolated (exactly like you mentioned) but even IO (lazy/async) didn't make it bad, but again it is encapsulated and not actually directly dealing with IO and it only materializes at absolute end of the computation.
Yeah. It does. Think about it. if I have a function called:
getNumberFromMutableMemory :: void -> IO[Int]
The value of the int is reliant on an external dependency. Whatever IO represents, whether it'd be user input or a database is an external dependency and by definition NOT isolated and NOT modular because the IO function is coupled with the external dependency.Additionally if the IO is mutable memory the value of the function is also dependent on every single other thing that modified that piece of memory.
The IO types form another type of isolation that is more meta. These types serve to segregate your code away from modular code that basically has NO external dependencies namely the above code cannot be directly composed with a function of this type:
addOne :: int -> int
This kind of thing is cleverly done by making it impossible to construct anything of type IO[T] without doing a system IO call. You will find that because of this even if all your functions are typed with IO, you can't really compose them together or even access the internal value of IO without using the bind operator to segregate IO from the world of pure calculations.>I have seen code that looks extremely modular, reconfigurable, and isolated (exactly like you mentioned) but even IO (lazy/async) didn't make it bad, but again it is encapsulated and not actually directly dealing with IO and it only materializes at absolute end of the computation.
Code being bad or good is a matter of opinion. I'm not saying IO makes code bad, it makes it less modular (and IO is also a required part of all apps, so all apps require a degree of broken modularity). Categorically, anything that touches IO or mutable memory is less modular simply because it has an external dependency on IO. Anything with a dependency is not modular by definition.
What you'll find is that there tends to be a type of IO that can maintain purity of your logic and that is if the IO only goes in one direction: outward. If all you're doing is printing statements to the console or writing but never reading from external memory, you don't even need an IO type to segregate your pure logic, everything will work transparently. Let's say I have a function called print that prints a value but also returns the same value given to it. I can use it without an IO type and it won't effect any of my pure logic:
print :: Int -> Int
addOnePrintThenAddOne = addOne . print . addOne
It's only when you start reading from memory that code becomes less composable.One key to modularity is to see how composable your logic is..
For example:
addTwo = addOne . addOne
The above is literally the definition of modular code. I take two pieces of logic and put them together to form new logic.Good luck trying to do the above with IO functions that write and read from IO or mutable memory.