The Microservices Dream vs. The Production Reality
•The Promise That Was Sold
Between 2012 and 2020, MicroservicesAn architectural style where each business capability is built as a small, independently deployable service that communicates with others over a network, instead of in-process function calls. became the dominant architectural dogma of the enterprise software industry. The sales pitch was seductive and, in certain narrow contexts, entirely justified: decompose your monolithic application into small, independently deployable services, each owning its own data store and communicating over well-defined network APIs. Individual teams could deploy on their own schedule without coordination. Services could scale independently — the payment processor could scale to 100 instances while the user-profile service stayed at two. Technology heterogeneity was suddenly possible: team A could use Node.js while team B ran Go.
Netflix, Amazon, Google, and Uber became the canonical reference architectures. Conference talks showed sprawling service-dependency graphs as proof of engineering sophistication. The message, implicit and explicit, was that if you were still running a MonolithA software application deployed as a single unified artifact — one codebase, one runtime process, one deployment unit. in 2018, you were architectural dead weight.
What those conference talks omitted — because the speakers were typically at companies with 500-person engineering organisations and problems most software teams will never have — was the bill. Not just the AWS bill, though that was real. The cognitive bill, the coordination bill, the debugging bill, the data-consistency bill. The cost that microservices extract in exchange for their genuine benefits is denominated not just in dollars, but in engineering hours, incident response complexity, and architectural decisions that become permanent the moment two services start talking to each other.
Microservices didn't fail as a pattern — they failed as a default.
•The Distributed Systems Problem No One Warned You About
The moment you split an application into two processes communicating over a network, you have left the world of functions calling other functions and entered the world of distributed systems. This transition is not gradual — it is a hard categorical shift, and it drags with it a suite of failure modes that simply do not exist in a single process.
A function call either succeeds or throws an exception. A network call can succeed, fail, time out, partially succeed, succeed on the server but have the response lost in transit, or succeed slowly enough that the client gives up and retries — causing the operation to run twice. Every one of those outcomes is possible on every single inter-service call, and every team building microservices must eventually write code that handles all of them correctly.
Peter Deutsch's Eight Fallacies of Distributed Computing — written in 1994 — remain embarrassingly relevant today. The network is not reliable. Latency is not zero. Bandwidth is not infinite. The network is not secure. Topology does not stay constant. There is not one administrator. Transport cost is not zero. The network is not homogeneous. Microservices architecture forces every service and every calling team to contend with all eight of these fallacies simultaneously, for every inter-service boundary they introduce.
- Partial failure: a downstream service can fail mid-operation, leaving data in an inconsistent state with no built-in rollback mechanism.
- Timeout ambiguity: when a network call times out, the caller cannot know whether the server received the request, processed it, or never saw it.
- Cascading failures: a single slow service causes upstream services to exhaust their connection pools and thread pools, triggering a system-wide outage from one degraded component.
- Retry storms: well-intentioned retry logic, firing simultaneously across hundreds of service instances, can amplify load on a recovering service by 10x — preventing the recovery it was designed to survive.
The Hidden Tax: What Microservices Actually Cost
•The Network Latency Tax
In a monolithic application, a function call costs roughly 1 nanosecond — a number so small it is effectively free in any real engineering budget. Crossing a network boundary in a microservices architecture costs, at minimum, 0.5 milliseconds on a low-latency internal network — often 1 to 10 milliseconds under real load. That is a difference of six to seven orders of magnitude. This is the Network Latency TaxThe unavoidable overhead — roughly 0.5-10ms per call — added to every inter-service network request in a distributed architecture, a cost that simply doesn't exist for an in-process function call inside a monolith. — an overhead paid on every single inter-service call, whether or not that call does anything computationally expensive.
This latency is not a bug in your implementation. It is the physics of network communication: the speed of light, the overhead of TCP handshakes, TLS encryption, serialisation and deserialisation of request payloads (JSON parsing alone on a complex object can cost 0.1–1ms), and kernel context switches. No amount of engineering optimisation eliminates this floor. You can reduce it at the margins with gRPC and Protobuf over JSON, with HTTP/2 connection multiplexing, with service co-location in the same availability zone — but the floor remains.
None of this is a flaw in any particular team's implementation — it's the physics and economics of crossing a network boundary, paid again on every single call.
| Call Type | Typical Latency | Per-Request Overhead on 5-Service Chain | Serialisation Cost |
|---|---|---|---|
| In-process function call | ~1 ns | ~5 ns total | None |
| In-process via message bus | ~100 ns | ~500 ns total | None |
| Local network gRPC (Protobuf) | ~0.5 ms | ~2.5 ms total | ~0.05 ms |
| Local network REST (JSON) | ~1–3 ms | ~5–15 ms total | ~0.1–1 ms |
| Cross-AZ REST call | ~5–10 ms | ~25–50 ms total | ~0.1–1 ms |
| Cross-region REST call | ~50–150 ms | ~250–750 ms total | ~0.1–1 ms |
ns = nanoseconds · ms = milliseconds · AZ = Availability Zone · gRPC = Google Remote Procedure Call · REST = Representational State Transfer · JSON = JavaScript Object Notation
•The Distributed Transaction Problem & The Saga Pattern
Relational databases provide ACIDAtomicity, Consistency, Isolation, Durability — the relational database transaction guarantee.Learn more transactions — Atomicity, Consistency, Isolation, Durability — as a core guarantee. Within a single database, you can update ten tables inside one transaction: either all ten updates commit together, or none of them do. There is no in-between state visible to any other database reader.
Microservices, by design, place different domain entities in different services with different databases. An e-commerce order creation that previously required one ACID transaction — deduct inventory, create order record, charge payment, send confirmation — now spans three or four services, each with its own data store. There is no native distributed transaction mechanism that provides ACID guarantees across service boundaries at scale — the closest available substitute is Eventual ConsistencyA consistency model where a distributed system guarantees that, given no new updates, all replicas will eventually converge to the same value — though a read in the interim may return stale data.: accepting that the data will converge to a correct state, just not instantly and not inside a single atomic step.
The industry-standard solution is the Saga PatternA sequence of local transactions, each with a compensating action to undo it, used to keep a multi-service operation consistent without a blocking distributed lock.Learn more: decompose the distributed transaction into a sequence of local transactions, each with a corresponding compensating transaction that reverses it if a later step fails. A choreography-based Saga publishes events that trigger subsequent steps; an orchestration-based Saga uses a central coordinator to explicitly invoke each step and its compensation.
- Saga complexity cost: implementing a saga for a five-step business operation requires writing ten pieces of business logic — five forward steps and five compensating transactions, each of which must be tested, monitored, and debugged independently.
- Idempotency requirement: every step must be IdempotentDescribes an operation where executing it multiple times produces the same result as executing it once — required for any step in a distributed workflow that might be retried after a timeout. — safe to call multiple times — because the orchestrator may retry a step after a timeout without knowing if the first attempt succeeded.
- Visibility gap: between the moment step 2 commits and step 3 begins, the system is in an inconsistent intermediate state that is visible to other concurrent reads — something an ACID transaction would have prevented entirely.
Orchestration-Based Saga: Order Creation Across 3 Services
Saga Orchestrator
Central coordinator
Inventory Service
Local DB write
Payment Service
Local DB write
Order Service
Local DB write
Inventory Service
Compensating TX: release reservation
•The DevOps Complexity Multiplier
A monolithic application has one CI/CD pipeline, one deployment artifact, one set of environment variables, one log stream to query, one set of health checks, and one place to look when something goes wrong at 3am. The operational surface area is bounded by the size of the application.
A microservices architecture multiplies that surface area by the number of services. Each service requires its own Docker image, its own CI/CD pipeline with its own secrets management, its own Kubernetes deployment manifest with its own resource limits and autoscaling policies, its own database migrations, its own API versioning strategy, and its own on-call runbook. The Kubernetes cluster itself — with its ingress controllers, Service MeshAn infrastructure layer (Istio, Linkerd, Cilium) that handles service-to-service communication in a microservices architecture — mTLS, traffic routing, rate limiting, and observability, via sidecar proxies.es, certificate managers, horizontal pod autoscalers, and cluster autoscalers — is a full-time operational product requiring specialist expertise.
- Service discovery overhead: services must find each other at runtime — via DNS, a service registry like Consul, or Kubernetes Service objects — adding a failure point that does not exist in a monolith.
- Distributed tracing cost: following a single user request across ten services requires a tracing infrastructure (OpenTelemetryAn open-source observability framework providing APIs, SDKs, and tooling for collecting distributed traces, metrics, and logs — the standard instrumentation layer for debugging microservices in production., Jaeger, or Zipkin), instrumentation in every service, and an engineer who knows how to read a trace waterfall — skills a monolith team never needs to develop.
- Schema coordination: changing the API contract between two services requires coordinating deployments across two teams, implementing backward-compatible versioning, and often maintaining deprecated API versions for months — overhead that an in-process function signature change eliminates entirely.
- Database-per-service explosion: ten services with ten databases means ten backup strategies, ten migration tools, ten connection pool configurations, and ten sets of database credentials to rotate.
The Case Studies: Real Migrations, Real Numbers
•Amazon Prime Video: The 90% Cost Reduction
In May 2023, Amazon Prime Video's engineering team published a technical blog post that became one of the most widely circulated architecture discussions of the year. The post described how their audio/video monitoring service — responsible for detecting quality defects in Prime Video streams in real time — had been built as a distributed microservices architecture using AWS Step Functions for orchestration and separate services for each analysis stage.
The system worked, but it had two structural problems. First, it hit the account-level limits of AWS Step Functions at scale, making horizontal scaling economically and technically constrained. Second, passing large video frame data between distributed components over the network created significant data-transfer costs and latency that a shared in-memory process would not have incurred.
The team's solution was to merge the distributed components into a single monolithic process, running the entire analysis pipeline in one EC2 instance with shared memory between stages. The result: infrastructure costs dropped by approximately 90%. The service became simpler to deploy, simpler to debug, and faster to execute — all simultaneously.
- What changed: distributed Step Functions orchestration → single EC2 process with in-memory data passing between pipeline stages.
- What stayed the same: the service is still deployed independently, still has its own codebase and team — the monolith is internal to that Bounded ContextA Domain-Driven Design concept defining an explicit boundary within which a particular domain model is internally consistent and valid — the correct unit of decomposition for both modular monoliths and microservices., not a company-wide architectural rollback.
- The key lesson: when the primary communication pattern between components is high-volume data transfer (video frames), network serialisation is not just slow — it is economically irrational.
•37signals: $3.2M Saved by Leaving the Cloud
David Heinemeier Hansson, creator of Ruby on Rails and co-founder of 37signals (makers of Basecamp and Hey), published a detailed accounting in 2023 of the company's decision to migrate off AWS and back onto owned hardware. The numbers were specific and verified: 37signals expected to save approximately $3.2 million per year by owning their own servers rather than renting compute from Amazon.
The economics were straightforward once laid out. Cloud computing trades capital expenditure for operating expenditure — instead of buying a server for $15,000 that lasts five years, you pay $8,000 per year to rent equivalent compute. For workloads with predictable, steady traffic — which most mature SaaS businesses have — the rental model becomes increasingly expensive over time relative to ownership.
37signals's Basecamp application is a Rails monolith. It serves millions of users. It has never needed to decompose into microservices because it was built with careful internal modular structure — clean boundaries between concerns, without the overhead of network-based service separation. The monolith runs on hardware the company owns and understands, with performance characteristics that are predictable rather than metered.
- Hardware purchased: Dell servers with NVMe SSDs — high-performance, predictable latency, no egress fees.
- Application architecture: Rails monolith with modular internal structure — not microservices, not a big-ball-of-mud. Clean boundaries enforced by Ruby module namespacing.
- Operational model: small, senior engineering team with deep system knowledge — versus the sprawling on-call rotation a large Kubernetes cluster requires.
- DHH's framing: this is not an argument against the cloud for every company — it is an argument that the cloud's value proposition weakens significantly as a business's traffic becomes stable and predictable.
•Shopify: The Majestic Monolith That Never Needed to Change
Shopify's core commerce platform is a Ruby on Rails monolith — a fact the company has been transparent about for years and has never apologised for. At peak traffic (Black Friday 2023 processed over $9.3 billion in merchant sales in a single day), the Shopify monolith handled a volume of transactions that most microservices architectures would struggle to match.
The secret is not that Shopify has a simple codebase — it is one of the most complex Rails codebases in production. The secret is that the engineering team invested relentlessly in making the monolith modular: enforcing strict component-level boundaries through static analysis tools, separating database access patterns, and treating internal modules as if they were external services in terms of API discipline — without paying the network tax of actually making them external.
Shopify did eventually introduce some service separation for specific high-traffic subsystems — but the core commerce logic remained a unified deployment. DHH's 2016 essay 'The Majestic Monolith', written while at Basecamp, articulated what Shopify had already been quietly practicing: a well-structured, internally disciplined monolith can outperform a microservices architecture on every dimension that matters to a product team.
A monolith is not automatically evidence that a team couldn't adopt microservices — sometimes it's evidence they knew exactly what they didn't need.
What Is a Modular Monolith, Actually?
•What a Modular Monolith Actually Is
The Modular MonolithA monolithic deployment unit with hard, enforced internal module boundaries — each module owns its own domain model and data, communicating with others only through a published public API. is frequently misunderstood as simply 'a monolith with good code organisation' — which undersells the architectural discipline it requires. A genuine modular monolith enforces hard boundaries between internal components that are structurally identical to the contracts between microservices, with one critical difference: those boundaries are crossed by in-process function calls rather than network requests.
Each module in a modular monolith owns its own domain model, its own data access layer (and ideally its own schema within a shared database), and exposes a well-defined public API to other modules. No module reaches directly into another module's internal implementation — the same rule that governs inter-service communication in microservices, enforced here at compile time rather than at runtime across a network.
This distinction is consequential. A Distributed MonolithThe worst-case outcome of a microservices migration: multiple services that are separately deployed but still tightly coupled, requiring coordinated deployment and providing none of the independence genuine microservices offer. — the worst outcome of a poorly executed microservices migration — has all the operational complexity of microservices with none of the independence: services that are technically separate but so tightly coupled that they must be deployed together anyway. A modular monolith avoids this failure mode entirely by keeping the coupling within a single process, where it is visible, refactorable, and free of network failure modes.
A modular monolith isn't just "a monolith with good organisation" — it's microservices-grade discipline enforced by the compiler instead of the network.
- Module boundary rule: no module may import from another module's internal packages — only from its explicitly published public interface.
- Data isolation: each module owns its own database schema. Cross-module data queries go through the owning module's API, not direct SQL joins across schema boundaries.
- Deployment unit: the entire modular monolith deploys as one artifact — one container, one binary — eliminating per-service deployment coordination entirely.
- Extraction path: a well-bounded module in a modular monolith can be extracted into an independent service in days, because its API contracts and data boundaries are already cleanly defined.
Modular Monolith Internal Architecture
Single Deployment Process
Orders Module
Public API only
Payments Module
Public API only
Inventory Module
Public API only
Notifications Module
Public API only
orders.*
schema
payments.*
schema
inventory.*
schema
•Enforcing Module Boundaries in Practice
Declaring that modules should not depend on each other's internals is easy. Enforcing it as code evolves under deadline pressure is the actual engineering problem. Several tools and patterns have emerged specifically to solve this in monolithic codebases.
- Ruby/Rails: Packwerk — Shopify open-sourced PackwerkAn open-source static analysis tool, built by Shopify, that enforces package boundaries in Ruby on Rails applications — failing a build if one module imports from another module's private internals., a static analysis tool that enforces package boundaries in Rails apps. Running in CI, it fails any build where a module imports from another module's private namespace.
- Java: ArchUnit — a test library that allows teams to write architecture rules as unit tests: 'no class in com.app.orders may depend on a class in com.app.payments.internal'. Failures break the build.
- Go: module graph analysis — Go's explicit package import graph makes cross-module dependencies visible and auditable with standard tooling.
- TypeScript: path aliases + ESLint rules — barrel exports per module combined with ESLint import boundary rules prevent cross-module internal access.
- Database: schema-per-module — physically separating module data into distinct PostgreSQL schemas prevents cross-module raw SQL joins and makes data ownership unambiguous.
The Distributed Systems Fallacies That Bit Everyone
•The Eight Fallacies of Distributed Computing
L. Peter Deutsch originally compiled what became known as the Eight Fallacies of Distributed Computing while at Sun Microsystems in the 1990s. They remain the most concise description of why distributed systems — including microservices — are categorically harder to reason about than single-process systems. Every team that adopted microservices without internalising these fallacies paid for the education in production incidents.
Every one of these eight assumptions is quietly true inside a single process — which is exactly why splitting that process in two is a bigger decision than it looks like on a whiteboard.
| Fallacy | The Wrong Assumption | The Real-World Consequence |
|---|---|---|
| 1. The network is reliable | Network calls succeed or fail cleanly | Partial writes, phantom timeouts, split-brain state — operations that appear to fail but actually succeeded |
| 2. Latency is zero | Service calls are effectively instant | 5-service chain = 5–50ms overhead minimum; p99 tail latency spikes unpredictably under load |
| 3. Bandwidth is infinite | Sending large payloads is free | Video/image data passed between services costs money and causes congestion (the Prime Video case study) |
| 4. The network is secure | Internal network traffic is trusted | East-west traffic between services requires mTLS, service mesh, and zero-trust policies |
| 5. Topology doesn't change | Service IPs and hostnames are stable | Kubernetes pod restarts change IPs; DNS TTLs cause stale routing; service discovery must be dynamic |
| 6. There is one administrator | One team owns and understands the system | 10 services = 10 teams = 10 deployment schedules = coordination overhead grows with every new service |
| 7. Transport cost is zero | Network calls are free | Egress fees, serialisation CPU cost, and connection pool exhaustion all have real dollar values |
| 8. The network is homogeneous | All services communicate consistently | Mixed protocols (REST, gRPC, WebSockets, message queues) create interoperability surface area |
mTLS = mutual Transport Layer Security · DNS = Domain Name System · TTL = Time-to-Live · CPU = central processing unit · REST = Representational State Transfer · gRPC = Google Remote Procedure Call
Industry Problems & Engineering Bottlenecks
•The Distributed Monolith: The Worst of Both Worlds
The most dangerous outcome of a microservices migration is not a failed migration — it is a partially completed one. When teams decompose a monolith into services without investing the time to establish clean bounded contexts and truly independent data ownership, the result is a distributed monolith: multiple separately deployed processes that are still tightly coupled to each other's implementation details.
A distributed monolith deploys services independently in theory, but in practice must coordinate deployments because service A depends on a specific internal schema or behaviour of service B. It has all the operational complexity of microservices — separate pipelines, separate logs, distributed tracing requirements — with none of the actual independence that justifies that complexity.
- Detection symptom: if two services must always be deployed together after a change, they are a distributed monolith in those components.
- Detection symptom: if service A calls service B synchronously on every request and service B going down causes service A to go down, your 'microservices' are a tightly coupled distributed system.
- Detection symptom: if you cannot explain in one sentence what business capability each service exclusively owns, the boundaries are wrong.
- Fix: merge tightly coupled services back into a single deployment unit and establish genuine bounded contexts before attempting to re-extract them.
•Service Mesh Complexity Overhead
At sufficient microservices scale, teams introduce a service mesh — Istio, Linkerd, or Cilium — to handle cross-cutting concerns that cannot reasonably be implemented individually in every service: mutual TLS between services, traffic routing and load balancing, rate limiting, Circuit BreakingA pattern that automatically stops sending requests to a failing downstream service once failures cross a threshold, preventing one degraded service from cascading into a system-wide outage., and distributed tracing injection.
A service mesh is itself a significant distributed system that must be operated, upgraded, and debugged. Istio, the most widely adopted mesh, runs a control plane (Pilot, Citadel, Galley) plus a data-plane Envoy sidecar proxy injected into every pod. The mesh adds latency (typically 1–5ms per request through the proxy), resource overhead (the Envoy sidecar consumes CPU and RAM on every node), and a new failure domain: if the control plane degrades, certificate rotation fails and services begin rejecting each other's connections.
•The On-Call Incident Complexity Problem
Debugging a production incident in a monolithic application follows a familiar sequence: check the application logs, find the stack trace, identify the failing function, examine the code. The entire investigation lives in one codebase, one log stream, and one deployment artifact. A senior engineer joining a company can diagnose most production incidents within hours of gaining access.
Debugging a production incident in a microservices architecture requires: identifying which service in a dependency graph is the root cause (not just the first service to surface an error to the caller), correlating log entries across multiple separate log streams using a trace ID, reading a distributed trace waterfall in Jaeger or Zipkin to find which service introduced the latency spike, potentially reproducing the issue in a local environment that requires running eight Docker containers simultaneously, and understanding the deployment history of multiple services to identify which recent change introduced a regression.
On-call cognitive load compounds with every service added to the graph. The Mean Time to Resolution (MTTR) for microservices incidents is consistently higher than for equivalent monolithic incidents, even when the root cause is no more complex — because the investigation tooling is more complex.
None of this complexity means anyone did microservices wrong — it's the standard price of the pattern, worth paying only when the organisation is actually large enough to need what it buys.
Decision Framework: When to Use What
•The Decision Framework: Monolith First
Martin Fowler, who co-authored the original microservices article that helped launch the pattern into mainstream adoption, has been clear in subsequent writing about the correct sequencing: almost all successful microservices implementations started as a monolith that was decomposed when the organisation and codebase had both grown large enough to justify it. Almost all failed microservices implementations started as microservices from day one, before the team understood where the genuine domain boundaries were.
The 'Monolith FirstA principle, articulated by Martin Fowler, that new systems should start as monoliths and extract services only once genuine, measured organizational or scaling pressure makes decomposition worthwhile.' principle is not a rule against ever using microservices. It is a sequencing argument: a modular monolith with clean internal boundaries is a lower-risk starting point for any new system, because it preserves optionality. If the system grows to the scale where microservices benefits are genuine — truly independent scaling requirements, organisational teams large enough to own independent services — a well-bounded module can be extracted into a service. If the system never reaches that scale, nothing was wasted on premature distributed systems complexity.
This is also where Conway's LawThe observation that organizations design systems that mirror their own communication structure — a microservices architecture tends to work best when the desired service boundaries already match the org chart. does its quiet work — an organisation's service boundaries tend to mirror its communication structure whether or not that was ever a deliberate design decision.
| Signal | Suggests Monolith | Suggests Microservices |
|---|---|---|
| Team size | < 20 engineers on the system | > 50 engineers across multiple autonomous teams |
| Deploy frequency | Multiple times per day from one pipeline | Multiple teams need independent deploy cadences |
| Scaling pattern | Uniform — the whole system needs more capacity | Heterogeneous — one component needs 100x another |
| Domain clarity | Bounded contexts not yet fully understood | Bounded contexts stable and well-defined for 2+ years |
| Failure tolerance | Strict consistency required across all operations | Eventual consistency acceptable for most operations |
| Org structure | Single team, shared codebase ownership | Conway's Law already forcing service boundaries |
| Traffic profile | Predictable, steady-state | Highly variable, component-specific spike patterns |
| Data model | Shared relational model works well | Each domain clearly owns separate, independent data |
Conway's Law: organisations design systems that mirror their communication structure. Microservices work best when your org chart already matches the service boundary you want.
•When Microservices Are Genuinely the Right Answer
This article is not an argument that microservices are always wrong. They are the correct architectural choice in a specific, narrow set of circumstances — and the companies that first popularised them (Netflix, Amazon, Google) genuinely inhabit those circumstances. The problem was not the pattern; it was the indiscriminate application of a pattern designed for 500-person engineering organisations to teams of five building their first product.
The mistake was never choosing microservices — it was choosing them as a default instead of as the answer to a problem the team actually had.
- Organisational independence: when you have 50+ engineers on a system and the coordination overhead of shared codebase ownership is measurably slowing down feature velocity, service boundaries create genuine team autonomy.
- Heterogeneous scaling requirements: when one component (video transcoding, payment processing, real-time bidding) genuinely needs to scale to 100x the rest of the system, independent deployment units are necessary.
- Technology heterogeneity: when a specific component genuinely benefits from a different runtime — a latency-sensitive service needing Rust, a ML pipeline needing Python — service separation is justified.
- Fault isolation requirements: when a non-critical feature failing must be guaranteed not to crash the core product — circuit breaking and bulkhead isolation across service boundaries is the correct tool.
- Regulatory compliance boundaries: when different parts of a system must meet different compliance regimes (PCI-DSS for payments, HIPAA for health data) and isolation is a legal requirement, not an engineering preference.
•The Extraction Path: Monolith to Services When Ready
A modular monolith built with clean boundaries is not a dead end — it is the correct first step on a path that can lead to selective service extraction when the genuine signals for doing so appear. The extraction process from a well-structured monolith is dramatically less risky than decomposing a tightly coupled legacy codebase.
The Strangler Fig PatternA migration strategy, named by Martin Fowler, where new functionality is built as independent services while existing monolith functionality is replaced incrementally, with a proxy routing traffic to old and new implementations during the transition. — named by Martin Fowler after the fig tree that grows around an existing tree, eventually replacing it — provides the canonical migration path. New functionality is built as an independent service from the start, and existing monolith functionality is migrated incrementally, one bounded context at a time, with an API gateway routing requests to old and new implementations simultaneously during the transition.
Strangler Fig Migration Pattern
All Traffic
API Gateway / Reverse Proxy
Monolith
Legacy routes still handled here
New Service
Extracted module, now independent
New Service (standalone)
Monolith route decommissioned
Comparative Summary Matrix
•Comparative Summary Matrix
Architecture is not a binary choice between monolith and microservices — it is a spectrum, and the right position on that spectrum is determined by your organisation's size, your domain's complexity, and the genuine scaling pressures your system faces.
| Dimension | Single Monolith | Modular Monolith | Mini-Services (2–5) | Microservices (10+) |
|---|---|---|---|---|
| Deployment complexity | Minimal (1 artifact) | Minimal (1 artifact) | Low (2–5 pipelines) | High (N pipelines + Kubernetes) |
| Local dev setup | 1 command | 1 command | docker-compose | Minikube / k3d + 10+ containers |
| Inter-component latency | ~1 ns (function call) | ~1 ns (function call) | ~1 ms (local network) | ~1–10 ms (network + serialisation) |
| Distributed transactions | Native ACID | Native ACID | Saga Pattern needed | Saga Pattern required everywhere |
| Debugging / observability | 1 log stream, 1 stack trace | 1 log stream, 1 stack trace | 2–5 logs, basic tracing | Distributed tracing required (Jaeger/Zipkin) |
| Independent scaling | Not possible | Not possible | Limited (by service) | Full per-service autoscaling |
| Team autonomy | Low (shared codebase) | Medium (module ownership) | High (per-service teams) | Full (independent deploy cadence) |
| Operational overhead | Very low | Very low | Moderate | Very high (service mesh, K8s, etc.) |
| Right org size | 1–10 engineers | 5–50 engineers | 20–100 engineers | 50–500+ engineers |
| Failure blast radius | Full system on crash | Full system on crash | Partial (circuit breakers) | Minimal (fault isolation per service) |
ns = nanoseconds · ms = milliseconds · ACID = Atomicity, Consistency, Isolation, Durability · K8s = Kubernetes · N = number of services
•Conclusion: Architecture is a Tool, Not an Identity
The microservices hangover is real — measurable in the AWS bills that Amazon Prime Video cut by 90%, in the $3.2M that 37signals recovered by owning their own hardware, in the incident post-mortems that trace cascading failures across a dozen services back to one misconfigured timeout. But the hangover is not an indictment of distributed systems as a concept. It is an indictment of the cargo-culting of an organisational scaling solution into contexts where the organisation never needed to scale in the first place.
Shopify did not fail to adopt microservices. Shopify made a deliberate, informed choice to invest its engineering complexity budget in product depth rather than distributed infrastructure — and built one of the most reliable, high-throughput commerce platforms in the world as a result.
The correct engineering disposition is neither reflexively pro-monolith nor reflexively pro-microservices. It is to understand what problem each pattern solves, to be honest about whether your organisation actually has that problem today, and to choose the simplest architecture that your current scale genuinely requires — while building it with the internal modularity that keeps all future options open.
Start with a modular monolith. Extract services when — and only when — you have a genuine, measured reason to. The companies that got microservices right did exactly that. The companies that are quietly returning to monoliths skipped the first step.
