Contents
At the start you should almost always choose a monolith — ideally a modular one with clear boundaries inside a single application. Microservices solve the problems of large teams and uneven load that a new product does not have yet, but you pay for them immediately in infrastructure, debugging effort and delivery speed. Split into services when a concrete reason appears.
Let us look at how the two approaches differ in practice, when microservices are justified and how to build a monolith you can split later.
How does a monolith differ from microservices?
A monolith is a single application with one codebase and usually one database that is built and deployed as a whole. Microservices are a set of independent applications, each with its own responsibility, its own data and its own release cycle, talking to each other over the network through APIs or message queues.
The difference is not size but where the boundaries sit. In a monolith, modules call each other like ordinary functions — fast and within one transaction. With microservices, every call is a network request that can fail, time out or arrive twice.
| Criterion | Monolith | Microservices |
|---|---|---|
| Speed to start | High | Low: infrastructure needed from day one |
| Deployment | The whole application | Each service separately |
| Transactions | Ordinary, in one database | Distributed: sagas, compensations |
| Debugging | One log, one stack trace | Tracing requests across services |
| Scaling | The whole application | Only the busy services |
| Team independence | Shared code and release | Each team owns its services |
| Infrastructure cost | Minimal | Higher: orchestration, monitoring, networking |
Why do microservices slow a new product down?
Microservices slow a new product down because service boundaries have to be drawn before the domain is understood. A new product changes every week, and a boundary drawn in the wrong place turns into constant coordinated changes across several services.
What else arrives on day one:
- contracts between services and their versioning;
- handling network errors, retries and timeouts;
- data consistency without a shared transaction;
- separate build and deployment pipelines per service;
- centralised logs, metrics and tracing, without which you cannot find a bug.
All of this is needed just to make the system work, and none of it brings the product closer to users. Our guide to choosing an MVP backend reaches the same conclusion: microservices at the start are one of the most expensive mistakes.
Photo: Unsplash
When are microservices actually justified?
Microservices are justified when the system has real reasons to split: several teams getting in each other's way in one codebase, or parts of the product needing different scaling or reliability. The reason has to be concrete, not "for the future".
Signs a separate service will pay off:
- Several teams work on the product and the shared release has become a bottleneck.
- One part of the system is an order of magnitude busier than the rest — media processing or recommendations, for example.
- One part has different reliability or security requirements — the payment flow, personal data processing.
- Some logic is better written in another stack: machine learning in Python next to a PHP backend.
Even then, you extract one service with a clear boundary rather than everything at once, and check whether it pays off.
What is a modular monolith and why is it good for a start?
A modular monolith is one application divided into modules with explicit boundaries: each module has its own data and public interface, and neighbours are not allowed to reach into its internals. You get the delivery speed of a monolith while preparing the ground for extracting services if you ever need to.
An example backend structure where each module owns its area:
app/
Modules/
Catalog/ — products, categories, prices
Contracts/ — the module's public interface
Models/
Services/
Orders/ — cart, orders, statuses
Payments/ — payments, webhooks, refunds
Users/ — sign-up, profiles, roles
Rules that keep boundaries intact
A module boundary only holds if it is checked automatically. An agreement to "stay out of other modules' models" breaks at the first deadline.
- modules talk through interfaces or events, not through each other's models and tables;
- every table has one owning module; others read its data through that module's API;
- dependencies between modules are checked in CI with architecture tests;
- events inside the monolith are shaped the way queue messages would be.
// Orders do not touch the payments table; they ask the payments module through a contract
final class OrderService
{
public function __construct(private PaymentGateway $payments) {}
public function pay(Order $order): PaymentStatus
{
return $this->payments->charge($order->id, $order->total);
}
}
If payments later become a separate service, the PaymentGateway implementation switches from a function call to an HTTP request, and the order code stays the same.
How do you know it is time to split the monolith?
It is time to split the monolith when you can name a concrete problem that extracting a service will solve, and measure it. "The codebase got big" is not a reason: a big codebase is fixed with modules and refactoring.
What usually triggers the split:
- one team's deployment regularly blocks another's;
- a heavy module slows the whole application, and scaling it alone is cheaper than scaling everything;
- a failure in one part regularly takes the whole system down;
- audit or security requirements call for isolating part of the data.
Services are extracted gradually: first the module is brought to clean boundaries inside the monolith, then moved into its own application, and the old code is switched to a network call. This is often called the strangler fig pattern: the new system gradually replaces the old one without a big rewrite.
In short
- For a new product choose a monolith: it is faster, cheaper and easier to debug.
- Make the monolith modular: boundaries, data owners and dependency checks in CI.
- Microservices solve the problems of large teams and uneven load — wait until you have them.
- Extract services one at a time, and only for a concrete, measurable reason.
FAQ
Can a monolith handle high load?
Yes. A monolith scales horizontally with several copies behind a load balancer. The bottleneck is usually the database, and indexes, caching and read replicas address it regardless of architecture.
Do microservices require Kubernetes?
Not necessarily: a few services can run on Docker Compose or managed cloud services. Kubernetes pays off when there are many services and you need autoscaling and self-healing.
How many services should a microservice architecture have?
As many as the system has independent areas with different teams or requirements. There is no right number; tiny services that always change together are a sign of poor boundaries.
Can a monolith's database be split gradually?
Yes. First assign an owning module to each table and remove direct access to other modules' tables, then move the module's tables into a separate database. Skip the first step and the second becomes a rewrite.



