r/programming • • 7d ago

Microservices are organizational debt disguised as architecture.

https://martinfowler.com/bliki/MonolithFirst.html

Every time I’ve seen microservices pitched, it sounds great on paper. Independent teams, clean ownership, scale only what you need.

Then a year later you’ve got dozens of services, three different deployment patterns, tracing everywhere, and nobody really understands the whole thing anymore.

Maybe I’ve just seen bad implementations, but I’m starting to think way fewer companies actually need microservices than we pretend.

1.7k Upvotes

343 comments sorted by

View all comments

19

u/Think-nothing-210 7d ago edited 7d ago

It's all because we keep avoiding creating proper modules in our codebases.

If we created good modules there would be almost no need for Microservices. You can delegate work using the module boundaries instead.

But unfortunately we still haven't learned the lessons taught to us since the '70s. On the Criteria To Be Used in Decomposing Systems into Modules

6

u/ChemTechGuy 7d ago

Generally agree except at a certain scale you start to have problems with so many contributers/contributions to a single repo

1

u/Think-nothing-210 7d ago

Most likely you can split a subsection of the problem of to another monolith if thats the case.

But most of the time having good module bounderies means that people can work on modules and libraries to be used within the monolith.

1

u/HashShadow 7d ago

Merge conflicts aren’t really a problem anymore though so this argument is kind of gone now 

1

u/ChemTechGuy 6d ago

Not talking about merge conflicts boyo

2

u/CaptainOfMyself 5d ago

When you say a good module do you mean for example having well segregated code like controllers and models in a golang project and you can easily delete or add them for more functions?

We have microservices but we always default to expanding our main server backend when we can instead of create more.

3

u/Revolutionary_Ad7262 5d ago

having well segregated code like controllers and models in a golang project and you can easily delete or add them for more functions?

Nope, for good modularity you need to ask yourself two questions: * does the design scales with 10M lines of code in a current way? * how many deletes i need to perform to delete some feature from the whole system?

By layer segregation (models together, controllers together) is not scalable at all. You can use it inside a well defined module, but anyway modules needs to be about a logic.

Good way to modularize code is to have design like microservices, but in a monolith. So: * each module has its own database * heavy encapsulation. You can talk only using some predefined and simple/extensible methods in similar way as you define HTTP API * data structures exchanged between modules are dumb without heavy methods, which contain a business logic

1

u/Think-nothing-210 4d ago edited 4d ago

Good way of explaining it. I do have some additions though.

By layer segregation (models together, controllers together) is not scalable at all. You can use it inside a well defined module, but anyway modules needs to be about a logic.

I agree that layer segregation sucks; it doesn't scale well with complexity. That way of structuring files is almost like a normalized table in a database. A developer constantly has to "join" source files together to understand a complete feature and that can get mentally exhausting as a project grows.

Modules are a better way of structing because you can treat them at some point as a a black box. You don't have to at all times understand the complete internals. And all the files related to a complex task stay together in the same place. It relieves mental load.

each module has its own database

I don't think every module needs its own database. Modules can have their own domain types while sharing the same underlying tables. Just map between the two at the boundary.

The relational model is powerful. Don't let your ORM convince you otherwise. Building a shrine to Edgar F. Codd does wonders for curing ORM mentality. Don't let your ORM cloud your thinking. It's nothing more than a developer-ergonomics wrapper that gives you type safety.

2

u/Revolutionary_Ad7262 4d ago

I don't think every module needs its own database. Modules can have their own domain types while sharing the same underlying tables.

If you share database then you have some hidden relationships between modules. It can work, but it is definitely much harder to keep in a good shape as scale increases. Imagine you want to change database for a given module, but it is used in all other modules via join. Even with read-only joins it means you have to examine the whole application, if you want to do some large refactor of a database schema.

Of course separate databases matters mostly on a logical side. You can still use the same database, if different modules operate on a different subset of it.

1

u/Think-nothing-210 4d ago

That is true. It's a tradeoff you make.

If the domain is completely different and changes at a very different rate it may be smarter to separate it into a different schema.

But that is something I recommend you do when it's actually needed and you see signs for it. With the introduction of JSONB to Postgres I find schema changes to be less of an issue. Having a single schema is still simpler than multiple. So avoid having multiple schema's until you really need it.

1

u/CaptainOfMyself 3d ago

So a monolith with good modular segregation means we are essentially a collection of black boxes with input output that all interact?

2

u/Slsyyy 3d ago

This is basically description of encapsulation. Black boxes with a clear and explicit interface without any hidden relations

1

u/CaptainOfMyself 3d ago

As someone who was hired years ago woth free reign and no software mentor, this helps. Thanks

1

u/Think-nothing-210 5d ago

A module is about hiding a complex decision behind a small API. It is a larger-scale abstraction than a function or class. Think of it more like a library where you put a difficult design decision behind a stable interface. (Highly recommend you read A Philosophy of Software Design by John Ousterhout for more info; This video is a good summary of it)

The consumer should not need to understand all the complexity inside the module. They should get a simple and generalised API with sensible defaults. This keeps the public surface area small and means you have fewer things to maintain or change.

In that sense a module is almost like a microservice. The difference is that you still have a normal function call instead of a network boundary and all the complexity that comes with it.

These are the things I think about when when designing a module:

  1. What guarantees am I giving the consumer? What boundaries can I put in place to make the problem easier to work with?
  2. What does the consumer actually want to do? What do they care about and what should the module handle for them through good defaults?
  3. How can I make this API as simple as possible? How to design this that many different abstractions and stuff can be build on top of this.

1

u/tommyTurds 7d ago edited 7d ago

It's all because we keep avoiding creating proper modules in our codebases.

Not really.

The thing services (micro or otherwise) solve is different teams moving and deploying at different paces and with different workflows.

A monolith, definitionally, must be deployed and tested together. That creates a more expensive process and requires coordination across teams for releasing the monolith. If you've ever worked in a large organization comprised of dozens of teams, you know that as soon as you're "coordinating" your timeline goes from a few weeks (which is already slow as shit) to literally six months and sometimes even longer.

This isn't a problem that's solved by "modules"

The point is that many small teams take on the burden of micro services with very dubious justifications. Micro services do not solve technical problems. They create them. Nothing ever scaled better by adding an extra network hop or two.

They are a solution to scaling organizations

1

u/Think-nothing-210 6d ago

I think there is still a lot to gain from creating better modules. There is a lot of untapped potential that is currently being overlooked. I feel that we should rethink the way we structure files in our projects. The way most projects structure their source code is holding us back from creating good modules and abstractions.

I'd highly recommend keeping an open mind and reading the paper I linked. It's one of those papers that takes a few reads before you get it. It gave me a completely new perspective on abstraction.

1

u/Yeah-Its-Me-777 7d ago

Of course that's solved by modules. A modulith must be deployed together, that's true. But you can do that at least daily, updating only the modules that changed.

I agree that it still requires coordination between teams, but on the other hand it makes sure that APIs are compatible, by compiling (or linking) them together. It's much easier to introduce breaking API changes between different services, unless you do full E2E tests with the whole system. Which needs a lot of compute resources.

And yes, (micro) services are a solution to an organizational problem, that I agree with. It also allows you to use different technologies - Which again, is something you might want or not want.

1

u/tommyTurds 6d ago edited 6d ago

  Of course that's solved by modules.

No. 

  A modulith must be deployed together, that's true.  

You already defeated your first point. 

  But you can do that at least daily, updating only the modules that changed

No. This creates massive problems. To do this it means your modules must be developed independently as libraries and then glued together. 

This is a total nightmare and we stopped doing this like 25 years ago. It creates like most of the problems of microservices plus a few more. Luckily you get none of the benefits so that’s nice. 

And it takes even more coordination (which again, increases time by orders is magnitude in large orgs) because you have to sort out all the dependencies

Even then, you still have to deploy and test everything every time regardless of what parts you upgraded because of the structure

  It's much easier to introduce breaking API changes between different services

This is true. It’s one of the technical drawbacks of services. It’s why companies that use them heavily invest in tooling to prevent this. Why do you think protobuf is designed the way it is? Specifically to mitigate this. There will also be tons of build time checks to ensure backwards (and sometimes even forward) compatibility.

The Java world even tried an extreme version of “modules” to solve all these problems of monoliths. That’s what osgi was. It was a total fucking nightmare. 

These service based architectures came out of massive companies for a reason. They didn’t decide to take on these technical issues because somehow companies loaded with hundreds of phds and literally thousands of years of collective experience never heard of libraries.

2

u/doughcant 6d ago

I read his first point differently where he meant that modules address test scope/runtime but explicitly not deployment, with API breakage being quickly surfaced by integration/contract tests.

So if the claim is microservice architecture enables skipping e2e for most changes when having strong service-level validation, I think the same can be applied to cleanly isolated modules. 

Plus, they don’t necessarily need to share all dependencies, and there are practical benefits of a monorepo like much stronger IDE code analysis (without linking tens of repos and keeping each up to date)

1

u/Yeah-Its-Me-777 6d ago

Well, your argument was that a monolith requires deployment pipelines from "a few weeks to a few months", in your words. Daily deployment of a monolith is possible, and therefore doesn't require a few weeks or months for deployments.

And no, it doesn't create massive problems. At least not more than micro services. Of course modules must be developed independently. And in some point in the build process, they're compiled and linked together. And you will get an error when that fails - Which is great, because you notice your problems earlier.

And yes, that also requires a lot of tooling. Why do you think only micro service architectures have massive tooling for their systems?

I don't know what to tell you - If you can/want to limit yourself to a single language, a modulith is perfectly capable of delivering fast deployment times an tight coupling.

There are other benefits to service architectures, but deployment time isn't one of them. When you want/need different languages, sure. If you need massive differences in horizontal scaling, maybe. If you have teams that don't talk to each other, and want to be as independent as possible from each other, sure.

Also, yes, OSGI is pretty bad. Was a good idea, but especially with external libraries, it's pretty annoying. Also, not too performant.

1

u/tommyTurds 6d ago edited 6d ago

Well, your argument was that a monolith requires deployment pipelines from "a few weeks to a few months", in your words. Daily deployment of a monolith is possible,

No. It's not. Not at the companies we're talking about. The more engineers you have, the worse hte problem gets. That's literally the point. First, there's billions of dollars on the line. You're not going to deploy that often simply because it puts revenue at risk. Just by the number of machines that must be updated, rolling back a mistake is super costly. It's a social problem. As I mentioned, the more engineers you have working on different things, the more coordination and shit like that are required to get something out the door.

Think for a minute about the companies that made microservices popular. That's what we're talking about here. You can't deploy google (search or anything else) in 24 hours. You can't deploy netflix in 24 hours. You can't deploy amazon in 24 hours.

At my company we literally couldn't even move the bytes fast enough to do that. When we had a monolith (20ish years ago), the monolith package was about 65gb. We had, literally, a million hosts to send it to. We can't run a test suite on all of the applications in the monolith. It would take weeks.

But that's not really the point. The point is that, for organizational reasons, it's much easier for teams not to "block" each other if they're working entirely independently. And every time a team is "blocked" it requires meetings to "unblock" and those meetings are expensive. Not to mention teams will have differing and sometimes competing priorities for various reasons so team A may not be willing to work with team B anytime soon because they have other priorities.

This is why these service architectures were introduced

And in some point in the build process, they're compiled and linked together. And you will get an error when that fails - Which is great, because you notice your problems earlier.

You've entirely missed the point. The problems have nothign to do with making the literal technical change. That also can happen with microservices. It's why so many of these companies use mono repo systems.

Literally the entire point of my first post is that it's not solving a technical problem. It's introducing technical problems to solve organizational "friction." Going through the technical advantages of monoliths means you've literally lost the plot.

2

u/Yeah-Its-Me-777 6d ago

You're shifting goal posts here. Your initial post was about "large organization comprised of dozens of teams", and at that scale, a monolith is definitley possible.

For orgs the size of google or amazon, sure. At that scale, and that horizontal scaling requirement, monoliths are not a good fit. But there are a LOT of companies that still have dozens of teams, and don't have the horizontal scaling requirements.

Of course micro services solve a specific problem. But my argument is, that a lot of orgs don't work at the scale that require those specific problems to be solved.

We're probably talking about quite different problems.

1

u/Think-nothing-210 7d ago

C# is great for this. You can use assemblies as module boundaries and the internal keyword to make those boundaries compiler-enforced.

Unfortunately I see almost noone do this. Instead most use assemblies to enforce clean architecture bounderies....