Menu
AgiCAD Home
Architecture April 17, 2026 · 13 min read

Microservices vs Monolith: When to Split and When to Stay Together

The architecture debate that every growing engineering team eventually faces — and the honest framework for making the right call for your situation, not for the tech community's preferences.

The microservices debate has a peculiar quality in the engineering community: nearly everyone has a strong opinion, and the opinions mostly align with whatever architecture the speaker is currently working with. Engineers in large microservices organisations evangelise the benefits of independent deployment and team autonomy. Engineers who have lived through the operational complexity of distributed systems advocate for monoliths with quiet intensity.

At AgiCAD, we have built and maintained systems across the full spectrum — from single-process Django monoliths serving millions of requests, to distributed systems with dozens of services coordinating across message queues and service meshes. This is our honest assessment of when each approach serves teams well, and when the wrong choice creates years of unnecessary friction.

Defining the Terms Clearly

A monolith is a single deployable unit containing all the functionality of the application. It is not, by definition, a big ball of mud — well-structured monoliths with clear internal boundaries are common and entirely maintainable at substantial scale. "Monolith" describes deployment topology, not code quality.

A microservices architecture decomposes the application into independently deployable services, each responsible for a bounded domain — typically running in its own process, communicating over a network (HTTP/gRPC/message queue), and potentially deployed on separate infrastructure. The independent deployability is the defining characteristic.

Between these poles sits the modular monolith: a single deployable unit with strong internal module boundaries enforced at the code level, which could be split into services later if needed without requiring massive rewrites. This is often the most practical architecture for growing teams that are not yet at microservices scale.

The Case for the Monolith

Simplicity Compounds

A monolith has one deployment pipeline. One observability setup. One language runtime to manage. One database to back up. When something breaks at 2am, you look in one place and you have one codebase to step through with a debugger. This operational simplicity is not a small thing — it is the foundation on which everything else in the engineering organisation rests.

Distributed systems introduce a category of failures that simply do not exist in a monolith: network partitions, service discovery failures, cascading timeouts, partial failures where some services are degraded but others are not, and distributed transaction coordination. Every one of these is manageable with the right tooling and expertise. But they represent a genuine complexity tax that your team pays on every incident, every deployment, and every new hire who needs to understand how the system works.

Database Transactions Are Free

One of the most underappreciated advantages of the monolith is trivial access to database transactions. In a monolith, keeping two tables in sync is a BEGIN TRANSACTION / COMMIT pair. In a microservices architecture where those tables live in different services with different databases, you need a distributed transaction protocol — two-phase commit, the saga pattern, or an outbox pattern — each carrying significant implementation complexity and failure modes. For business logic with consistency requirements, this matters enormously.

The Modular Monolith Is Not a Compromise

The false choice is "messy monolith or clean microservices." A well-structured monolith with clear internal module boundaries — strict separation between domains, no circular dependencies, explicit interfaces between modules — provides most of the code-level benefits of microservices (independent development of modules, clear ownership, limited blast radius of changes) without the operational overhead of service-to-service communication.

Django's app structure, Rails' engines, Java's modules, Go's packages — every mature framework has conventions for structuring large codebases as collections of loosely coupled modules that happen to deploy together. These conventions exist precisely because this architecture serves the majority of applications at the majority of scales.

The Case for Microservices

Independent Deployability at Scale

The primary genuine benefit of microservices is the ability to deploy changes to one service without deploying the entire application. In a monolith with a single deployment pipeline, a change to the payment module requires deploying the entire application — including the authentication module, the user profile module, and everything else. At large scale with dozens of engineers merging code simultaneously, this creates deployment bottlenecks and release train coordination overhead that genuinely reduces engineering velocity.

For organisations with 50+ engineers across multiple teams, the ability for the payments team to deploy three times a day without coordinating with the recommendations team is a meaningful productivity gain. Below this scale, the same benefit is achievable with good continuous deployment tooling and disciplined branching strategies.

Independent Scaling

If your image processing service needs 50x the compute resources of your user authentication service, microservices allow you to scale them independently. In a monolith, you scale the entire application — wasting resources on the low-compute modules when you need to scale the high-compute ones.

This matters in practice for products with genuinely heterogeneous compute profiles — compute-intensive ML inference next to lightweight API endpoints, or high-throughput real-time data processing next to occasional batch jobs. It matters much less for typical CRUD-heavy web applications where the compute profile is relatively uniform across features.

Technology Heterogeneity

Microservices allow different services to be written in different languages and frameworks. Your ML inference service can be Python; your high-performance data processing service can be Go; your customer-facing API can be TypeScript. This is genuinely useful when different problems have different optimal technology choices.

It is also a significant source of operational complexity: different runtimes, different deployment pipelines, different observability tooling, different team expertise requirements. The right answer here depends heavily on your team's composition and whether the performance gains from using the optimal language for each problem justify the operational cost.

The Decision Framework

Our practical framework for making the monolith vs. microservices decision involves four questions:

1. How many engineers do you have?

Below 20 engineers, a well-structured monolith almost always outperforms a microservices architecture. The coordination overhead of distributed systems — deployment pipelines, service discovery, distributed tracing, inter-service authentication — requires infrastructure and operational investment that small teams cannot sustain without it becoming the dominant activity. Between 20 and 50 engineers, the calculus starts to shift, and team topology begins to matter more than team size alone. Above 50 engineers, microservices benefits become more concrete and the costs more justifiable.

2. Do you have genuinely independent deployment needs?

Do different parts of your system have fundamentally different deployment cadences, reliability requirements, or team ownership? If your payments service needs 99.99% uptime and your marketing content service needs 99%, these are genuinely different reliability profiles that might justify separation. If everything deploys together and fails together at the moment, splitting does not change that without significant investment in decoupling and resilience engineering.

3. Can you draw clean domain boundaries?

The most common microservices failure mode is splitting a monolith along the wrong boundaries — creating services that are tightly coupled at the data level, requiring synchronous calls between services for every request, and resulting in a distributed monolith that has all the operational overhead of microservices with none of the independence benefits. If you cannot articulate clean boundaries between domains with minimal cross-cutting data, do not split.

4. Do you have the platform infrastructure to support it?

Microservices require investment in platform infrastructure that monoliths do not: service discovery (Consul, Kubernetes DNS), distributed tracing (Jaeger, Honeycomb), centralised logging (ELK stack, Loki), circuit breakers, API gateways, and service mesh configuration for advanced routing and observability. Without this infrastructure, a microservices architecture is opaque when things go wrong — and things will go wrong. Building and maintaining this platform is a full-time engineering concern.

Migrating from Monolith to Microservices: The Strangler Fig Pattern

If you have decided that your monolith needs to be decomposed, the Strangler Fig pattern — named by Martin Fowler after the tropical tree that grows around and eventually replaces its host — is the most reliable migration strategy.

The approach: rather than a "big bang" rewrite, extract one domain at a time. Identify the clearest boundary in your monolith — the module with the fewest data dependencies on other modules — and extract it as a service. Route traffic to the new service, run it in parallel with the monolith version for a period to verify correctness, then retire the monolith code for that domain. Repeat for the next module.

This incremental approach has several advantages: risk is bounded to one service at a time, you can validate your service boundaries against real usage patterns before committing to them, and the team builds distributed systems expertise progressively rather than all at once on a single large migration.

The anti-pattern to avoid is the parallel complete rewrite: running two full systems simultaneously in the hope of cutting over at a single point. This approach doubles operational complexity, delays delivering value, and typically results in the new system inheriting the problems of the old one because insufficient understanding of the domain drove the original design.

Our Recommendation

Start with a well-structured monolith. Invest in clean internal boundaries and strong module separation. Deploy frequently. Build good observability into the monolith. As your team grows and genuine deployment independence or scaling heterogeneity needs emerge, extract services incrementally along the boundaries you have already established.

The teams that successfully operate microservices architectures almost universally built them by extracting from a working monolith — not by designing them top-down from the start. The extraction process reveals what the real boundaries should be, in ways that architectural design sessions alone cannot.

If you are starting a new project today with a team of 5–15 engineers, the architecturally correct decision is almost certainly a modular monolith. Save the microservices complexity for the problems that actually require it. Your future self, debugging a production incident at midnight with full database transactions and a single-process stack trace, will thank you.


Have questions about your architecture? Get in touch with the AgiCAD team — we help development teams navigate exactly these decisions.