TL;DR — default to a modular monolith
Default to a modular monolith: one deployable app with clean, enforced domain boundaries. It fits most web applications, including B2B SaaS, marketplaces and enterprise portals. Move to microservices only when you have a concrete reason. That usually means 20+ engineers, per-domain scaling profiles that have genuinely diverged, or independent deploy cadences a monolith cannot express, and it typically shows up above roughly 10M requests per day.
The architecture debate of 2016–2020 promised that monoliths were dead and microservices were the future. Hard operational experience has largely corrected that. Here is the short version:
- Default to a modular monolith. Clean modules, enforced domain boundaries, a single deploy pipeline. This is the right starting point for the vast majority of web applications, including B2B SaaS, marketplaces and enterprise portals.
- Extract services when you have a concrete reason. Different scaling profiles, independent deployment cadences, or team-ownership boundaries that a monolith cannot express cleanly. Not before.
- Full microservices make sense at scale. Think 20+ engineers, 10M+ requests per day, and service SLAs that genuinely diverge. Below that threshold, the overhead just taxes your team's velocity.
- A plain monolith is not shameful. Shopify, Stack Overflow and Basecamp shipped at billions of dollars in revenue on well-tuned monoliths. The architecture is only wrong if it is stopping you from doing something you need to do.
The three options defined
Traditional monolith
A single deployable application. All business logic, data access and presentation live in one codebase and one process. Every feature ships together, the database is shared, and scaling means running more copies of the whole application. A classic Rails app is a monolith; so is a Django project or a Spring Boot service.
Modular monolith
Still a single deployable binary, but the code is partitioned into well-defined modules. Each module owns its domain logic, its database schema namespace and its public interface. Modules talk to each other through in-process APIs or message contracts rather than HTTP calls. The codebase deploys simply, yet it is structured so you can pull a module out into its own service later. This is what we reach for first when we build our web application development service engagements for clients in the US and EU.
Microservices
A collection of independently deployable services. Each one owns a narrow domain and talks to the others over HTTP or a message broker (Kafka, RabbitMQ, SQS). Each also carries its own database, its own CI/CD pipeline and its own operational footprint. Do this well and large organisations ship fast without stepping on each other. Do it too early and you get a distributed monolith: all the drawbacks of both worlds, none of the advantages of either.
What a modern monolith can actually do
The idea that "monolith equals legacy" is mostly a marketing artefact of the Kubernetes era. So let us be precise about what a modern monolith can and cannot do.
They scale horizontally
Run four copies of a Rails or Django app behind a load balancer and you are already scaling horizontally. That works until the database becomes the bottleneck. At that point you reach for read replicas and connection pooling, not necessarily a new service. Stack Overflow serves billions of pageviews on nine on-premise servers. The monolith was never the bottleneck there; the real culprits were missing caches, missing indexes and loose query discipline.
They support fast iteration
A monolith has one deploy pipeline, one set of integration tests, and one place to grep when you are chasing a bug. For a team of 3–15 engineers, that is a real coordination advantage. Every distributed system you bolt on multiplies the failure modes you have to monitor and the rollback scenarios you have to rehearse.
They support compliance boundaries
GDPR data residency, HIPAA audit logs and SOC 2 controls are easier to implement inside one application than to enforce across a mesh of services. You keep one audit trail, one secrets store and one TLS termination point. See the patterns we use in how to build a multi-tenant SaaS; multi-tenancy is fully achievable inside a modular monolith.
Where monoliths genuinely struggle
There are real limitations, though. If your checkout service has to handle 50x the traffic of your reporting module, scaling the whole app just wastes resources. If your ML inference pipeline runs on completely different requirements (GPU, Python, its own deploy cadence), keeping it in the same process gets awkward. Those are the right reasons to extract a service. Engineering fashion is not.
When do microservices genuinely pay off?
Microservices are the right answer in a handful of scenarios. Here is how to recognise them when you see them.
Genuinely different scaling profiles
If your product recommendation engine takes 1,000 requests per second at peak while your user-settings API takes 5, there is no sense in scaling both together. Once you can measure that gap in production, and not just sketch it in a whiteboard session, pulling the hot path into its own service pays for itself. For how inference-heavy workloads often justify that split, see the 2026 enterprise AI-agent stack.
Independent deployment cadences
When separate product teams need to ship several times a day without coordinating, a single deploy pipeline turns into a bottleneck. That, not raw performance, was Amazon's original reason for microservices: organisational velocity. If your analytics team and your billing team keep waiting for each other's code to stabilise before a release, the architecture has manufactured a human coordination problem that a service boundary would solve.
Isolated fault domains
A catastrophic bug in a monolith takes down the whole application. In a microservices setup, a bug in your notification service does not have to take checkout with it. Circuit breakers, bulkheads and graceful degradation only make sense once you have service boundaries to enforce them at. For high-revenue flows such as payments, the core API and authentication, that isolation guarantee is worth the operational cost.
Regulatory and data-residency boundaries
A US company serving EU customers under GDPR may genuinely need EU-resident data processed in a separately deployed and audited service, not just a config flag inside a shared app. PCI DSS scope isolation works the same way. Cardholder data handled in its own service, behind its own network boundary, is architecturally cleaner than trying to whittle down a monolith's surface area.
Real-world examples: what large systems teach us
Theory is useful. Production outcomes are better. Here are four well-documented cases that illustrate when each architecture paid off — and what it cost.
Netflix: extreme scale justified the investment
Netflix migrated from a DVD-shipping monolith to hundreds of independent services between 2009 and 2015, triggered by a 2008 database corruption event that took the service offline for three days. At Netflix's scale — 200 million subscribers streaming simultaneously — fine-grained scaling genuinely mattered: the encoding service needed GPU instances, the recommendation engine needed ML infrastructure, and the streaming API needed sub-millisecond response times across global PoPs. The operational investment was enormous; Netflix had to build Hystrix, Chaos Monkey and a full observability platform from scratch. The lesson is not "do what Netflix did" — it is that Netflix had measured, concrete scaling problems that made microservices worth the cost before the migration began.
Amazon: organisational velocity was the real driver
Amazon's service decomposition began with a 2002 internal mandate: every team must expose its data and functionality through service interfaces, and communicate exclusively through those interfaces. The goal was not performance — it was enabling hundreds of teams to ship independently without release coordination bottlenecks. The architectural side effect was AWS: when Amazon decomposed its own compute, storage and database infrastructure into services, those services became sellable products. If your organisation does not have Amazon's cross-team coordination problem at that scale, you do not need Amazon's solution.
Shopify: a modular monolith at commerce scale
Shopify ran on a single Ruby on Rails monolith for over a decade, serving millions of merchants and handling tens of billions in GMV annually. Their approach was deliberate: heavy investment in modular design, background job queues, read replicas and a carefully structured multi-tenant data model. They did eventually extract some services (specifically storefront rendering and checkout), but the core commerce platform remained a well-maintained monolith. The lesson: the performance ceiling for a well-tuned monolith is much higher than most engineering teams will ever hit.
Stack Overflow: nine servers, billions of pageviews
Stack Overflow runs on nine on-premise servers by deliberate choice, serving billions of requests per month. The engineering team attributes this efficiency to aggressive caching, query optimisation and a mature monolith. When more capacity was needed, the team profiled first: bottlenecks were nearly always specific slow queries, missing indexes or uncached hot reads — not the monolith. This is the most useful calibration point for any team planning microservices "for scale" before they have measured where their actual ceiling is.
Cost, team size and operational burden
Microservices are not free. Every service you add multiplies the infrastructure and staffing cost. Here is an honest accounting.
Infrastructure cost
Each service needs its own compute (container, Lambda or VM), its own database or database namespace, its own secrets-manager wiring, its own load balancer or API gateway routing rule, its own log-aggregation entry, and its own health-check monitor. Run ten services and you are paying for ten of each. On AWS or GCP, a modular monolith often costs 60–80% less in monthly infrastructure than an equivalent microservices mesh carrying the same traffic.
Observability cost
A monolith fails with a stack trace in one log stream. A distributed system fails with partial traces smeared across five services, two async queues and a caching layer. That is why distributed tracing (Jaeger, Tempo, AWS X-Ray), structured log aggregation (Loki, Datadog, CloudWatch) and service-health dashboards stop being nice-to-haves and become a requirement. Budget a dedicated platform engineer plus $2,000–8,000/month in SaaS tooling once a team runs 10+ services.
Team size requirements
Amazon's two-pizza rule (6–10 engineers per service) is the operational floor for keeping a service maintained without constant context switching. Below 20–30 engineers in total, microservices force every engineer to own several services at once, which is exactly the coordination overhead the architecture was meant to remove. For how those upstream architecture calls feed into the product reliability that drives retention, see the SaaS churn-reduction playbook.
| Dimension | Monolith | Modular Monolith | Microservices |
|---|---|---|---|
| Deploy complexity | Low | Low | High |
| Infra cost (same traffic) | Lowest | Lowest | 2–5x higher |
| Observability overhead | Minimal | Minimal | Significant |
| Horizontal scalability | Coarse-grained | Coarse-grained | Fine-grained |
| Independent team deploys | No | Partial | Yes |
| Min. team to run well | 3–5 engineers | 5–15 engineers | 20+ engineers |
| Time to first production deploy | 1–2 weeks | 1–3 weeks | 4–8 weeks |
Data architecture and consistency trade-offs
The hardest part of a microservices migration is rarely the service itself — it is splitting the database. This deserves its own section because it drives more failed migrations than any other factor.
Monolith: shared database, ACID guarantees
A monolith typically uses a single relational database. This gives you ACID transactions across domain boundaries at no extra cost. When a payment succeeds and an order is confirmed in the same request, both writes happen inside one database transaction. If either fails, everything rolls back. Cross-domain queries are also simple: a single SQL JOIN is far cheaper than two service-to-service calls with client-side aggregation.
Microservices: database per service, eventual consistency
The microservices principle is one database per service — the service owns its data and exposes it only through its API. This gives you schema isolation and independent schema evolution, but eliminates ACID guarantees across service boundaries. When a payment service and an order service each own their data, and the payment write succeeds but the order write fails, you have a distributed consistency problem that must be solved explicitly. The standard patterns are saga choreography (each service publishes events that trigger the next step) and saga orchestration (a central coordinator sequences the steps). Both add substantial implementation and debugging complexity that simply does not exist in a monolith.
Practical guidance
Start with a shared database, even inside a modular monolith. Enforce domain boundaries through schema namespaces — separate Postgres schemas per module — and strict module APIs, not separate database instances. Extract the database only when you extract the service, and only after the service boundary has been stable in production long enough to trust. Splitting a database speculatively, before a service is extracted, delivers all the consistency pain with none of the deployment independence.
Scaling a web app the right way
Before you pick an architecture for scale you do not yet have, work through these levers in order. Most applications never need to go past step three.
1. Vertical scaling and query optimisation
Double the database instance size. Add an index to the slow query. Turn on connection pooling (PgBouncer, RDS Proxy). This is free or near-free engineering, and it commonly buys 5–10x of capacity headroom. Most applications that supposedly "need microservices for scale" actually need one database index and a Redis cache.
2. Horizontal application scaling
Run multiple instances of your monolith behind a load balancer. Add read replicas for read-heavy workloads. Use a CDN to offload static assets and cached API responses. A single Rails or Node.js process at 4 vCPU handles ~500 requests/second. Eight instances behind a load balancer handle 4,000. You reach this ceiling slowly.
3. Caching and async queuing
Put Redis or Memcached in front of your hot read paths. Hand anything that does not need to be synchronous to a background job queue (Sidekiq, Celery, Bull): emails, webhooks, report generation, third-party API calls. Moving that work off the request path removes a whole class of slow-request tail latency that makes your p95 look terrible, and it takes no architectural change at all.
4. Extract one service at a time
When one specific hot path is genuinely bottlenecked and you have exhausted the steps above, extract that service with the strangler fig pattern. Route new traffic to the service while the monolith keeps serving the rest, and migrate incrementally. Do not attempt a big-bang rewrite from monolith to microservices; the failure rate is high and the cost is enormous.
Migration: moving from monolith to microservices
If you have decided that a specific part of your system genuinely needs to become its own service, here is how to execute that migration without a big-bang rewrite — which has a very high failure rate in practice.
The strangler fig pattern
Named after a tree that grows around its host and gradually replaces it, this is the standard approach for incremental service extraction:
- Identify a seam. Find a module with a clear domain boundary — payments, notifications, search — where the interface with the rest of the monolith is already well-defined and stable.
- Build the new service. Implement the same functionality independently, with its own database namespace and its own deployment pipeline.
- Route traffic at the edge. Use an API gateway or load balancer rule to send a portion of requests to the new service. The monolith continues to handle the rest.
- Migrate incrementally. Move traffic gradually — 1%, 10%, 50%, 100% — with observability at each step. Roll back instantly if error rates diverge from the monolith baseline.
- Remove the old code. Once the service handles 100% of traffic and has been stable through at least one on-call cycle, delete the module from the monolith and clean up the database schema.
Anti-patterns to avoid
- Distributed monolith. Services that share a database, or that call each other synchronously for every request, give you none of the benefits of microservices and all the complexity. If services cannot be deployed independently, they are not microservices — they are a monolith with a network tax.
- Big-bang rewrite. Rewriting a working system from scratch as microservices, all at once, has a very low success rate. The monolith contains years of accumulated business logic, edge cases and compliance handling that are invisible until they break in the new system.
- Premature extraction. Extracting a service before its domain boundary is stable means you will refactor the contract repeatedly, which is expensive. Wait until the domain is genuinely stable in production — then extract.
- No observability before the split. If you cannot see distributed traces, aggregate logs across services and get per-service error rates, you are flying blind. Instrument before you extract, not after.
Decision matrix
Use this matrix when you are actually making the call. Score each row for the situation you are in now, not for the company you hope to be in three years.
| Criterion | Choose modular monolith if… | Consider microservices if… |
|---|---|---|
| Team size | Under 20 engineers | 20+ engineers across multiple product squads |
| Traffic pattern | Uniform or low-volume today | Measured, divergent peak loads per domain |
| Deploy cadence | One or two teams, coordinated releases | Multiple teams, independent cadences required |
| Compliance isolation | Standard GDPR/SOC 2 manageable in one app | PCI DSS cardholder scope or strict data residency |
| DevOps maturity | No dedicated platform engineer | Platform/SRE team already in place |
| Technology mix | One primary language/runtime | Genuine need for mixed runtimes (e.g. ML GPU service) |
| Fault tolerance requirement | Full downtime acceptable for rare incidents | Partial degradation required (checkout must survive analytics outage) |
Architecture trends in 2026
A few shifts in the 2026 landscape are worth factoring in when you make a new architecture decision.
The modular monolith renaissance
A 2025 CNCF survey found that 42% of organisations that adopted microservices have since consolidated some services back into larger deployable units. The primary reasons were debugging complexity, network latency, and operational overhead that did not deliver proportional business value. The modular monolith — a term that carried an almost apologetic connotation in 2018 — is now a mainstream, deliberate architectural choice, not a concession.
AI inference as a natural service boundary
Machine learning inference is one of the few workloads where microservice extraction is almost always justified: it runs on different hardware (GPUs or NPUs), has a different deployment cadence (model releases are separate from application releases), and has a meaningfully different scaling profile. If your product calls an LLM or runs on-device model inference, treating the inference endpoint as a separate service — even if everything else is a monolith — is a clean, practical pattern that does not require committing to a full microservices architecture.
Platform engineering replacing service mesh complexity
Service mesh adoption (Istio, Linkerd) peaked and then declined: most teams found the operational weight of a mesh layer — mTLS configuration, sidecar proxies, custom CRDs — exceeded the benefits at their scale. The successor pattern is platform engineering: centralising CI/CD pipelines, secrets management, observability configuration and service templates in an internal developer platform, so individual product teams do not re-solve those problems for every new service. If you are planning microservices at scale, budget for a platform team before you start multiplying services.
Serverless for event-driven and sporadic workloads
Functions-as-a-service (AWS Lambda, Cloudflare Workers, Google Cloud Run functions) have matured to the point where they are a viable deployment target for many microservice use cases — specifically event handlers, webhooks, scheduled jobs and background processing that have irregular or low-volume traffic. For teams that want service-level isolation without Kubernetes overhead, serverless is worth evaluating before reaching for full container orchestration.
FAQ
Should a startup use microservices?
Almost never at the beginning. Microservices multiply operational surface area — service discovery, distributed tracing, independent CI/CD pipelines and a team large enough to own each service. A startup's constraint is shipping product and learning quickly, not infrastructure flexibility. Start with a modular monolith; extract services when a concrete scaling or ownership boundary forces you to.
What is a modular monolith?
A modular monolith is a single deployable application internally divided into well-defined, loosely coupled modules — each owning its own domain logic, database schema namespace and public API surface. It deploys as one process but is structured so that individual modules can be extracted into separate services later with minimal refactoring. It is the sweet spot for most teams under 50 engineers.
When do microservices pay off?
Microservices pay off when: (1) different parts of the system have genuinely different scaling profiles — checkout handles 10x the load of admin; (2) independent teams need to deploy without coordinating releases; (3) a specific service requires a different technology stack, SLA or compliance boundary. If none of those are true today, the overhead of microservices is pure cost.
Are microservices more scalable than a monolith?
Not automatically. A well-tuned monolith on modern cloud infrastructure handles millions of requests per day with horizontal scaling, connection pooling and read replicas. Microservices allow fine-grained scaling of hot paths — but that benefit only materialises when different services genuinely have different traffic shapes. Many teams add microservices complexity before they have the traffic that would justify it.
How big should the team be to run microservices?
Amazon's two-pizza rule is a useful guide: each service should be ownable by a team of 6–10 engineers. In practice you need at least one dedicated DevOps or platform engineer, SRE coverage, observability tooling and enough developers to maintain each service without constant context switching. Below 20–30 engineers total, microservices usually create more coordination overhead than they remove.
Can I migrate from a monolith to microservices later?
Yes — and this is the recommended path. Build a modular monolith with clean domain boundaries from the start, and extracting a service later is a contained effort: move a module's code, split its database tables and add an API contract. The strangler fig pattern — routing traffic incrementally to a new service while the monolith handles the rest — is battle-tested for this migration. Do not design for microservices on day one unless you already have the team and traffic that requires it.
What is eventual consistency and why does it matter for microservices?
In a monolith with a shared database, operations across different parts of the application participate in ACID transactions — all-or-nothing guarantees that never leave data in a partial state. In a microservices architecture, each service owns its database, so you lose that guarantee across service boundaries. Instead, you get eventual consistency: the system will converge to a consistent state given enough time and no further updates, but intermediate states may be visible. Handling this correctly requires explicit patterns — the saga pattern, outbox pattern, and event sourcing. Underestimating this complexity is one of the most common causes of troubled microservices migrations.
What is Domain-Driven Design and how does it relate to service boundaries?
Domain-Driven Design (DDD) is a software design approach that structures code around business domains and their explicit boundaries, called bounded contexts. Each bounded context — payments, inventory, user accounts — has its own model, language and data ownership. In a microservices architecture, bounded contexts map directly to service boundaries: each service owns its bounded context and exposes it only through an API. In a modular monolith, bounded contexts map to modules. DDD gives you the language and modelling tools to define those boundaries rigorously before you commit to any deployment topology — which is why it appears in nearly every serious microservices design resource.
Last updated 4 September 2026. Architecture guidance based on production engagements delivered for US and EU clients between 2022 and 2026. Cost figures are estimates; actual infrastructure spend varies by cloud provider, region and traffic shape.


