The Majestic Monolith: Why You Probably Don't Need Microservices
It has become a rite of passage in the modern engineering lifecycle: a startup hits a certain scale, performance dips slightly, and a senior engineerβfresh from a conference or a stint at a FAANG companyβsuggests the inevitable: "We need to rewrite this as microservices."
It sounds professional. It sounds scalable. It sounds like what "serious" engineering teams do. But for 99% of companies, this isn't a solutionβit's a tax. It is the voluntary adoption of distributed systems problems before you have distributed systems benefits.
The Golden Rule of Architecture
"Don't distribute your system until you have a problem that literally cannot be solved any other way. Complexity is the enemy of execution."
The "Microservices Premium"
Martin Fowler, one of the original proponents of the pattern, explicitly warns against starting with it. When you move to microservices, you trade function calls (nanoseconds, reliable, in-memory) for network calls (milliseconds, unreliable, over-the-wire). That single decision introduces a new failure mode: the network.
The Fallacy of Distributed Computing
Developers often assume latency is zero and bandwidth is infinite. In a monolith, if the User Service calls the Billing Service, it's a memory pointer. In microservices, it's a serialized JSON payload sent over TCP/IP, potentially failing, requiring retry logic, circuit breakers, and exponential backoff.
The Data Consistency Nightmare
Goodbye `BEGIN TRANSACTION` and `COMMIT`. In distributed systems, you are forced into Eventual Consistency. You have to handle scenarios where a user is charged (Service A) but their inventory isn't reserved (Service B) because the message bus went down. Now you are writing Sagas and compensatory transactions.
The Alternative: The Modular Monolith
Defending the monolith isn't defending "Spaghetti Code" (a Big Ball of Mud). The sustainable alternative is the Modular Monolith. This architecture enforces strict logical boundaries between domains (Billing, Users, Inventory) within a single physical codebase.
What it looks like
Instead of organizing by layer (Controllers, Models, Views), you organize by feature.
src/ βββ modules/ β βββ billing/ // Only knows about Money β β βββ api/ // Public Interface β β βββ internal/ // Private Logic β β βββ tests/ β βββ inventory/ // Only knows about Stock β βββ users/ // Only knows about Auth βββ shared/ βββ infrastructure/ // Logger, EventBus
How to enforce it
You don't need microservices to prevent coupling; you need a linter. Tools like dependency-cruiser (JS) or ArchUnit (Java) can fail the build if `billing` tries to import `inventory` internals directly.
// .dependency-cruiser.js module.exports = { forbidden: [ { name: 'no-cross-module-imports', from: { path: 'modules/([^/]+)' }, to: { path: 'modules/$1', inverse: true // Can't touch other modules } } ] };
But... Does it Scale?
This is the most common counter-argument. "We need microservices to scale!" No, you probably don't. Stack Overflow runs on a monolithic .NET architecture. Shopify was a monolith for a decade. GitHub is a Rails monolith.
Horizontal Scaling Works
You can run 50 copies of your monolith behind a Load Balancer just as easily as you can run 50 microservices. If one endpoint is CPU intensive, you scale the whole set. Computing power is cheap; engineering hours spent managing Kubernetes orchestrators are expensive.
Database Scaling
The bottleneck is usually the database, not the application code. Microservices don't fix a slow SQL query. Read Replicas, caching (Redis), and proper indexing solve scale problems long before splitting the codebase does.
When to Actually Split
We are not dogmatic. There are valid reasons to extract a service, but they should be the exception, not the default. Extract a service only when:
- Hardware Isolation You have a video encoding feature that needs massive GPUs, while the rest of the app is simple CRUD.
- Language Requirements Your core app is Node.js, but you need a high-performance Data Science module written in Python/Pandas.
- Organizational Scale You have 300+ developers pushing to the same repo, and merge queues are taking 4 hours.
Spend Innovation Tokens Wisely
"Your customers do not care if your architecture is 'pure'. They care if the product works. We choose the monolith because it allows us to ship features, not infrastructure configuration."
Read our Engineering Principles