Short answer: for most projects the right starting point is a well-structured monolith, often called a modular monolith. Microservices make sense when several teams need to release independently, when parts of the system have very different scaling needs, or when some domains must be kept separate. Without those conditions, moving to microservices multiplies operating costs without solving a real problem.
What are a monolith and microservices?
A monolith is an application built from a single codebase and deployed as one unit; orders, inventory and invoicing run in the same process and usually share one database. In a microservices architecture each business domain is its own service with its own code, its own database and its own release schedule, and services talk to each other over the network through API calls or message queues.
There is a third option, and it is often the most sensible one: the modular monolith. The code ships as one unit, but inside it modules have clear boundaries; one module never touches another's tables directly and only talks through a defined interface. If a module ever needs to become its own service, the boundary is already drawn.
Microservices vs monolith: a comparison
- Development speed: a monolith is much faster early on; microservices need a contract between services for every new feature.
- Releases: a monolith has one pipeline; with microservices every service has its own pipeline, version and rollback plan.
- Scaling: a monolith is replicated as a whole; with microservices only the part under load is scaled.
- Debugging: in a monolith one log and one stack trace are enough; microservices require central logging and distributed tracing.
- Data consistency: a monolith can rely on one database transaction; with microservices cross-service consistency has to be designed.
- Operating cost: microservices need more servers, more monitoring tools and more DevOps effort.
Microservices are an organisational technique before they are a scaling technique: the problem they really solve is teams waiting on each other. A single-team project does not have that problem.
The hidden costs of microservices
Microservices look flexible on paper; the bill arrives later. What used to be a function call becomes a network call that can time out, slow down or fail halfway. A flow like "create the order, reduce stock, issue the invoice", which a monolith handles safely in one database transaction, has to be redesigned across three services with compensating steps.
- A separate pipeline, monitoring, alerting and backup setup for every service.
- Version compatibility between services: one API change can affect several services at once.
- Without distributed tracing and central logs, finding which service caused an error can take hours.
- Local development gets heavy; trying out one feature means starting five services.
As the number of services grows, automated testing and release pipelines become mandatory; a microservices system released by hand is unmanageable. We explain how to set up that infrastructure in what is CI/CD.
What does a modular monolith look like in practice?
In a modular monolith each business domain lives in its own folder and exposes only a small interface. In the TypeScript example below, the orders module never reads the inventory tables directly; it calls the function the inventory module publishes. If inventory moves to its own service tomorrow, only the implementation behind that interface changes.
// modules/inventory/index.ts — the module's only public door
export interface InventoryApi {
reserve(productId: string, qty: number): Promise<boolean>;
}
// modules/orders/createOrder.ts
import type { InventoryApi } from '../inventory';
export async function createOrder(
inventory: InventoryApi,
productId: string,
qty: number,
) {
const ok = await inventory.reserve(productId, qty);
if (!ok) throw new Error('Insufficient stock');
// the order is written only to the orders module's own tables
}This discipline can be enforced automatically with a lint rule that restricts imports between modules. That keeps the monolith from slowly turning into a "big ball of mud" where everything depends on everything.
When should you move to microservices?
- Teams working in the same codebase keep waiting on each other's releases, and it has become a chronic bottleneck.
- One module carries many times more load than the rest (reporting or image processing, for example), and scaling the whole system with it is expensive.
- One domain has different security, compliance or technology requirements, such as payment data or AI workloads.
- Module boundaries have settled, and which data belongs to whom is no longer up for debate.
The move is never done in one go. The healthiest path is to extract the module with the clearest boundary and the most pressure, shift traffic to the new service gradually, measure the result and only then move on to the next one. If you are starting a new product, it is wiser to prove the product first, as described in what is an MVP, than to make the architecture heavy from day one; for multi-tenant products we cover these decisions in how to build a SaaS product.
Conclusion
The answer to microservices vs monolith is not fashion; it is your team structure and load profile. For most companies the right order is: start with a modular monolith, keep the boundaries clean, and split out only the part that becomes a real bottleneck. In our custom software and corporate solutions projects, we build the architecture for today's needs while leaving room for tomorrow's growth. If you want to decide on the right architecture for your project together, get a quote.