I’m not seeing ‘tightly coupled code’ as one of the drawbacks of a monolithic application architecture

couplingdistributed-systemmicroservicesweb services

One of the most common things I see when discussing pros/cons of microservice vs monolithic architecture is that monolithic applications have, or always trend toward, 'tight coupling.'

To be honest, I'm not seeing why this is true if your developers know how to leverage any standard OO language. If I need a certain part of my application to handle, say, payments, and multiple other parts to interact with the payments system, can I not leverage the abstraction features of the language I'm using (classes, access control, interfaces) to create modularization between different application functions?

For example, if I'm using Java, I could create a 'PaymentsDAO' (data access object), or maybe 'PaymentsClient', which can expose functions that the rest of the code can use to interact with the payments database, etc. If one sub-team in my team wants to work on payments, they can continue to write code in the PaymentsDAO, publish that code to the central repo, etc, while I simply use the DAO's function signatures, which would not change, and continue to write code wherever I need it, right? Where's the coupling? If payments code changes, I don't need to change anything in my code, or understand the changes, to account for that.

Is the only drawback of this 'coupling' that I need to git pull more often, since the payments code would need to be in the same deployment as my change, as opposed to a separate deployment, and then consumed over the network through an API call?

To be honest, I'm not seeing a strong case for the 'tight coupling', and I want someone to change my view here because my current team at work is using a microservice architecture 😀 I'm more certain about the other pros of MSA, like scalability, flexibility of technology stacks across microservices, fault tolerance of a dist system, and less deployment complexity, but I'm still uncertain on coupling.

Best Answer

Why X when you can do Y?

The problem with questions like these is that no one has ever claimed that you can't take the more difficult route if you really want to. People aren't pointing out that the difficult route is impossible, but rather that it is needlessly difficult.

So, yes, if you avoid certain approach and instead spend all needed effort making sure things don't go wrong, that inherently means that things aren't going wrong and you didn't need to use that certain approach. But can you really be sure that:

  • You're not spending more effort than if you had followed the suggested approach
  • You and every other developer will not make any mistakes in avoiding those problems
  • You'll be able to recover when you come across such a problem, and not just realize you've painted yourself in a corner?

The odds are not in your favor.


Different types of coupling

There are degrees of loose coupling. It's not a binary state. This is not a complete list, but for the purposes of this discussion, some degrees are:

  • Level 0 - No abstractions, hardcoded references
  • Level 1 - Abstractions (interfaces), but everything in one project
  • Level 2 - One solution, separate projects abstractions
  • Level 3 - Abstractions are not part of the project but are loaded separately (think NuGet)
  • Level 4 - Independently hosted microservices

You're at level 1/2 now. And it avoids some of the more blatant problems with tight coupling, what I tend to refer to as "code coupling". There are different types of coupling, and not all of them are related to the code of a specific application. Subsequent levels are going to solve additional problems with certain kinds of coupling.

Level 3 moves the abstraction out of the solution, so that it can live its own life. This is the most useful when more than one solution consumes this abstraction, or when the developers of the abstraction are a completely different team from the developers of the main product. Maybe it's a different company team, maybe they're different companies altogether.

You've probably used several third party libraries. How much did you know about their teams, who developed them, or the internals of the library they created? Not much. And that's the point, because the abstraction is so loosely coupled to the end product that you don't even need to know anything (other than its interface) about it anymore.

But level 3 still suffers a specific problem: if the abstraction gets updated, then all of its consumers will have to rebuild (or at the very least re-release) to account for these new changes. This requires an active line of communication, at least one-way (e.g. a third party developer's news feed announcing a patch to fix issues or expand the feature set).

This is what level 4 (microservices) solve. Because it's now an independently deployed application, any published change goes live for every consumer at the same time. The consumers don't need to change anything or re-release their own application.
As long as the interface or public URL doesn't change, the two services don't even need to communicate anything about any of their changes.

A very clear example of why this is desirable is e.g. an identity provider.

  • Levels 0/1/2 all amount to authorization for this specific application.
  • Level 3 makes it possible to standardize identity management across all of the library's consumers, but in the end each consumer is still choosing to update to new versions of the library individually.
  • By having this as a microservice (level 4), any changes made to the identity provider (e.g. changed rights, user access, different security protocols) automatically deploy everywhere at the same time, and the developers of the consuming applications don't even need to be kept in the loop at any stage.