Or am I missing something?
Or am I missing something?
In my experience, with microservices, if you don't put much effort into following best practices, you end with components coupled at the interface level at worst, but you can still deploy independently and don't have to worry about service B subscribing to some internal event in A or that kind of things.
In a monolith, for the same amount of effort, after some time you usually naturally end up with all the kinds of coupling mentioned here, which is very hard to then get out of.
A monolithic system's architect would have to make an explicit choice at some point, to reject/restrict coupling (by e.g. building the monolith on an actor-model language/framework, where components then become "location neutral" between being in-process vs. out-of-process, making the "monolithicness" of the system just a deployment choice — choosing to deploy all the components as one runtime process — rather than a development paradigm.)
Service-oriented code, on the other hand, has low coupling by default, until coupling is introduced via explicit choices like making synchronous RPC calls between components.
Sadly, people refactoring a monolith into SOA code will often introduce such coupling, as it's seemingly the cleanest 1:1 "mapping" of the monolith's code into SOA land. But the point of refactoring into SOA isn't to do a 1:1 semantic recreation of your monolith, including its inter-component dependencies; by doing so, you just produce a "distributed monolith emulator." A proper SOA refactoring should carry over the semantics of the individual components, but not the semantics of their interdependencies.
This is because coupling is a design instead of an implementation decision. "Make a synchronous RPC call between components" is still the "easy" and "obvious" way to get things done.
It's sometimes easier to spot high coupling when looking at code in a microservice world, but it still requires you to know what you're looking for and know how to avoid it, and not give in to the temptation.
Last time I was working with people trying to split up a monolith I pleaded with them to try to understand the coupling first, and figure out what a system would look like without that coupling before actually just moving all the code around and replacing direct method calls with ... other method calls that made synchronous RPC network calls.
No luck.
Error rate went up.
It all depends on relative friction. Systems designed from the ground up to "think service-oriented" will use languages/runtimes/frameworks that make low-coupling options easier and more idiomatic than synchronous RPC calls.
For example, if your SOA is built on CQRS/ES, then adding an Event to the event store, for another Command to react to, should be easier than making an RPC call.
You can carve out A as a microservice with a clean general interface, and write an adapter layer to translate calls from your old messy interface to your new one, and B, C, D call that. Then start refactoring B, C, D one by one depending on your priorities.
That way new service X that needs to use A, can directly start using the clean general API even if B, C, D are not refactored yet.
The more everything in the pipeline know about each other, the easiest is to code it and the FASTEST will be.
You can say any performance gains is possible when you exploit this facts. And the more "abstract/invisible" everything is the more impossible is to make it so.
I had a misfortune to work on a project where this exact thinking was used to carve up relatively simple system into about a dozen microservices. Turns out referential integrity and transactions are really, really useful. For example, most task required calls to multiple microservices and if something broke in one of the microservices involved it would often leave other microservices in inconsistent state and retrying failed task became almost impossible. Project was scrapped after 3 years of development.
Sure you may be able to call it "looser" coupling, but you may also be able to call it a rats nest of complexity!
If you break out a separate service from your monolith and it’s not independently useful to things other than your app then you probably should put it back.
We need a definition of what it means for two services to communicate while being “uncoupled”. I think “are versioned independently” or “don’t have to be upgraded in parallel” meets the bar.
Yes... exactly. More things are monoliths than we want to admit.
> We need a definition of what it means for two services to communicate while being “uncoupled”
Take a dead simple example: an application server that talks to an in-house video encoding micro-service
Even if that video encoding service only has a single really well designed endpoint, there's STILL coupling between the application server and the video encoding service.
Just because we've replaced a method call with an HTTP request doesn't mean things aren't coupled anymore.
Sure -- you may be able to deploy certain changes to your video encoding service without changing your application service. However, you need to be keenly aware of what changes are compatible with existing application servers and which are not and that adds complexity and cognitive load. Maybe it's worth it in some cases. In many cases, it's not.
Granted, this is not something to worry about with S3 or SQS.
> If we go with your definition then my app is a monolith with S3 and SQS
okay sure, I agree that calling your app a monolith with S3 and SQS is ridiculous.
The point is that we all may think that we're writing another S3 or SQS when we write our own micro-services. However, in practice maintaining a stable, backwards compatible, public API like that is quite costly and we usually end up re-implementing function calls as HTTP requests and then calling it "loosely coupled".
Your definition of uncoupled makes some sense. It's a good starting place at the very least.
Sometimes you gain real benefits from this approach (e.g. maybe a component needs to be scaled independently), but I find that very commonly, people write tightly coupled micro-services and end up with a distributed monolith.
However, scaling a monolith (as someone who does this for their day-job) is significantly harder than scaling independent services. This is mainly because performance issues in one function can hog resources that other functions share. This is particularly the case when you have a single, central database where one poorly optimized query can cause performance for all users to degrade severely.
Being able to scale individual components/services will be the most efficient solution, but if there doesn't happen to be a particular bottleneck that bogs down everything else (besides the database), it seems like the traditional monolithic load balancing approach may not be so bad.
Especially compared to the effort of trying to split an existing monolith into microservices. And if there are other bottlenecks, you can focus your effort on improving or isolating those parts.
(I only have intermediate experience in this area and certainly way less than you do; definitely not trying to claim anything authoritative.)
It only makes sense to write services (note, "services", not "micro-services") when it is inevitable and obvious that your software is hitting scaling issues related to some subsystem, or when you have systems that need to be independently reliable. (i.e. ATM's should work even when the banks website is down)
I would've had to do all the same work to extract it to microservices, but also would've had to do a lot more on top. Had it not been a legacy system, probably worth it - but as it was, leaving it as a monolith querying a now-split-up set of DBs was cheaper and faster.