The worst architecture I have had to work on was six microservices maintained by four people. Every feature touched at least three of them. A local development setup required running all six plus two databases and a message broker. Deploying meant coordinating releases in the right order, and getting it wrong took the checkout flow down. The team had adopted microservices because they had read that monoliths do not scale.
Their traffic was about forty requests per second.
I am not against microservices. I have built systems where splitting was clearly correct and the alternative would have been painful. But the split has a price, it is paid daily by everyone on the team, and the question is never "which architecture is better." It is "have we hit the problem that this price buys us out of."
What You Are Actually Buying
Microservices solve a specific and real set of problems. It is worth naming them precisely, because if none of them describe your situation, you are paying for nothing.
Independent deployment. Team A ships without waiting for Team B's half-finished work to be release-ready. This is the strongest argument and it is fundamentally organisational. With one team, it buys you very little.
Independent scaling. Your image processing needs eight instances and your admin panel needs one. In a monolith you scale everything to the needs of the hungriest part.
Fault isolation. A memory leak in the reporting module should not take down checkout. In a monolith, one process means one blast radius.
Technology freedom. The machine learning service is Python, the API is Node. Real, though usually less important than people expect, and it carries an operational cost of its own.
Notice that three of those four become compelling as the team grows, not as traffic grows. A monolith on well-sized instances behind a load balancer serves an enormous amount of traffic. The forcing function is almost always people, not requests per second.
What It Costs, Daily
The bill is not the initial setup. It is everything after.
A function call becomes a network call. That is the whole story, really. Function calls do not fail halfway, do not time out, do not arrive twice, and do not require you to decide what to do when the other side is unavailable. Network calls do all of that, on every single boundary you create.
Debugging becomes archaeology. One stack trace becomes correlating logs across four services. You need distributed tracing before you need it, because the first time you go looking for a cross-service bug without it, you will spend a day on something that should take ten minutes.
Data consistency stops being free. A database transaction across two services does not exist. You are now writing sagas, compensating actions and reconciliation jobs — and reasoning about the window where one service has committed and the other has not.
Local development gets heavy. If a new developer needs six containers running to see the login page, they will avoid running things locally, and your feedback loop rots.
Coordinated changes hurt. A field added across a service boundary is now two pull requests in two repositories, released in a compatible order. In a monolith it is one commit and the compiler tells you what you missed.
The Modular Monolith Is the Right Default
The middle ground people skip past: one deployable, one database, but genuine internal boundaries.
Organise by feature rather than by technical layer — a billing module, an orders module, a notifications module, each with its own controllers, services and data access. Modules talk to each other through explicit interfaces, never by reaching into each other's internals or querying each other's tables directly. Enforce it with tooling if your language allows.
What this gives you is most of the design benefit — clear ownership, understandable boundaries, code that is not a mud ball — with none of the distributed systems tax. You deploy once. You debug one stack trace. A transaction is a transaction.
And when the day comes that one module genuinely needs to be its own service, the boundary already exists. Extracting a well-isolated module is a manageable project. Extracting a module from a codebase where everything queries everything is the thing that takes a year.
When to Actually Split
The signals I trust, in rough order of how much they justify the cost:
Two teams keep blocking each other on releases. The strongest signal, and it is about people.
One component has a wildly different scaling profile. Bursty, CPU-heavy or GPU-bound work sitting inside a request-serving process is a genuine reason to separate.
One component has different reliability requirements. Payment processing that must stay up while an analytics feature can fail freely.
Different compliance boundaries. A component handling regulated data that you want in a separate environment with separate access.
Signals I do not trust: the architecture diagram looks more professional, a conference talk said monoliths do not scale, or a component "feels separate." Feeling separate is what modules are for.
How Services Should Talk
Once you do split, the communication choice matters more than the split itself.
Synchronous — HTTP or gRPC — when the caller genuinely cannot proceed without the answer. Simple to reason about, and it couples availability: if the callee is down, you are down. A chain of three synchronous calls has the combined failure probability of all three, which is how a minor dependency takes out a major flow.
Asynchronous — events on a broker — when the caller does not need an answer. The order service publishes OrderPlaced; billing, notifications and analytics each react. The order service does not know they exist and does not care whether they are up.
Event-driven is the pattern that makes microservices worth having, and it is the one teams adopt last. It is what actually decouples services rather than just distributing them. The trade is that flow becomes harder to follow — nothing in the order service tells you that three things happen after it publishes.
Two rules that keep event systems sane. Consumers must be idempotent, because every broker worth using delivers at least once and duplicates are normal, not exceptional. And events describe what happened, not what should happen next — OrderPlaced, not SendConfirmationEmail. The moment an event names an action, the publisher has become coupled to the consumer again and you have a distributed monolith.
// Consumers assume redelivery. Always.
async function onOrderPlaced(evt: OrderPlaced) {
const seen = await processed.add(evt.eventId); // atomic, TTL'd
if (!seen) return; // already handled
await sendConfirmation(evt.orderId, evt.customerEmail);
}
Node.js Specifics
A few things that come up on every Node service I have built or reviewed.
Keep one database per service, genuinely. Shared databases are the most common way teams end up with distributed monoliths — two services deploying independently but unable to change a schema independently, which is the worst of both worlds.
Watch the event loop. Node handles I/O-bound concurrency beautifully and CPU-bound work terribly. A synchronous JSON parse of a large payload, or heavy crypto in a request handler, blocks every other request on that instance. That work belongs in a worker thread or a separate service — and that, rather than the architecture diagram, is a legitimate reason to split something out.
Propagate a correlation ID through every call and every event, and include it in every log line. This costs almost nothing to add at the start and is close to impossible to retrofit usefully.
The Advice I Give
Start with one well-organised deployable. Draw the module boundaries carefully and defend them in code review, because that is where the real architectural work happens. Add distributed tracing and correlation IDs before you have anything distributed.
Then split when something specific hurts — a team blocked, a component that needs different infrastructure, a failure that took down more than it should have. Split that one thing. Live with it for a while before splitting the next.
Every distributed system I admire got that way by being pulled apart under pressure, one piece at a time. The ones I have had to rescue were designed that way on a whiteboard, before anyone knew where the pressure would come from.



