Why Early-Stage SaaS Usually Needs a Modular Monolith, Not Microservices

There is a point in many SaaS products where microservices begin to feel like the responsible next step.
The product is no longer a toy. There are real customers, or at least serious pilots. The codebase is growing. Background jobs appear. Permissions get more awkward. Integrations stop being a nice edge detail and start becoming part of the daily workload. The team can feel that structure matters now, and that instinct is usually correct.
What is less obvious is which kind of structure helps at that stage, and which kind mostly makes the next year harder.
Microservices tend to arrive in the conversation dressed as maturity. Independent services sound clean. Independent deployment sounds freeing. Independent scaling sounds like preparation for success. If the team has spent enough time around modern architecture content, the single deployable application can start to feel almost embarrassing, like something provisional that should have been left behind already.
I usually think that instinct is early.
Most early-stage SaaS products are not mainly constrained by scale. They are constrained by fuzzy boundaries, shifting workflows, limited engineering capacity, and the cost of keeping the system understandable while the product is still teaching the team what it really is.
That distinction matters more than it seems.
If the real problem is that the team does not yet know where the domain boundaries truly are, splitting the system into services does not solve the uncertainty. It hardens it. A guess becomes an API. A rough line in the architecture diagram becomes a deployment unit. A workflow that is still changing becomes a distributed interaction, with retries, logging, versioning, partial failure, trace correlation, and all the other little taxes that distributed systems charge with perfect indifference.
That is how architecture turns into infrastructure around unfinished thinking.
A modular monolith is often a better starting point because it keeps the system close enough to understand while still forcing some discipline into the code.
By that, I do not mean a large application where every part can reach into every other part and the only real boundary is the folder tree. That is just a monolith with better manners. I mean one deployable application organised around actual modules: customers, billing, documents, notifications, reporting, integrations, workflow, audit evidence, or whatever the product genuinely needs.
Each module should own something real. It should expose a small surface. It should hide its internal details. Other parts of the application should not treat its data model as common property. The database should not become a social space where every module casually rearranges everyone else’s furniture.
That is the part people sometimes miss when they hear the word monolith. A monolith is only a problem when coupling is cheap and nobody takes boundaries seriously.
For an early team, the practical advantage is hard to ignore. One deployable unit is simpler to run. Local development is simpler to reproduce. A production issue is easier to trace. A feature that touches several parts of the business can still be changed in one place, in one commit, without coordinating a small parade of services and contracts.
That is not a lack of ambition. It is oxygen.
Early SaaS teams need room to learn. Customers reveal awkward truths slowly. The workflow that looked obvious in month two can look naive in month six. The module that seemed secondary can become central. The clean conceptual boundary from the first diagram often turns out to be more porous in actual use. A compliance-heavy product, for example, may start as a neat set of nouns like policies, controls, evidence, reviews, and frameworks. Then real usage shows that ownership, traceability, review loops, background processing, and exception handling matter more than the nouns suggested.
Those things tend to emerge from use, not from architectural self-confidence.
A modular monolith gives you a way to learn without pretending the learning is finished.
It also preserves a believable extraction path later.
If a module inside the monolith has real boundaries, clear ownership, and enough internal cohesion, pulling it out later becomes a practical engineering decision rather than a rescue mission. That is a much better position than starting with services and discovering that what looked like service boundaries were really just premature packaging decisions around one still-evolving workflow.
This is the real reason I prefer the modular monolith path. It is not because microservices are bad. It is because they are expensive in ways that early teams often underestimate.
Every service wants deployment, configuration, secrets, health checks, logs, metrics, alerting, migrations, test fixtures, and local-development support. Then the team needs habits for debugging failures that move across process boundaries. Then product changes that cross several services become heavier to ship, even when each individual service looks neat in isolation.
Large teams can justify that. Products with genuinely different scaling profiles can justify that. Security isolation can justify that. Very noisy integrations can justify that. A document-processing pipeline that deserves a separate runtime might justify that. A stable, high-pressure subsystem with a distinct operational profile might justify that.
But that is the point: the justification should come from pressure the system has actually shown.
The mistake is treating the destination as the starting line.
The opposite mistake exists too, of course.
A monolith can rot. Modules can become ceremonial. Shared code can turn into an unlabeled junk drawer. Business logic can leak into controllers, jobs, handlers, helpers, and little convenience layers that nobody owns properly. Teams can claim to have modular boundaries while bypassing them every time a deadline becomes uncomfortable.
When that happens, the problem is real. But in my experience, the main cause is usually unmanaged coupling, not the fact that the system deploys as one unit.
That is why the word modular has to earn its place.
A useful modular monolith needs friction in the right places. It needs dependency rules that are more than good intentions. It needs code reviews that care about boundaries, not only about whether the feature works. It needs tests around important behaviour. It needs someone to occasionally ask whether the module structure still reflects the product that actually exists now, not the one the team imagined six months earlier.
The goal is not architectural purity. It is keeping change affordable.
That is usually the right question for an early-stage SaaS product. Can we change a workflow without touching half the system? Can we diagnose production issues without archaeology? Can a new engineer tell where behaviour belongs? Can we defer operational complexity until it starts paying rent? Can we keep moving without letting the codebase dissolve into a polite version of mud?
If the answer is mostly yes, the architecture is doing its job.
Microservices have their place. I do not see them as a mistake in general, only as something teams often buy before they need the bill. When the product has stable boundaries, when different workloads genuinely want different runtime treatment, when team ownership becomes a scaling problem of its own, or when isolation requirements are no longer optional, separate services can make perfect sense.
I just do not think most early SaaS products begin there.
They usually need something more modest and more demanding at the same time: a system simple enough to run, structured enough to grow, and honest enough to show where its real boundaries are still forming.
That is usually a modular monolith.