Should your web app be a monolith or microservices? Compare costs, scaling, team fit, and a path that avoids a costly rewrite.
In this article
For most new products, start with a well-structured monolith and split out services only when a specific, demonstrated problem justifies the extra complexity. A monolith is a single deployable application; microservices split the system into many small, independently deployed services that communicate over a network. Microservices can help large teams ship independently and scale hot spots separately, but they add operational cost from day one. A modular monolith gives you most of the structure with far less overhead.
Scalability is often discussed as if it were only about traffic. In practice it covers three things: handling more users, handling more features without the code becoming unmanageable, and allowing more developers to work without blocking each other. The architecture you choose should address the constraint you actually have, not one you might have in three years.
What a monolith really is
A monolith runs as one process (or several identical copies behind a load balancer). All features share one codebase and, typically, one database. It is easy to run locally, easy to test end to end, and a single deployment updates everything.
"Monolith" is not a synonym for "messy". A modular monolith divides the code into clearly bounded modules, such as billing, accounts, orders and notifications. Each module has a defined public interface, owns its own tables, and does not reach into the internals of another. This discipline is what makes later extraction possible.
What microservices really are
In a microservice architecture each service owns a business capability and its data, is deployed on its own, and communicates through APIs or messages. Teams can choose different technologies per service, scale services independently and release them on separate schedules.
The cost is that you have turned in-process function calls into network calls. Network calls fail, time out and arrive out of order. You now need service discovery, distributed tracing, centralised logging, API versioning between services, and a way to handle transactions that span several databases. Much of this work belongs to the cloud and DevOps side of a project, and it must be budgeted for.
Comparison
| Concern | Modular monolith | Microservices |
|---|---|---|
| Initial build speed | Faster; one codebase, one pipeline | Slower; infrastructure and contracts first |
| Local development | Simple | Needs several services running or mocked |
| Deployment | One unit | Many units, each with its own pipeline |
| Data consistency | Normal database transactions | Eventual consistency, sagas, compensation logic |
| Scaling | Scale the whole app horizontally | Scale only the busy services |
| Failure isolation | A bug can affect everything | Better isolation if designed well |
| Team autonomy | Shared code needs coordination | Teams own services end to end |
| Operational load | Lower | Higher: monitoring, tracing, orchestration |
Signs a monolith is still the right choice
- One small team (roughly one team that can sit around a table) builds and owns the product.
- You are still discovering what customers want, so boundaries between features keep moving.
- A single relational database satisfies your reporting and consistency needs.
- You lack the monitoring and deployment automation that many services require.
When requirements are unsettled, wrong service boundaries are expensive. Moving code between modules in a monolith is a refactor; moving it between services is a migration project.
Signs it may be time to split
- Independent scaling needs. One component, such as image processing or report generation, needs very different resources from the rest.
- Team contention. Several teams keep blocking each other in one codebase and one release train.
- Different reliability requirements. A payments component must stay up even if a recommendation feature fails.
- Technology constraints. A capability genuinely needs a different runtime or language.
- Long deploy and test cycles that slow everyone, which better module boundaries and test strategy cannot fix.
Notice that each of these is a measurable problem you can point to. "Microservices are modern" is not one of them.
Principles that matter in either style
Keep state out of the application servers
If sessions or uploaded files live on one server's disk, you cannot add a second server safely. Store sessions in a shared store such as Redis, and files in object storage. Then any instance can serve any request, which is the basis of horizontal scaling.
Design the database deliberately
The database is usually the first real bottleneck. Index the queries you actually run, avoid fetching more than you display, and use read replicas or caching only when measurements show a need. Many products run happily on a single well-tuned PostgreSQL instance for a long time.
Move slow work off the request path
Sending emails, generating PDFs or calling third-party APIs should not block the user's request. Put the work on a queue and process it in a background worker. This one habit removes many apparent scaling problems.
Use clear contracts between modules
Whether modules talk through function calls or services talk through HTTP, define the interface explicitly and test it. Our article on REST vs GraphQL covers choosing the style for the external API.
Observe before you optimise
Add logging, metrics and error tracking early. Scaling decisions made without data are guesses.
A simple modular structure
Here is a folder layout for a Node.js and TypeScript modular monolith. Each module exposes only what other modules need through its index.ts:
src/
modules/
accounts/
index.ts // public interface
accounts.service.ts
accounts.repo.ts
billing/
index.ts
billing.service.ts
billing.repo.ts
orders/
index.ts
orders.service.ts
orders.repo.ts
shared/
db.ts
logger.ts
server.ts
A rule such as "modules may import only from another module's index.ts" can be enforced with a lint rule. When one module later needs to become a service, its boundary already exists and the calls through its interface can become network requests.
A path that avoids rewrites
- Build a modular monolith with explicit boundaries and one database schema per module (or clearly separated tables).
- Keep sessions and files outside the application servers.
- Introduce a queue and background workers for slow tasks.
- Add monitoring, tracing and alerts, and watch real usage.
- If one module becomes a proven bottleneck, extract that module as a service, leaving the rest alone.
- Repeat only when data justifies it.
This sequence is sometimes called the strangler approach: new services grow around the old system gradually, without a risky big-bang rewrite.
Common mistakes
- Starting with microservices for an unproven product. You pay the full operational cost before you have users to justify it.
- The distributed monolith. Services that must be deployed together, or that share one database, give you the downsides of both styles.
- Splitting by technical layer (a "database service", a "validation service") instead of by business capability.
- Chatty services. If one user action triggers a dozen calls between services, latency and failure rates multiply.
- No ownership. A service without a named owning team decays quickly.
- Scaling before measuring. Adding caches, replicas and services without profiling often moves the bottleneck without removing it.
- Forgetting the front end. A fast back end is wasted if pages are slow; see how rendering choices affect this in our rendering guide.
Planning an architecture with us
The right architecture depends on your team size, your growth expectations, your compliance needs and your budget for operations. We would rather help you start simple and evolve with evidence than sell you complexity. If you are planning a new platform or untangling an existing one, our web application development team covers API development, multi-tenant platforms and microservices architecture, and can advise on which fits your situation.
Share this article
General educational information, not legal or professional advice for your specific situation.
