Skip to main content
Resources Architecture 7 min read

Should You Build Microservices or a Monolith?

The microservices debate continues, but the answer depends on your situation. When each architecture makes sense, and why most teams should start with a monolith.

The microservices debate has calmed down from its peak hype, but teams still struggle with the decision. Should we break things into services? How small? When is a monolith actually better?

After years of watching teams succeed and fail with both approaches, patterns have emerged.

The Default Should Be Monolith

This isn’t a contrarian take designed to be provocative—it’s practical advice based on what actually works for the majority of applications. The industry went through a period of microservices enthusiasm that led many teams to adopt distributed architectures prematurely, and we’ve seen the consequences.

Monoliths are simpler in ways that matter. One codebase means one place to understand the system. One deployment means one thing to roll back when something goes wrong. One runtime means one set of logs to search when debugging. When something breaks, you know where to look—there’s only one place it can be. When you need to change something, you change it in one place and deploy one artifact. The cognitive overhead of understanding your own system remains manageable.

The complexity of microservices—network calls replacing function calls, distributed transactions or eventual consistency replacing database transactions, service discovery replacing direct references, deployment orchestration for multiple interdependent services—only pays off at certain scales and organizational structures. Most applications never reach that scale. Most teams never grow to the size where coordination overhead within a monolith becomes the primary bottleneck.

Starting with microservices is premature optimization in its most expensive form. You’re solving scaling problems you don’t have (and may never have) while creating coordination problems you definitely do have. The architectural flexibility that microservices provide is meaningful only when you actually need that flexibility—and building for flexibility you won’t use means paying complexity costs without receiving the benefits.

When Microservices Make Sense

Microservices have real advantages in specific situations. When these situations genuinely apply, the architecture’s complexity becomes justified rather than merely tolerated.

Different scaling requirements. When parts of your system have genuinely different load patterns, separate services let you scale them independently. The search indexer that needs 100x resources during batch processing shouldn’t scale with the user authentication service that needs consistent modest capacity. The video transcoding service that needs GPU instances shouldn’t be bundled with the API service that runs fine on commodity compute. When you’re paying cloud bills, the ability to scale components independently can translate into significant cost savings—but only if the scaling patterns are actually different enough to matter.

Different technology needs. If one component genuinely works better in a different language or framework, service boundaries let you use the right tool without forcing the entire system into one technology stack. Machine learning workloads might genuinely benefit from Python’s ecosystem. Real-time processing might perform better in Go. Data-heavy CRUD operations might be most productive in Rails. Service boundaries let each component use what works best for its specific problem domain. The key word is “genuinely”—using microservices to experiment with new technologies is a common trap that creates complexity without corresponding benefit.

Separate teams with clear boundaries. When distinct teams own distinct parts of the system, services can reduce coordination overhead in ways that matter. Team A deploys their service without waiting for Team B’s approval or testing cycle. The contract between services—documented APIs with versioning—is clearer and more enforceable than informal agreements about shared code. This benefit is real, but it only materializes when teams are truly independent. If teams still need to coordinate constantly for features that span services, you’ve added architectural complexity without actually reducing coordination cost.

Failure isolation. When one component failing shouldn’t take down others, service boundaries provide isolation that’s harder to achieve in a monolith. A bug in the recommendation engine shouldn’t crash the checkout flow. A memory leak in image processing shouldn’t affect the API. Service boundaries create blast radius containment that can improve overall system reliability—though this requires careful attention to how services handle failures in their dependencies.

The Hidden Costs

Teams underestimate what microservices require. The complexity isn’t just in building the system—it’s in operating, debugging, and evolving it over time.

Network isn’t free. What was a function call in a monolith becomes a network request in a distributed system. Latency that was measured in microseconds is now measured in milliseconds. Failures that were deterministic (the function either works or throws an exception) become probabilistic (the network might be slow, might drop packets, might be partitioned). Every service call needs explicit handling for timeouts, retries, circuit breakers, and fallback behavior. Debugging a problem that spans multiple services means correlating logs across multiple systems, each with their own timestamps and trace contexts. The mental model required to understand a distributed system is fundamentally more complex than understanding a single process.

Data consistency is hard. Transactions that were simple in a monolith—wrap everything in a database transaction and either it all commits or none of it does—become distributed transactions or eventual consistency patterns when data spans services. Distributed transactions are complex to implement correctly and often perform poorly. Eventual consistency is simpler to implement but harder to reason about and can create user experiences where the system appears to behave inconsistently. Neither is as simple as single-database ACID transactions, and getting either right requires expertise that many teams don’t have.

Operational overhead multiplies. Each service needs its own deployment pipeline, monitoring dashboards, alerting rules, and runbooks for when things go wrong. Ten services doesn’t mean 10% more operational overhead—it means ten times the surface area for things to fail, ten times the deployment configurations to maintain, ten times the dashboards to monitor. The operations team (or the developers doing operations) needs to understand how each service behaves, what its failure modes are, and how to recover when it breaks.

Testing becomes complex. Integration testing across services requires infrastructure to run multiple services together. Contract testing helps ensure services agree on interfaces but adds its own tooling and process overhead. End-to-end tests that verify complete user flows become slow (because they traverse multiple services and networks) and flaky (because any service instability causes test failures). Achieving the same confidence in correctness that was straightforward in a monolith requires significantly more testing investment.

Organizational overhead. Service boundaries need to be defined and maintained. APIs need to be versioned so that services can evolve without breaking their consumers. Breaking changes need coordination across teams—or very careful deprecation periods. This coordination is easier than coordinating within a monolith only when teams are truly independent and rarely need to change shared interfaces. In practice, most organizations find that the interface coordination overhead is as significant as the code coordination overhead they were trying to avoid.

The Middle Ground: Modular Monolith

The false dichotomy that frames architectural decisions as “monolith or microservices” obscures a valuable middle ground that often provides the best tradeoff for most teams.

A modular monolith keeps everything in one deployable unit—maintaining the operational simplicity that makes monoliths attractive—but enforces strict boundaries between modules within that single deployment. Each module has a defined interface that other modules must use; direct access to another module’s internals is prohibited by code review, linting rules, or architectural tests. Dependencies flow in one direction, preventing the tangled coupling that makes monoliths hard to evolve. The database may even have separate schemas per module, with modules accessing only their own tables through their own data access layer.

This architecture gives you the simplicity of monolith deployment—one artifact to build, deploy, and monitor—with the organizational benefits of clear boundaries that help teams work independently. The boundaries create documentation of intent: this module owns this capability, and here’s the interface you use to interact with it. If you later need to extract a service because one component genuinely needs different scaling or technology, the boundaries are already defined. The extraction is a mechanical exercise in moving code and adding network calls rather than an archaeological expedition to understand hidden dependencies.

Most teams should aim for a modular monolith as their default architecture. Extract services only when specific needs justify the complexity that distributed systems introduce. The modular monolith is not a compromise or a stepping stone—it’s often the right final architecture, not just a temporary state on the way to microservices.

Signs You Might Need Microservices

Consider extracting services when you observe concrete problems that service extraction would solve:

  • A component has genuinely different scaling needs that are costing you money or performance. If you’re paying to run 20 instances of your entire application because one component needs 20x capacity while the rest needs 1x, the cost difference might justify the complexity of extraction.
  • Teams are blocked waiting for each other despite clear module boundaries. If deployment coordination is a regular bottleneck—Team A can’t ship because Team B’s changes aren’t ready—and this is causing meaningful delivery delays, service extraction might help. But verify the problem is actually deployment coupling rather than feature coupling, which services won’t solve.
  • A component needs different technology and the integration within the monolith is awkward. If the machine learning component would genuinely work better in Python while the rest of the system is in Java, and the in-process integration is causing problems, a service boundary might make sense.
  • Fault isolation for a specific component would prevent system-wide outages. If historical incidents show that one component’s failures regularly take down the entire system, and that component could reasonably run independently, extraction provides resilience that’s hard to achieve otherwise.
  • You’ve already hit the limits of modular monolith organization. If the codebase has become too large to build and deploy efficiently, if module boundaries are consistently violated despite enforcement efforts, or if teams are genuinely blocked by the single-deployment model—these are signs that service extraction might help.

Note what’s not on this list: “best practices say so” or “we might need to scale someday.” Hypothetical future needs are not reasons to add complexity today.

Signs You Should Stay Monolith

Stay with a monolith (or modular monolith) when your situation doesn’t include clear forcing functions toward microservices:

  • Your team is small and everyone works across the whole system. The organizational benefits of microservices emerge at organizational scale. A team of five that shares ownership of everything gains nothing from service boundaries and pays all the costs.
  • Your load is predictable and moderate. If scaling isn’t a problem you have today, and growth projections don’t suggest it will be soon, there’s no scaling-driven reason to distribute.
  • You don’t have operational expertise for distributed systems. Running microservices well requires expertise in distributed systems debugging, orchestration, service mesh configuration, and failure mode analysis. If that expertise doesn’t exist on your team, you’ll struggle.
  • You’re still figuring out domain boundaries. Early-stage products often have domains that will change as you learn more about the problem space. Service boundaries are hard to change; module boundaries are easier. Get the boundaries right first.
  • Deployment speed isn’t blocked by monolith size. If you can build, test, and deploy your monolith in acceptable time, deployment frequency isn’t a reason to break it apart.

The Extraction Path

If you start with a monolith and later need services, the path is clearer than starting distributed. This is one of the strongest arguments for the modular monolith approach: it preserves the option to extract services without paying the cost upfront.

First, establish clear modules within the monolith. Define interfaces between them and document what each module owns. Enforce boundaries through code structure, automated tests that check for boundary violations, and code review that treats boundary crossing as a serious concern.

When a module genuinely needs to become a service—because you’ve hit a real scaling need, a real technology requirement, or a real team coordination problem—the boundaries already exist. The extraction work is clearer because you know exactly what the module owns, what interfaces it exposes, and what dependencies it has on other modules. You’re essentially replacing in-process method calls with network calls and adding the operational infrastructure for an independent deployment.

Going the other direction—consolidating microservices into a monolith—is harder because distributed systems accumulate complexity that’s difficult to remove. Data gets replicated across services. Interfaces evolve in incompatible ways. Teams build processes around independent deployment. Undoing all of this is significant work, which means premature microservices adoption creates lock-in to the distributed architecture even when it’s not the right choice.

Our Recommendation

Start with a well-structured monolith. Enforce module boundaries from the beginning—not because you’re planning to extract services, but because boundaries make the codebase easier to understand and maintain. Treat it as a modular monolith from day one.

Extract services only when you have concrete evidence that the extraction solves a real problem you’re experiencing today. “We might need to scale” is not concrete evidence. “We might have independent teams someday” is not concrete evidence. If you can’t point to a specific problem that service extraction would solve, you don’t need microservices yet.

Most applications—even successful ones that handle significant scale—work fine as monoliths indefinitely. The applications that genuinely need microservices usually know it clearly when the time comes: the forcing function is obvious, the problem is concrete, and the cost-benefit analysis favors extraction. If you’re debating whether you need microservices, you probably don’t.

Don’t optimize for problems you don’t have. Build something that works, evolve the architecture when actual constraints require it, and let your real experience guide architectural decisions rather than theoretical best practices.

Have a Project
In Mind?

Let's discuss how we can help you build reliable, scalable systems.