Most architecture debates at small companies aren’t really about monoliths versus microservices. They’re about which mistake the team would rather make: shipping a tangled codebase that’s hard to change, or shipping a distributed system that’s hard to operate. Both are real risks. Only one of them tends to kill early-stage products, and it’s usually not the one founders worry about first.
What microservices actually cost you
Splitting an application into services is a trade: you give up simplicity in exchange for independent deployment, independent scaling, and fault isolation between components. That trade only pays off once you have enough traffic, enough team members, or enough conflicting release cadences that the simplicity was already costing you more than the distribution would.
Before that point, microservices add cost with no offsetting benefit. A single feature that touches two services now needs two deployments, a versioned contract between them, and a plan for what happens when one is up and the other isn’t. Local development needs multiple services running together, or a set of mocks that drift from reality. Debugging a request means reading logs from several processes instead of one stack trace. None of this is exotic engineering — it’s just work that a team of three or four cannot easily absorb alongside actually building the product.
The failure mode is familiar: a small team adopts a services architecture because it’s what they’ve read about, then spends its first year fighting deployment pipelines, service discovery and network timeouts instead of validating whether anyone wants the product at all.
What a monolith gives up
The counter-argument isn’t imaginary. A monolith that grows without discipline becomes exactly the thing microservices promise to prevent: modules with no clear boundaries, a shared database that every part of the code reaches into directly, and a deploy that can’t go out because one unrelated change is broken. At that point, every release carries the risk of the whole system, and every new engineer has to understand the whole codebase before they can safely touch a small part of it.
That outcome isn’t caused by choosing a monolith. It’s caused by choosing a monolith and not enforcing any internal structure. The fix usually isn’t a rewrite into services — it’s discipline inside the single deployable unit you already have.
The option most teams skip: a modular monolith
A modular monolith is one deployable application with enforced internal boundaries: separate modules for billing, users, notifications, whatever your domains are, each with its own interface, its own tables, and a rule that other modules only reach it through that interface, never through a shared table or a shortcut import. Nothing crosses a module boundary except through code you can see and review.
This gets you most of what people actually want from microservices — clear ownership, the ability to reason about one part of the system without reading all of it, and a codebase that could be split apart later if it needs to be — without the operational cost of running and monitoring several services, or the network calls that turn a function call into a potential failure point.
It also gives you a much better position to split from later. If a module has clean boundaries and its own data, pulling it out into its own service is a mechanical, bounded piece of work. If your modules have been quietly sharing tables and calling each other’s internals for two years, the “quick split” turns into a rewrite regardless of which architecture you started with.
Signals that a service is actually worth splitting out
Rather than deciding architecture up front on principle, it’s worth watching for specific pressures that a monolith genuinely can’t absorb:
One component has a resource profile wildly different from the rest — for example, a video processing or transcription job that needs GPU time and would otherwise force every instance of your web app to carry that capacity.
One part of the system needs to scale independently and by a large factor — an ingestion endpoint taking spiky traffic from thousands of IoT devices, sitting next to an admin dashboard used by five people.
Teams working on different modules are blocking each other’s releases, and the coupling is organisational rather than technical — two groups genuinely need to ship on different schedules.
A component has materially different reliability or compliance requirements — for example, payment handling that needs tighter isolation and audit logging than the rest of the product.
You need to run a piece of logic in a different location entirely, such as on-device or at the edge, rather than centrally.
If none of these apply, the cost of a second deployable is not yet buying you anything. If one or two clearly do, splitting that specific piece out — not the whole system — is usually the right scope for the change.
How we think about it on our own products
We build and run several of our own products alongside client work, which means we carry the operational cost of whatever we choose. In practice that pushes us towards starting new products as a single, well-structured application, and only pulling a component into its own service when one of the signals above shows up concretely — a transcription pipeline with different compute needs than the rest of the app, or a device-facing ingestion path that needs to scale independently of the parts humans interact with. It’s a decision made against a specific pressure, not against a diagram of what a “proper” architecture is supposed to look like.
The question isn’t “monolith or microservices”. It’s “what, specifically, is a single deployable unit failing to do for us right now” — and if you can’t answer that concretely, you don’t have the problem yet.
A checklist before you split anything
Before pulling any component into its own service, it’s worth being able to answer each of these:
What specific limitation are we hitting today — not one we expect to hit eventually?
Does the team have the capacity to own an extra deployment pipeline, an extra set of dashboards, and an extra on-call surface?
Have we enforced module boundaries inside the monolith already, so the split is mechanical rather than a rewrite?
Is the boundary we’re proposing aligned with a real domain (billing, notifications, device ingestion), not just a technical layer like “the API” or “the database”?
What happens to a user-facing request if the new service is briefly unreachable — and is that acceptable?
If most of those answers are comfortable, the split is probably worth doing. If you’re reaching for microservices because of what the architecture is supposed to say about the company, rather than a limitation you can point to, a well-organised monolith will very likely serve you better, for longer, than the alternative.