SaaS PlatformBy Abdul Rahman08 Sept 20262 min read
Modular Monolith vs Microservices: Choosing an Architecture for a Growing SaaS
Microservices are not a default. Why most SaaS products should start as a well-structured modular monolith, and the signals that tell you it's time to split.

For a decade, "microservices" was the answer to almost every architecture question. Many teams have since learned that splitting too early trades application complexity for distributed-systems complexity — network failures, eventual consistency, versioned contracts and a much larger operational burden.
For most SaaS products, the better starting point is a modular monolith.
One deployable unit, with modules that communicate through explicit interfaces — ready to be extracted later.
What Is a Modular Monolith?
A modular monolith is a single deployable application whose code is divided into well-bounded modules — for example accounts, billing, academics and notifications. Each module:
- owns its data (its own tables or schema),
- exposes a small public interface,
- never reaches into another module's internals or tables directly.
You get most of the organisational benefits of microservices — clear ownership, independent reasoning — with one deployment, one database transaction boundary and in-process calls.
Why Start Here
- Speed. One repository, one pipeline, one place to debug.
- Consistency. Database transactions just work; no sagas for simple operations.
- Refactorability. Moving a boundary is a code change, not a migration between services.
- Lower cost. Fewer moving parts to host, monitor and secure.
Enforce the Boundaries
A modular monolith only works if boundaries are real. Practical techniques:
- Public entry points per module (
billing/index.ts) and lint rules that forbid deep imports. - Separate schemas or table prefixes per module, with no cross-module joins.
- Domain events for cross-module reactions instead of direct calls:
// billing module
events.publish("subscription.activated", { tenantId, plan });
// notifications module
events.subscribe("subscription.activated", async ({ tenantId }) => {
await sendWelcomeEmail(tenantId);
});
When a module later becomes a service, the event becomes a message on a queue — the rest of the code barely changes.
Signals It's Time to Extract a Service
Split a module out when there's a concrete reason, such as:
- Different scaling needs — a report generator or media processor that needs far more CPU than the rest.
- Different reliability or security requirements — payment processing isolated for compliance.
- Independent team ownership — a team blocked by others' release cycles.
- Different technology — a machine-learning component better written in Python.
"It feels cleaner" is not on the list.
Operational Readiness Before Microservices
Before running many services, make sure you have centralised logging, distributed tracing, automated deployments, service discovery and clear API versioning. Without these, microservices multiply incidents rather than reducing them.
Key Takeaways
- Start with a modular monolith: clear modules, owned data, explicit interfaces.
- Enforce boundaries with entry points, lint rules and separate schemas.
- Communicate across modules with events to ease future extraction.
- Extract services for scaling, compliance, team or technology reasons — not fashion.


