Monolith vs microservices in short
A monolith is one application built and deployed as a single unit, with modules that call each other in the same process. Microservices split the system into separately deployed services that each own their data and talk over the network.
For most new products, a well-structured monolith is the cheaper starting point. Splitting pays off when specific pressures appear, which the last section lists.
Neither style is better in general. A monolith concentrates coordination in one codebase, while microservices move it into the network, the deploy process and data consistency. The main differences:
- Deployment: one artifact, versus many independent release pipelines
- Calls between modules: in-process function calls, versus network requests that can fail or time out
- Data: one database with joins and transactions, versus a database per service
- Scaling: run more copies of the whole app, versus scale each service on its own
- Teams: a shared codebase and release, versus a service owned by each team
Deployment, scaling and failure
A monolith has one build, one deploy and one thing to roll back. Calls between modules are function calls, so they are fast and can't fail halfway across a network. The downside is coupling at release time: every change ships together, and one bad change or memory leak can take down every feature.
Microservices let each team deploy on its own schedule and scale only the busy part, such as image processing, without copying the whole application. In exchange, every call between services can be slow, time out or fail. Each one needs timeouts, retries and a plan for when the other side is down.
Service APIs also become contracts. Changing a response field means coordinating versions with every calling service, much like changing a public API.
Data ownership and consistency
In a microservice design, each service owns its data, and other services reach it only through that service's API or events. That ownership is what makes independent deployment possible, because no other team's queries depend on your table layout.
You lose joins and transactions across services. An order flow that reserves stock, charges a card and creates a shipment can't be one database transaction anymore. It needs a saga: a sequence of local transactions with compensating steps, such as releasing the stock if the charge fails. Until the last step finishes, the services can disagree about the order's state.
Services that share one database keep the network costs of microservices and the coupling of a monolith. This is often called a distributed monolith, and it is the outcome to avoid.
Team boundaries and operational cost
Conway's law observes that systems tend to mirror the communication structure of the organizations that build them. Microservices work best when each service matches a team that owns it end to end, from code to on-call. With one small team, splitting adds coordination work without a matching benefit.
Each service also brings its own operating work:
- A build and deploy pipeline per service, plus versioning between services
- Monitoring, logs and alerts for each service, and distributed tracing to follow one request across several
- Service discovery, load balancing and authentication between services
- Local development that needs several services, or stubs of them, running at once
- Contract tests that catch breaking API changes before a deploy
The modular monolith as a middle path
A modular monolith is one deployable application split into modules with enforced boundaries. Each module exposes a public interface, keeps its internals private and owns its own tables. Other modules call that interface and never touch its tables directly.
This keeps in-process calls, a single deploy and database transactions, while giving you most of the design discipline of services. Boundaries can be enforced with lint rules on imports, separate packages in a monorepo or the module visibility rules of your language.
It also makes a later split cheaper. A module that already owns its data and talks through an interface can become a service by replacing in-process calls with network calls. Replacing a system piece by piece while the rest keeps running is known as the strangler fig pattern.
Signals that justify splitting a service out
Split when you can name the specific problem a separate service solves. A messy codebase is a weak reason, because splitting tangled code produces tangled services with network calls between them.
Signals that usually justify extracting a service:
- One module needs different resources or scaling from the rest, such as CPU-heavy media processing next to a light web API.
- Part of the system needs stronger isolation for security or compliance, such as payment card handling.
- Teams regularly block each other's releases, and the module boundary between them is already clean.
- One component fails in ways that take down unrelated features, and isolating it would contain the damage.
- A module needs a different runtime or language for a concrete technical reason.