What 42% Who Abandoned Microservices Chose Instead — Monolith vs. Microservices: The Reality of Architecture Decisions
To be honest, I used to treat microservices as a silver bullet. I had no idea how casually I'd say "just spin up another service" — until I paid the price. I've pulled all-nighters debugging Kubernetes clusters, and the helplessness of watching data get corrupted from a failed distributed transaction is still vivid in my memory. Right around the time I started asking "was this even the right architecture?" — I realized I wasn't alone.
Industry surveys show a striking figure: 42% of organizations that adopted microservices are consolidating services into larger deployment units (byteiota, citing 2025 CNCF survey). Amazon Prime Video switched a specific media pipeline to a single process and cut infrastructure costs by 90%. Shopify handles 32 million requests per minute with a single monolith. Doesn't something seem off?
This article explores when modular monoliths and microservices each truly shine, and what choice fits your team's current situation. If you're starting a new project or revisiting your current architecture, by the end you'll be able to summarize the answer to "what does our team need right now?" in a single checklist.
Core Concepts
Modular Monolith — "One Unit, But Organized"
A modular monolith is a single deployment unit, but with clearly defined domain boundaries internally. The difference from a traditional spaghetti monolith comes down to one thing — each module owns its own business logic and data, and nothing reaches directly into another module's internals. And those boundaries are enforced at the code level.
Below is a Java example using Spring Modulith. The package structure itself expresses module boundaries, and boundary violations are automatically caught in tests.
// Package structure example
com.example.shop
├── order/ // Order module — internal implementation is private to outsiders
│ ├── OrderService.java
│ ├── OrderRepository.java
│ └── internal/
│ └── OrderValidator.java // Not accessible from outside the module
├── payment/ // Payment module
│ ├── PaymentService.java
│ └── internal/
│ └── PaymentValidator.java // Test fails if referenced directly from outside
└── inventory/ // Inventory module
└── InventoryService.java// Spring Modulith: automatically validates module boundary violations
@ApplicationModuleTest
class OrderModuleTests {
// If the Order module directly accesses Payment internals (internal/), the error below occurs
}Violating a boundary causes the test to fail with a message like this:
org.springframework.modulith.core.Violations:
- Module 'order' depends on non-exposed type
'com.example.shop.payment.internal.PaymentValidator'
in com.example.shop.order.OrderService
→ Use the public API of 'payment' module insteadYou might think "it compiled fine, so why is this a problem?" — but this is the safety net that prevents boundaries from collapsing when a different team member touches this code six months later.
Spring Modulith: An officially supported Spring Boot library that automatically detects module boundary violations and generates module relationship diagrams as documentation. Tools that serve the same role include ArchUnit (Java), NetArchTest (.NET), import-linter (Python), and dependency-cruiser (Node.js).
Microservices — "Independent, But Complex"
Microservices decompose an application into small services that communicate over a network. You get independent deployment, independent scaling, and the freedom to use a different technology stack per service. In exchange, your local development environment already carries this level of complexity:
# docker-compose.yml — example local microservices environment
services:
order-service:
image: shop/order-service:latest
ports: ["8081:8080"]
environment:
- DB_URL=jdbc:postgresql://order-db:5432/orders
payment-service:
image: shop/payment-service:latest
ports: ["8082:8080"]
environment:
- DB_URL=jdbc:postgresql://payment-db:5432/payments
api-gateway:
image: kong:latest
ports: ["8000:8000"]
order-db:
image: postgres:16
payment-db:
image: postgres:16In production, you layer on top of that: Kubernetes (container orchestration), Istio (inter-service communication management), Jaeger (distributed log tracing), and Consul (service discovery). Whether you have the people to manage this stack is the key variable in your decision.
Core Differences Between the Two Architectures
| Category | Modular Monolith | Microservices |
|---|---|---|
| Deployment unit | Single process | Multiple independent services |
| Communication | In-process calls | Network calls (HTTP/gRPC/message queue) |
| Data isolation | Logical separation (physical DB can be shared) | Independent DB per service recommended |
| Operational complexity | Low | High |
| Scaling | Entire application as one unit | Independent scaling per service |
| Infrastructure cost | Baseline | 3.75× to 6× depending on workload |
In-process call: Directly calling a function within the same process without going through a network. Latency is in the nanosecond range, with zero network overhead.
Real-World Applications
Example 1: Amazon Prime Video — Microservices Weren't the Right Fit for This Workload
When I first came across this case, I was honestly a bit shocked. It's widely known as "Amazon abandoned microservices too," but to be precise, this is the story of a specific media pipeline workload called Video Quality Analysis. I want to flag this context, because reading it without it leads to the wrong conclusion.
The original architecture was a distributed microservice system based on AWS Step Functions (a distributed workflow orchestration service). Video frame data was passed between each analysis stage via S3.
[Original architecture — Microservices]
Frame Detector → S3 store → Defect Detector → S3 store → Alert Generator
↑ ↑
Lambda Lambda
(network I/O occurs) (network I/O occurs)
[After migration — Single process]
Frame Detector → Defect Detector → Alert Generator
Direct in-memory transfer (In-process)Switching from routing frame data through S3 to passing it in memory produced dramatic results.
| Metric | Before (Microservices) | After (Single Process) |
|---|---|---|
| Infrastructure cost | Baseline | 90% reduction |
| Data transfer path | Network I/O via S3 | Direct in-memory transfer |
| Scaling performance | Limited | Improved |
Thinking about the perspective of the team that made this decision — this is not a conclusion that "microservices are bad." Pipeline workloads that continuously pass intermediate state, like frame analysis, are inherently structured such that network hops between services become a performance bottleneck. The team changed the architecture to fit that characteristic.
Example 2: Shopify — How to Handle Massive Traffic with a Monolith
Shopify maintains its entire codebase as a single Rails monolith. Looking at the traffic volume it handles challenges the preconception that "monoliths have limits."
| Metric | Figure |
|---|---|
| Peak request throughput | 32 million requests per minute |
| DB query throughput | 11 million MySQL queries per second |
| Data throughput | 30 TB per minute |
The secret is two things: strictly enforcing internal domain boundaries with Packwerk, and selectively extracting only high-load areas with unusual traffic patterns — like Checkout or fraud detection — into microservices.
Here's how Packwerk enforces those boundaries:
# components/order/package.yml — package boundary declaration
name: Order
dependencies:
- components/payment # Can only depend on Payment's public API
enforce_privacy: true # Direct access to types under internal/ is prohibited# components/order/app/services/order_service.rb
module Order
class OrderService
def process(cart)
# Access only through Payment's public API
Order::OrderCreator.new.create_from_cart(cart)
end
end
endIf you try to directly access Payment's internal implementation, running packwerk check catches the violation like this:
components/order/app/services/order_service.rb:8:5
Privacy violation: '::Payment::Internal::PaymentProcessor' is private to 'Payment'
Is there a public entrypoint in 'Payment' that could be used instead?Packwerk: A Ruby package boundary enforcement tool built by Shopify. You declare the allowed dependency scope in configuration files, and adding
packwerk checkto CI automatically catches boundary violations. ArchUnit (Java) and NetArchTest (.NET) serve the same role.
Example 3: Selection Criteria Based on Team Size
A question that comes up often in practice is "when should we move to microservices?" I use the following criteria like a checklist:
- Team size under 10 → Modular monolith. There's no one to operate it.
- Team size 10–50 → Start with a modular monolith, and recommend extracting only bottleneck modules as separate services.
- Team size over 50 + independent scaling needed per service → You can seriously consider microservices.
- Startup / MVP stage → Strongly recommend starting with a modular monolith without exception. If you split services early when domain boundaries aren't clear yet, the cost of correcting the wrong boundaries later is enormous.
- Compliance isolation requirements like HIPAA or PCI → Microservices are virtually mandatory in this situation.
The "Monolith First" principle that Martin Fowler has long advocated is getting renewed attention because more teams have experienced the pain of redesigning microservices that were split along the wrong boundaries.
Pros and Cons Analysis
Advantages
You probably have a decent intuition from the cases above, but to summarize the strengths of each architecture:
Modular Monolith
| Item | Detail |
|---|---|
| Deployment simplicity | Single artifact, one deployment pipeline |
| Zero latency | No network round-trips with in-process calls |
| Data consistency | No distributed transactions needed with a single DB transaction |
| Low operational cost | No service discovery or distributed tracing needed |
| Incremental migration | Specific modules can be extracted as services later |
Microservices
| Item | Detail |
|---|---|
| Independent scaling | Add resources only to services under heavy traffic |
| Faster releases | Deploy specific services independently |
| Technology freedom | Choose language and framework per service |
| Fault isolation | Prevents fault propagation when Circuit Breaker is applied |
| Team autonomy | Large distributed teams can develop without interfering with each other |
Disadvantages and Caveats
Honestly, this section might be more important. Teams tend to know the advantages when they adopt something, but more often regret it later because they didn't know the downsides.
Modular Monolith
| Item | Detail | Mitigation |
|---|---|---|
| Risk of boundary erosion | Devolves into a Big Ball of Mud without enforcement mechanisms | Automatically validate boundaries with ArchUnit, NetArchTest, Packwerk |
| Whole-unit scaling | Can't scale a specific function independently | Extract bottleneck modules as separate services |
| Increasing build times | CI bottleneck in large teams | Introduce module-level caching and incremental builds |
Microservices
| Item | Detail | Mitigation |
|---|---|---|
| Network overhead | Latency incurred on every inter-service call | Optimize with gRPC, message queues |
| Distributed transactions | Complex to handle transactions spanning multiple services | Apply Saga pattern, Outbox pattern |
| Infrastructure cost explosion | Costs can run several times higher for equivalent functionality | Verify the scale justifies the cost first |
| Debugging difficulty | Distributed log tracing is hard | Introduce OpenTelemetry + Jaeger/Zipkin |
| Platform team required | Minimum 2–4 dedicated personnel needed | If those people aren't available, reconsider the timeline |
Saga Pattern: A pattern that handles distributed transactions by breaking them into multiple local transactions and compensating transactions. It's the standard approach for maintaining data consistency in microservices, but it's far from trivial to implement and debug.
Circuit Breaker: A pattern that automatically blocks requests when a connected service starts failing, preventing faults from cascading. Resilience4j (Java) and Polly (.NET) are representative implementations.
The Most Common Mistakes in Practice
-
Starting microservices before domain boundaries are clear: The cost of re-aligning service boundaries later far exceeds getting the initial design right. It's recommended to validate boundaries first in a modular monolith, then extract from there.
-
Not using boundary enforcement tools in a modular monolith: Without architecture tests (ArchUnit, Packwerk, etc.), boundaries will eventually collapse on team conventions alone. There's a high probability it becomes a Big Ball of Mud within six months.
-
Adopting microservices without considering team size and operational capacity: Netflix's microservices strategy presupposes hundreds of engineers and infrastructure at the scale of hundreds of millions of dollars. Copying the architecture without that context just accumulates operational debt.
Closing Thoughts
So what's the first question our team should ask right now? My answer: "Do we currently have the capacity to secure two dedicated platform engineers?" If that question gets a "no," the rest of the choices are almost made for you.
The figure showing 42% returning from microservices doesn't mean microservices are wrong. It means they were applied in an unprepared state, or to the wrong problem. A modular monolith is an excellent starting point, and if you establish domain boundaries well, the path to extracting only the parts you need as services later opens up naturally.
Three steps you can start right now:
-
Visualize dependencies: Regardless of language, it's recommended to first map out the inter-module dependency relationships in your current codebase. Understanding what is coupled to what is the first step.
- Java:
ApplicationModules.of(App.class).verify()(Spring Modulith) - .NET: Reference implementation at kgrzybek/modular-monolith-with-ddd
- Python:
import-linter, Node.js:dependency-cruiser, Ruby:packwerk
- Java:
-
Apply the team size checklist: Check which item in Example 3's selection criteria applies to your current team situation. Especially if you don't have "2–4 dedicated platform engineers," it's recommended not to rush microservices adoption.
-
Add architecture boundary tests: If you plan to maintain a modular monolith, add automated boundary violation detection tests to your CI pipeline. This one test prevents the spaghetti that would otherwise arrive six months later.
- Java: ArchUnit, .NET: NetArchTest, Python: import-linter, Node.js: dependency-cruiser
References
- Rethinking Microservices in 2026: When Modular Monolith Architecture Actually Win | Enqcode
- Modular Monolith: 42% Ditch Microservices in 2026 | byteiota
- Why Teams Are Moving Back From Microservices to Modular Monoliths in 2026 | Medium
- What Is a Modular Monolith? | Milan Jovanović
- Modular Monolith: A Primer | Kamil Grzybek
- Is the Modular Monolith Shopify's Best-kept Secret to Scaling? | Educative
- Monolith vs Microservices 2025: When Amazon Cuts Costs 90% | byteiota
- Microservice Trade-Offs | Martin Fowler
- Monolith vs Microservices vs Modular Monoliths | ByteByteGo
- Spring Modulith with DDD | GitHub
- Modular Monolith with DDD (.NET) | GitHub
- Crafting a self-documenting Modular Monolith with DDD | Spring I/O 2025