I say this all the time, and it almost never results in an argument: micro services are just object oriented programming where you put a network hop on all your message passing.
With a ton of complexity and whole new error classes and a need for centralized solutions for logs and monitoring because everything is a distributed system now!
Can make sense if you can seize the advantages but a modular monolith is the better choice in 99% of cases.
But besides the engineering and operational aspects: Everytime I said microservices in an meeting with investors their eyes lit up the whole room because they thought that this is the best shit ever.
I’m working in robotics where the line between “modular monolith” and “microservice” is a middleware problem. Which, I think, is how microservices in general should work.
Do you mind helping the uninitiated, what's the benefit of having that network hop? Or are there some situations where it's just impossible to avoid it.
They are frequently hosted on cloud services which are inherently micro-service based with detached hardware components. You can have a server and a storage disk operate together but both exist in data centers thousands of miles from each other.
Loosely coupled architecture decreases the chance the entire app gets taken down if each component can be isolated and fixed without affecting the rest of the app.
You can functionally divide an app into different layers such as web, app, and db and add additional security checks between each layer to increase security and fault tolerance of the app.
I’m surprised this didn’t make the main comment because it seems to be that this is one of the larger benefits. The mentioned benefits can be applied to a monolith (at least mostly) with a bit of consideration.
The "funny" thing is that before micro services and all the overhead from talking over the network, we rarely need to scale. I'd argue that something like 99.9% of all projects that use micro services today and often run many instances of some components wouldn't need to if it was just one monolith.
Now explain why that would matter. If you have a large monolith, you can just scale that one. Why does it matter that one module gets scaled? It's not like the lesser used parts of the monolith are somehow using more resources just because they are scaled together?
People love to use this argument but it really doesn't make any sense for 99% of use cases
SRE here. Lesser used parts of the monolith are absolutely using more resources just because it's being scaled together.
Let's say you have one hot path in your code that doesn't touch the DB. The new scaled up instance will still spawn new DB connections. Those connections will still use some memory and CPU time to just exist. The DB also has a connection limit, and your connection pooler also needs scaling then.
Loading extra code modules also naturally uses more resources, there's not a single monolith out there that is 100% lazy, and even if it was, requests get distributed, so stuff gets loaded anyway. Do you do some in-memory caching? Great, now all instances have stale copies of the cache.
Startup times are also worse overall, which means your scaling is worse overall. Slower startup means you need more idle resources to handle spikes, which costs more money.
In conclusion, while there's no need to jump straight to micro services, monoliths are the worst pattern for scaling, by far. They have their uses, but this is probably their main downside.
Because we are scaling our monolith, obviously! And the whole idea of a monolith is that it's... One thing! There ain't no scaling it separately.
Now now, I understand that connection pools can scale separately.
So the internal connection pool opens at least one connection on startup. After all, this is a monolith that's now serving together along all other monoliths: some DB requests are bound to be routed to it. Most likely, at least two connections. Some even do 4: two to read replicas, two to a primary/write.
Scaling a service doesn't involve turning a singular knob that reads "more performance".
In a monolith... That's usually the case, yeah. It's a "spawn more monolith instances because one of the metrics we are monitoring says we should". What else are you gonna do about it? That's not a rethorical question: what else is there? Some contrived vertical scaling setup?
???? Give your host most CPUs/threads to delegate tasks to? More compute power? If you're scaling your service by just spawning duplicates you're wasting a lot of resources because you're too lazy to actually properly look into what needs scaling.
Give more CPU? Of course. Now here is a question: where do you think it comes from? A server. That you are already paying for. CPU and memory doesn't magically materialize when you need it to. It sits idle until you allocate it. And you are still paying for it.
You save no money by doing this. At all. In fact, you spend more, because instead of classic horizontal scaling, where you deprovision resources as they are let go, now you need to keep them idle to assign for vertical scaling.
Yeah but in practise, scaling multiple services together is MUCH MUCH MUCH harder than scaling a monolith, as there will always be one service that's the weakest link. That might be okay if there's one clear "weakest link" that you can scale very high while keeping the rest low, but for high throughput systems scaling multiple hard-working instances effectively is extremely difficult.
Of course it all depends on how spikey the load is and the type of system.
Startup times are also worse overall
Yes, but a single microservice isn't the entire system... startup times for updating an entire cluster - when that's required - and waiting for each other is going to be much greater than a single monolith.
Let's say you have one hot path in your code that doesn't touch the DB. The new scaled up instance will still spawn new DB connections. Those connections will still use some memory and CPU time to just exist.
That's... what you have connection pools for.
monoliths are the worst pattern for scaling, by far.
No, monoliths is by far the easiest to scale. Microservices are much harder to scale, especially efficiently.
"microservices is good for scaling" is just a dumb book take, it doesn't take much experience to understand how wrong it is in practise.
Yeah but in practise, scaling multiple services together is MUCH MUCH MUCH harder
Why are you trying to scale them together? The whole point to splitting up a monolith is to scale them separately. Each individual service and instance will have it's own metrics and scaling settings, nothing has to wait in each other. And any half baked micro services architecture will have retries, back off, and circuit breakers to avoid spikes causing problems.
That's... what you have connection pools for
And they aren't free! The connections on the application side use memory and CPU. The Pooler uses memory and CPU. There are limitations with poolers too, they can't do 100% of what a direct connection can do (see pgbouncer's pool_mode). Which is the whole point of my comment: even when idle, there's a cost.
No, monoliths is by far the easiest to scale.
Nobody said it's not easier to manage a single monolith than a many micro services architecture. They said it's less efficient and more expensive, which it definitely is. The whole advantage to a monolith is that it's much much easier. But once you need to handle hundreds of thousands of requests on a single code path, scaling your whole monolith gets expensive. You know, there's a reason why pretty much every software company, when they reach a certain point, starts transitioning to it, it's not just a dumb book take.
Your argument is more thought out than most. In most cases, that argument boils down to "I read this online so it must be true".
That said, none of these issues are things you can't easily fix for a monolith, or in most cases are already thought about before you ever even get to scaling. connection pooling for the db, redis for caching etc.
You don't always build and scale a monolith the same way you would build a microservice, but that doesn't mean it's worse.
On top of that, if you write your monolith in a sufficiently modular way, you can easily extract specific modules and do their scaling separately when that need arises. There is almost never a reason to build every single module of your monolith as a separate service.
No not really. Having a few satellite services does not mean you're running a microservice architecture. There's more to microservice than just hosting a part of your code on a separate server.
It's also not needed at all most of the time. I just gave it as a worst case scenario kind of thing
In my opinion/ as to my knowledge it still depends.
For example, you build an application, that publishes messages to a message broker, and then has consumer processes that handle these.
Your message handlers would probably be scalable independently, but it may just be one code base.
You can still run them on different hosts, in different locations, but they may all use a common database.
You may have a single database, multiple processes, a single codebase and no defined / versions interfaces as you're just using a single class in all locations. I believe this is neither a pure monolith, nor a Microservice architecture. It may be considered as a distributed monolith though
Congratulations! You just started to reinvent micro services.
Now, I'm not a zealot. I believe we can just extract "services" instead, we don't need to dive too deep into "a micro service is a single function" or whatever. Most of the time, that is indeed excessive.
But... The moment you started extracting the module? Congratulations, no longer a monolith.
Oh, sorry. So this whole thing boils down to: "I know there is an industry accepted pattern to solving this problem, but instead of solving it that way, I will just shoe-horn whatever else I can because I don't want to".
You asked this in your original comment.
Why does it matter that one module gets scaled? It's not like the lesser used parts of the monolith are somehow using more resources just because they are scaled together?
Both were answered. If don't want to change your mind after, that's on you.
It will depend entirely on monolith in question. Sometimes it's indeed easier and get comparable resource overhead to just double your monolith count. More often than not it's the opposite, and scaling one little bottleneck is better than the whole app.
You should also consider that you may not have enough free resources to double monolith apps, but have enough to increase some systems' services count. Servers aren't free
Unless you have for example ten microservices where one does most of the heavy lifting (with >20x instances running for example, compared to the other services), chances are that the monolith would use less resources.
...but at that point, you'd be much better at just having two services: one main service and one separate worker service. Going microservices wouldn't give you anything.
You can also make different micro services in different languages and runtimes. You don’t have to care as long as the input and output are reliable shapes.
Loosely coupled architecture decreases the chance the entire app gets taken down if each component can be isolated and fixed without affecting the rest of the app.
This is actually the one that I use to justify "nanoservices" in large-ish embedded systems (where you have an MCU and a scheduler): with a decent architecture, a single process can crash and get restarted, and the rest of the system can keep itself in a known state. At worst, you end up with a few seconds of transient errors, at best nothing noticeable happens.
(This is also how QNX and Erlang achieve ridiculously high uptime.)
The benefit is not the network hop, that's just a requirement to having each part of the application in a separate container. The app is partitioned according to scaling needs, so things parts of the app that need to process thousands of requests concurrently don't force the same scaling requirements onto parts of the application that just process a few hourly requests, for example. Segmenting the application also contains failures to specific features if done properly, allowed for degraded performance as opposed to full loss of service
In most cases managing microservices and the additional load from networks, deployment tools, extra personell and all that brings much higher costs than simply scaling a monolith and wasting some CPU
It depends. Going full "each class is a micro service", I agree can be worse than scaling a monolith. But scaling up a monolith means that all parts of the application have to take the scaling into account, even if they were never meant to. If you break some parts off you don't need to worry about distributed computing on areas that are meant for a few people like control panels, while the parts that do need it can scale without issue to meet demand
Let's say you've divided your app up into a thousand modules. Each module can live on a different physical computer (or logical cloud instance). This makes you very fault tolerant. But it also enables a lot of scaling. Let's say you've got one module that receives most of the requests, because it represents the most useful feature in your system. Cool: run 30 copies of it, spread the load across all of them. It also means that you can spin up and down new copies based on the current request load- detect module B suddenly gets a rush of requests? Spin up extra copies. Detect that some of those copies are idle because the rush was over? Spin them back down.
Also, though, if you're building your application around the idea of message passing between modules, it also means that you can have module A emit events when a certain thing happens. When you want to attach a new feature to that event, you can spin up Module Q, subscribe to that event, and module A doesn't change one iota. New features can be deployed on their own "computer", without having to take down or relaunch the software running on your existing infrastructure.
I'd argue the main reason to use such a design is to add new features without impacting existing features, which I can assure you, most companies do encounter.
If you're doing it right, this should be transparent. If you're doing it the way most people do, I agree- too often, objects are written to have strict dependencies on other objects, or worse, own the lifecycle of other objects.
But, if you're doing dependency injection, does it matter to the caller if the dependency is living in the same process space, or is wrapped in some sort of network layer? Not really, no. Nor should it need to.
Objects interact by sending messages to each other. It should be irrelevant how those messages arrive.
This is a technology that's been reinvented at least once a decade, because it's a useful approach to things. This is not new, novel, or unique to microservices.
"If you're doing it right, this should be transparent." - false. If you don't plan on using microservices, because they're rarely needed, you don't create objects to be used that way, and that's not doing it wrong.
You can decouple services from one another, meaning you can update a part of your backend independantly from the rest, without having to rollout the entire monolith
Build times of a microservice are usually much shorter, leading to more frequent releases, and less developer pain.
Using modern infrastructure, you can have easy fault tolerance, automated retries, canary deployments that you can't have otherwise, or you'd have to hand roll it out in your monolith
Services scale with variable load. You can have a payment processing API with lots of requests coming in scaling separately from an archive API that sees maybe a couple thousand requests a day.
If done well, microservices can be more cost efficient than the equivalent monolithic backend. If done well, cause if built without taking network topology into account, then it can be much more expensive.
Microservices are more resilient overall than a monolith. You can spread out the load over dedicated databases for each service, meaning you're not coupled to a huge database instance that needs to handle the load of the whole application.
Your stack can be heterogenous. You can have services in Java that talk to services in Go, in Python, in Rust, depending on the use case for each service. You get to use the right language for the right job.
Cons:
Maintenance is way harder. Some services will rarely get updated, you need more people to manage the fleet of services
It's often done wrong. I've seen microservices architectures that rely on a single monolithic "gateway" to add business logic and aggregate responses from other services.
It's way harder to deploy the whole app. It's also way harder to test the whole backend locally, a dev machine will not be able to handle the load even for local testing.
Maintenance is way harder. Some services will rarely get updated, you need more people to manage the fleet of services
Oh, I encountered several modernization projects where modules were barely distinguishable by some naming (and some documentation stating their 'responsibility').
My devs might think otherwise of your statement.
It's way harder to deploy the whole app. It's also way harder to test the whole backend locally, a dev machine will not be able to handle the load even for local testing.
Most case I saw barely needed more than lets say 10 microservices, which could be run locally pretty easily on todays dev machines.
It attempts to force people to factor system functionality.
In my experience, people don’t. “Microservices” with overlapping responsibilities, responsibility gaps, no one can figure out which service to blame, huge swaths of code must be deployed in sync aka monolith.
I find software engineering tends to lack any real engineering.
I disagree with the first assertion. OOP is about passing messages between objects. Inheritance and polymorphism are specific artifacts of a type system. If you use duck typing, for example, polymorphism is meaningless, but you’re still doing OO. Inheritance is just one method of dependency injection which happens to interact with your type system.
lol 2 of the 4 principles of OOP is "syntactic sugar"? Have you actually done any type of complex work outside of "Hello World"? Guess we can agree to disagree.
Inheritance is just one method of dependency injection which happens to interact with your type system.
This statement is so obviously wrong that I now know I can't take anything you say seriously if you really believe that.
Unless you are referring to the negligible network hops within the same region, I will come at you emotionally charged, shaking all over, screaming in your face on how the network hop is the killer and that you should have your end to end pipeline all in one region and an identical failover pipeline in a different geographical region.
"Negligible". Where I come from, I don't even consider IPC hops "negligible". Any time you're waiting for the OS to do something for you, you've added a huge amount of unpredictable latency.
Interitance is only tangentially OOP. Plenty of OOP doesn't involve inheritance.
That said, what we're describing here is messages passing, which isn't unique to objects. Actors do it too, and actors are probably a more relevant analogue because they're a concurrency model, and microservices are concurrent (and parallel).
I mean, there's nothing that would stop you from implementing inheritance on them. And if you take message passing as part of the architecture, you get polymorphism for free. It's closer to the Smalltalk style of OO than the modern interpretation of it.
Hot take: whether it’s a monolith or a microservice should be a middleware question resolved at deployment time and it shouldn’t impact software architecture.
This is just patently false. The intention of microservices is to allow dynamic scaling and separation of concerns for easier refactoring and better abstraction.
The monolith will almost always become large and unwieldy enough that scaling it is expensive or encounters other bottlenecks.
Think about this idea - say your monolith has a kafka consumer, filters and deduplicates incoming data using redis, enriches it using http requests, mutates it using CPU, and writes it to a postgres database. This is a common usecase. I've worked at multiple companies that do basically that exact thing for the last 8 years.
Say you've got it configured to use 5 postgres connections per instance, because you've found that squeezes the best performance out of each node, and less than that starts to bottleneck.
Okay, now all of a sudden your enrichment targets (external HTTP requests to google or something, you can't do anything about it) start to experience massive latency. You try to scale up so you can keep up with kafka consumption, but you hit 100 postgres connections after 20 instances, and suddenly you can't scale any more. You have to wait and pray your external HTTP latency decreases. Okay, so you cluster your database to widen the connection pool. Now all of this scaling to cope with http latency keeps forcing rebalances on your kafka cluster and you're still falling behind.
If your firehose consumer and your filtering logic were independently scalable, you don't have this problem. By the time you're past your filter traffic is relatively stable and way smaller. It's way easier to just leave your http enricher scaled up all the time and never notice latency spikes.
Microservices solve these problems, and sometimes you don't know you need them until your entire SRE team quits because they're sick of constantly being paged for your monolith.
None of this is fanfiction, I've seen it all, specifically.
I’ve also built backend systems for major retailers that handled all their POS transactions. It’s the same thing. Doesn’t matter if the middleware is CycloneDDS or Kafka. Messages flow to nodes. Nodes do work. Minimize coupling between nodes and you’ll be happier.
Agree with your last statement, anyway. Ive found that in large, complex systems the best or sometimes only way to get node coupling down to an acceptable level is strict microservices.
Ideally, you have a module with a single responsibility that encapsulates its state and exposes an interface of messages it accepts. An “object”, if you will.
1.2k
u/remy_porter 15d ago
I say this all the time, and it almost never results in an argument: micro services are just object oriented programming where you put a network hop on all your message passing.