Microservices vs Monolith: Lessons from 26 Years of Builds

Microservices solve problems of scale and team size. If you don't have those problems yet, you pay the costs without the benefits. Here is what early microservices cost, the service-ready modular monolith I recommend instead, and the signals that tell you it's time to split.
Most startups split too early
Microservices are the default answer in too many architecture meetings. A three-person team with no paying users draws a diagram with twelve services, a message queue and a Kubernetes cluster, because that's how the big tech companies do it.
In 26+ years, I've built both: classic monoliths in PHP and Laravel, and microservices platforms in Spring Boot, Node.js and Nest.js. The lesson is consistent. Microservices solve problems of scale and team size. If you don't have those problems yet, you pay the costs without getting the benefits.
This post covers what early microservices cost, the middle ground most startups should start with, and the signals that tell you it's time to split.
What early microservices cost a small team
Every service you add brings work that has nothing to do with your product:

- More things to run. Each service needs its own deployment pipeline, monitoring, logs, alerts and scaling rules.
- Network calls instead of function calls. A simple feature now crosses several services, each call can fail or slow down, and you need retries and timeouts everywhere.
- Data split across databases. Reports and joins that took one query now need several APIs or a separate data pipeline. Keeping data consistent across services is hard.
- Slower debugging. One user complaint means tracing a request through many services and log streams.
- Higher cloud bills. Many small services, each with its own containers, databases and load balancers, cost more than one well-run application at low traffic.
- Change gets harder, not easier. Early on, you don't know where the right boundaries are. Splitting in the wrong place means every new feature touches several services and needs coordinated releases.
For a team of three to ten engineers, this overhead can eat a large part of every sprint.
The middle ground: a modular monolith
A modular monolith is one application, deployed as one unit, but built as clearly separated modules. It gives you most of the structure of microservices with almost none of the operating cost.
- Clear module boundaries. Payments, users, orders, notifications: each module owns its code and its tables.
- Modules talk through defined interfaces, never by reaching into each other's data. This is the rule that makes a later split easy.
- One deployment, one database, with separate schemas or table prefixes per module.
- Background jobs and queues for slow work, so the main application stays fast without becoming distributed.
When a module later needs to scale, deploy or change on its own, you lift it out along a boundary that already exists. That's a planned extraction, not a rewrite.
The hybrid approach I recommend: a service-ready modular monolith
The best of both worlds is to run as one application today, but build every module as if it might become a service tomorrow. You get the speed and low cost of a monolith now, and a clean, low-risk path to microservices the day you have a real need.
What that looks like in practice:
- One codebase, many modules. Organise the code as a monorepo of self-contained modules, such as Nest.js modules, Spring Modulith modules or Laravel packages. Each has its own folder, tests and public interface.
- Modules talk through contracts, not shortcuts. Calls go through a defined interface or API, with shared request and response types. No module reads another module's tables.
- A schema per module. One database server, but each module owns its own schema. Splitting later means moving a schema, not untangling joins.
- Events through an in-process event bus. Modules publish events like "order placed" behind an interface. Later, you swap the in-process bus for a real queue (such as RabbitMQ, Kafka or a cloud queue) without changing the module code.
- One API gateway from day one. Clients call the gateway, not the modules. When a module becomes a service, the gateway routes to it and no app or partner integration changes.
- Deployable as one, or many. Package the app in containers, and make each module able to start on its own through configuration. Today it runs as one process; tomorrow the same code runs the payments module as its own service.
- Observability built in. Request IDs, structured logs and tracing across modules from the start, so the first split doesn't leave you blind.
| Day one: modular monolith | When a real need appears: extract a service | |
|---|---|---|
| Deployment | One app, one pipeline | Same code, the module deployed on its own |
| Data | One database, a schema per module | That module's schema moves to its own database |
| Communication | Function calls behind interfaces; in-process events | Same interfaces over HTTP or gRPC; events over a queue |
| Client access | Through the API gateway | Still through the gateway, now routed to the new service |
| Team | One team owns everything | A team owns the extracted service |
The extraction then follows a simple path: put the module behind its interface (already done), give it its own database, deploy it separately, route traffic to it through the gateway, and remove the old in-process wiring. Because the boundaries existed from day one, it's a planned project of routing changes and a data migration, not a months-long rewrite.
From monolith to microservices, one step at a time
Most products move through three stages. The mistake is jumping straight to the last one.

In practice, many successful products never go past a modular monolith with two or three extracted services. That's a sign of good architecture, not a lack of ambition.
How I've split real systems
On the microservices platforms I've architected, each service split had a specific reason. None was split just to follow a pattern.
| Platform | Services that earned their own deployment | Why they were split |
|---|---|---|
| EV charging management SaaS (Nest.js) | Charger communication (OCPP), billing, load balancing | Thousands of chargers send constant status messages, which must scale separately from billing and apps |
| Food delivery SaaS (Nest.js) | Ordering, kitchen, delivery routing | Order spikes at meal times and festivals hit ordering far harder than the rest of the platform |
| iGaming talent marketplace (Node.js) | Payments and escrow, KYC, matching | Money and identity checks need strict security and audit trails, kept apart from fast-changing product features |
| Exam preparation platform (Spring Boot) | Content, testing and analytics | Exam-day traffic on mock tests needs to scale without slowing content management |
The pattern: split where load, security or release speed is genuinely different from the rest of the system. Everything else stays together until it has its own reason to move.
Ops costs to budget for
If you do move to microservices, budget for the platform work around them, not just the services:
- Container orchestration (Kubernetes or a managed container service) and someone who knows how to run it.
- Centralised logging, tracing and monitoring across every service.
- An API gateway, service-to-service security and secrets management.
- A message queue or event bus for work that crosses services.
- Automated testing and deployment pipelines for each service.
Where you deploy matters too. Customers in regulated markets often expect their data to stay in their region, and each extra region multiplies the infrastructure you run.

| Region | Common cloud regions | Why it matters |
|---|---|---|
| Europe | Frankfurt, Ireland, London | GDPR and customer expectations of EU or UK hosting |
| Middle East | UAE, Bahrain, Saudi Arabia | PDPL rules and data residency for regulated clients |
| Africa | Cape Town, Johannesburg | POPIA, and lower latency for African users |
| India | Mumbai, Hyderabad | DPDP Act and latency for Indian users |
| South East Asia | Singapore, Jakarta | Regional hub, plus local data-localisation rules in some countries |
A monolith deployed in two regions is far simpler to run than twenty services deployed in two regions.
Before you split: a checklist
- You can name the specific problem the split solves: load, security, team ownership or release speed.
- The module already has a clean boundary and its own data.
- You have centralised logs, monitoring and tracing in place.
- Deployments are automated and tested.
- A team or person will own the new service.
- You've estimated the added cloud and maintenance cost per month, in USD.
- You've planned data residency for each region you serve.
Not sure your architecture fits your stage?
With 26+ years of building platforms, I help founders choose the architecture that fits their team and growth, and plan when and how to split.
About Vikram Malihan
Fractional CTO with 26+ years of experience building and scaling technology teams across multiple continents. Specializing in helping startups and scale-ups navigate technical challenges, from architecture decisions to team building and fundraising support.
Learn more about Vikram
