Monoliths have high coupling by default — coupling components together is the "easy" and "obvious" way to get things done in a monolithic codebase, and so it's what junior devs will inevitably do when pressed for time.
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.
Microservices worked on by those same developers have high coupling by default, too.
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.
> "Make a synchronous RPC call between components" is still the "easy" and "obvious" way to get things done.
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.
I’m very intrigued by this comment. It probably captures the thing that has held me back from microservices. Can you explain how one can break up a monolith without keeping the interdepencies?
Say you have some unit of work A within a monolith, and it is used by service B, C, D also in the monolith.
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.
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.