Skip to main content

On This Page

Draft / Scheduled Content

This article is a draft or scheduled for future publication. The content is subject to change.

Microservices: The Great Architecture Grift of the 2010s

6 min read
Share

The Distributed Monolith

Around 2014, a collective madness infected the software engineering industry.

Suddenly, the “Monolith” was declared a legacy relic. It was a big, scary, unmaintainable ball of mud. The future belonged to “Microservices”—small, independent, single-responsibility services communicating over the network. We were promised independent scaling, independent deployments, technology diversity, and clean domain boundaries.

So, teams of five developers carved up their simple web apps into twelve different services.

Ten years later, the bills have come due.

Instead of decoupled, agile microservices, most organizations ended up with a distributed monolith. They have all the coupling of a monolith (if you change a field in the User service, three other services break) but with all the complexity, latency, and failure modes of a distributed system.

We traded in-memory function calls for HTTP requests. It is one of the worst architectural bargains in the history of computer science.

The Microservices Tax

When you move your application boundaries from a single process to the network, you pay a massive tax:

1. Network Reliability

In a monolith, calling another module is a CPU instruction. It takes nanoseconds and it is guaranteed to succeed.

In microservices, calling another module is a network request. It can time out. It can fail because of packet loss. It can get rate-limited. It can fail because the target service is restarting.

Suddenly, your code is littered with retry logic, circuit breakers, backoff algorithms, and fallback handlers. You have to learn about the CAP theorem the hard way.

2. Data Consistency and the Death of JOINS

This is where microservices truly hurt. Each microservice must own its database.

Need to show a list of orders with the customer’s name and the product’s current stock? In a monolith, that is a simple SQL query:

SELECT o.id, u.name, p.stock 
FROM orders o 
JOIN users u ON o.user_id = u.id 
JOIN products p ON o.product_id = p.id;

In a microservices architecture, you can’t join across databases. You have to:

  1. Query the Order service for orders.
  2. Extract the user IDs and product IDs.
  3. Make an HTTP request to the User service to fetch users.
  4. Make another HTTP request to the Product service to fetch products.
  5. Manually stitch the data together in memory on the API Gateway or client.

If one of those requests fails, what do you show the user? What about transactions? If a user buys a product, you need to charge their card (Billing service), deduct inventory (Inventory service), and create an order (Order service). If the inventory deduction fails after the card is charged, how do you roll back?

You are forced to implement complex distributed transaction patterns like Sagas, Outbox patterns, and asynchronous message queues (RabbitMQ, Kafka). You are building a database engine rather than writing business logic.

sequenceDiagram
    participant Gateway
    participant OrderService
    participant InventoryService
    participant BillingService

    Gateway->>OrderService: Create Order
    OrderService->>InventoryService: Deduct Inventory
    alt Inventory is Available
        InventoryService-->>OrderService: Inventory Deducted
        OrderService->>BillingService: Charge Card
        alt Billing Fails
            BillingService-->>OrderService: Billing Failed
            OrderService->>InventoryService: Rollback/Compensate Inventory (Saga Pattern)
            OrderService-->>Gateway: Order Failed (Billing Error)
        else Billing Succeeds
            BillingService-->>OrderService: Card Charged
            OrderService-->>Gateway: Order Created Successfully
        end
    else Inventory is Out of Stock
        InventoryService-->>OrderService: Out of Stock
        OrderService-->>Gateway: Order Failed (Out of Stock)
    end

3. Operational Overhead

Instead of monitoring one application, you are monitoring twenty. You need centralized logging (ELK stack), distributed tracing (Jaeger/Zipkin) to figure out which service is slow, service discovery (Consul/DNS), and complex CI/CD pipelines to deploy everything.

You need a platform team just to keep the lights on.

The Myth of Technology Diversity

One of the main arguments for microservices was: “You can write the Billing service in Go, the Recommendation service in Python, and the frontend service in Node.js!”

In practice, this is a nightmare.

It means every developer has to be a polyglot, or your teams are completely siloed. Shared libraries (like database helpers or logging formats) have to be rewritten for every language. Security patches have to be applied to five different runtimes.

Most successful companies that use microservices eventually enforce strict language guidelines (e.g., “everything is Java” or “everything is Go”) to prevent this chaos. The technology diversity benefit turned out to be a trap.

The Modular Monolith: A Better Way

The problem with monoliths wasn’t the single process. The problem was bad code design. Developers wrote spaghetti code, coupling modules tightly together in memory.

But you don’t need to put a network boundary between modules to keep them clean. You just need discipline.

Enter the Modular Monolith.

In a modular monolith, your application is a single deployable unit, but its internal structure is strictly segregated. Modules communicate through defined, public interfaces (APIs). Database tables are logical segregated (using schemas or prefixes) and modules are not allowed to query other module’s tables directly.

Monolithic Process
+------------------------------------------+
|  User Module  -> [Public User API]       |
|       |                                  |
|       v (In-memory method call)          |
|  Order Module -> [Public Order API]      |
+------------------------------------------+

If you design your modular monolith correctly:

  • It is extremely fast (no network latency).
  • It is easy to test (no mock network services).
  • It is simple to deploy (a single container or binary).
  • If you ever need to scale a specific module, you can extract it into a separate service later because the logical boundaries are already clean.

It is far easier to split a clean monolith into microservices than it is to merge a mess of microservices back into a monolith.

Conway’s Law and Why Microservices Exist

Microservices were not invented to solve technical problems. They were invented to solve organizational problems.

Conway’s Law states that organizations design systems that copy their communication structures. When companies like Amazon or Netflix grew to thousands of developers, they could no longer work on a single codebase without stepping on each other’s toes. Merge conflicts, deployment queues, and coordination overhead were killing productivity.

Microservices allowed them to break the organization into independent teams that could ship code without coordinating with other teams.

If your team is less than 50 developers, you do not have this problem. You can coordinate easily. Splitting your app into microservices is solving a coordination problem you don’t have, by introducing technical problems you don’t want.

Choose the Monolith First

Don’t let the blogs of tech giants dictate your architecture. They are playing a different game with different resources.

Build a monolith. Keep it modular. Keep it clean. Spend your cognitive energy on building features that your users actually care about. If you ever reach the scale where the monolith is genuinely blocking your engineering velocity, you will have the money and the context to refactor it. Until then, stay in one piece.

Related Content