You Probably Do Not Need Microservices
An honest accounting of what distribution costs a small team
6 min read

The strongest argument for microservices I have ever heard, in private, from a working engineer, was that it would look good on the team's resumes.
He was joking. He was also not joking, and everyone in the room knew it. That is the honest starting point for this conversation, because the technical arguments get made loudly and the career arguments get made quietly, and both are doing work in the decision.
I want to lay out what distribution actually costs when you are a team of one to ten, because the pattern is not wrong. It is priced for a problem most of us do not have.
What you are actually buying
Services solve a small number of real problems, and they solve them well.
You can deploy independently, which matters when teams are large enough that coordinating a release is genuinely expensive. You can scale one component separately, which matters when one component has a load profile the others do not. You can enforce boundaries organisationally, which matters when the alternative is thirty people reaching into each other's code. And you can isolate failure, so one bad component does not take the process down.
Every one of those is a real benefit. Notice how many of them are about the size and structure of the team rather than the software. Independent deploys solve a coordination problem. Enforced boundaries solve a discipline problem at scale. If your coordination problem is a group chat with two people in it, you are buying a solution to something you do not have.
What you are actually paying
The bill has items that do not appear in the architecture diagram.
Every call that used to be a function call is now a network call, which means it can be slow, it can fail, it can succeed and then be lost on the way back, and it can succeed twice. Code that was previously guaranteed to run once and either return or throw now needs timeouts, retries, and idempotency at every hop. That is not extra work at the boundary. That is a different way of writing all the code that crosses one.
Transactions stop being available. In one process with one database, a write that touches three tables either happens or does not. Split those tables across services and you are hand rolling consistency, usually with events and compensating actions and a reconciliation job that runs nightly and that somebody has to look at.
Debugging changes character. A stack trace stops being the whole story, because the story now spans four processes. You need distributed tracing before you can answer questions you used to answer by reading a log line, and setting that up is a project.
And then the operational tax, repeated per service. A pipeline. A deploy target. A secret store entry. Monitoring. Alerting. Version pinning. A dependency upgrade that used to be one pull request is now five, or worse, it is one and the other four drift.
None of this is fatal. All of it takes time that is not being spent on the product, and small teams are, definitionally, short of exactly that.
The version that works at small scale
The alternative is not a big ball of mud. That is the false choice that pushes teams into distribution before they need it.
You can have real boundaries inside one deployable. Modules with their own models and their own service interfaces, where the rule is that modules talk to each other through exported functions and never by importing each other's internals.
That rule is the entire trick, and it is enforceable in review. Break it and you get the worst outcome, which is a codebase that is tangled internally and would be miserable to split later. Hold it and you get most of what services promise. Clear ownership, clear interfaces, the ability to reason about one module at a time.
The part people undersell is that this is also the cheapest possible preparation for splitting up later. When one module genuinely outgrows the process, its boundary already exists. Extracting it becomes a mechanical change rather than an archaeology project. You have kept the option without paying for it.
The exception that is actually real
There is one case where I would split early, and it is not about scale.
If a component has a fundamentally different runtime shape, put it somewhere else. A video transcoder that pins CPU for minutes at a time does not belong in the process serving your API, because it will make request latency ugly in ways that are hard to trace. A machine learning inference service with a five gigabyte model and a Python runtime does not belong in your Node process for obvious reasons.
That is a resource isolation argument, not an architecture fashion argument, and it is easy to recognise because you can state the specific interference you are avoiding.
The related case is a component with a wildly different availability requirement. If webhook ingestion must stay up while the rest of the platform is being deployed, that is a real reason to separate it, and I would separate exactly that and nothing else.
How I would decide
Ask what specific pain you are trying to remove, and check that it exists today rather than in the plan.
If the answer is that deploys are risky, the fix is a test suite and a pipeline, not distribution, because distributed deploys are riskier per unit of change, not less. If the answer is that one part is slow, measure which part, then scale or optimise that part. If the answer is that the codebase is tangled, modules fix that inside one process and services do not, because a network boundary does not untangle logic, it just makes the tangle harder to see.
If the answer is that it will look good when you are hiring, or when you are being hired, say so out loud. It is a legitimate consideration and it deserves to be weighed against the cost in the open rather than dressed up as scalability.
My platform runs three front ends and one backend process, and the backend hosts both an HTTP API and a WebSocket server on the same port. That is a bet with a known expiry date, and I have written down what triggers the split. Until that trigger fires, the simplest thing that is honestly bounded is winning.