← All Posts

The Monolith Isn't the Enemy

Matthias Bruns · · 7 min read
architecture opinion microservices

The Microservices Kool-Aid

Somewhere around 2015, the software industry collectively decided that monoliths were bad. Legacy. Technical debt personified. If you weren’t breaking your application into dozens of independently deployable services communicating over HTTP and message queues, you were doing it wrong.

A decade later, I’m watching companies migrate back to monoliths. Amazon famously moved their Prime Video monitoring from microservices to a monolith and cut costs by 90%. Shopify runs one of the largest e-commerce platforms in the world on a monolithic Rails app. Basecamp has been a monolith since day one.

What happened? Reality happened.

The Microservices Tax

Microservices aren’t free. They come with a tax that every team pays, whether they acknowledge it or not:

Network Is Not Free

When your function call becomes an HTTP request, you inherit an entire category of problems that didn’t exist before:

  • Latency. A local function call takes nanoseconds. A network call takes milliseconds — at minimum. Multiply that by the 15 services your request touches, and you’ve added hundreds of milliseconds to every user interaction.
  • Failure modes. Networks fail. Services go down. Timeouts happen. Now every inter-service call needs retries, circuit breakers, fallbacks, and timeout configuration. That’s a lot of code that has nothing to do with your business logic.
  • Serialization overhead. JSON marshaling and unmarshaling on every boundary. Protocol buffers help, but they add their own complexity.
# A "simple" user profile request in microservices land:
Client → API Gateway → Auth Service → User Service → Profile Service
                                     → Preferences Service
                                     → Avatar Service (→ CDN)
       ← Aggregate response

# In a monolith:
Client → App → database query → response

# Same result. One has 6 network hops. One has zero.

Distributed Systems Are Hard

The moment you split your application across multiple services, you’re building a distributed system. That means dealing with:

  • Distributed transactions. Your monolith had database transactions. Now you need sagas, compensation logic, and eventually consistent state machines. Good luck explaining that to a junior developer.
  • Data consistency. Service A updated its database. Service B’s cache is stale. The user sees inconsistent data. This is a fundamental problem of distributed systems, and there’s no magic solution.
  • Debugging hell. A request fails. Which of the 12 services involved is the problem? Hope you have distributed tracing set up. And that it’s working. And that you can read the waterfall chart from 47 spans.

Operational Complexity

Each microservice needs:

  • Its own CI/CD pipeline
  • Its own deployment configuration
  • Its own monitoring and alerting
  • Its own logging
  • Its own scaling rules
  • Its own database (if you’re doing it “right”)

Five microservices means five of everything. Twenty microservices means twenty of everything. And a platform team to manage the platform that manages the services.

I’ve seen 8-person teams running 30+ microservices. They spent more time on infrastructure than on features. That’s not engineering — that’s self-inflicted operational pain.

When Monoliths Work

A well-structured monolith is a perfectly valid architecture for most applications. Here’s when it’s the right call:

Your team is small. If you have fewer than 20 engineers, the coordination overhead of microservices likely outweighs the benefits. A monolith lets everyone work in the same codebase, deploy together, and debug together.

Your domain is cohesive. If your services would constantly be calling each other, you don’t have independent services — you have a distributed monolith. That’s the worst of both worlds.

You’re still figuring out your domain. Microservice boundaries are extremely hard to get right. If you split too early, you’ll spend years refactoring service boundaries. A monolith lets you move fast and refactor freely until you understand where the real boundaries are.

Your data model is relational. If your services share a lot of data and you’d end up duplicating it across service databases or making constant cross-service queries, a single database with a monolith is simpler and more consistent.

The Well-Structured Monolith

The key word is well-structured. A monolith doesn’t have to be a big ball of mud. You can have clean architecture inside a single deployable:

/cmd
  /server          # Entry point
/internal
  /user            # User domain
    /handler.go    # HTTP handlers
    /service.go    # Business logic
    /repository.go # Data access
  /billing         # Billing domain
    /handler.go
    /service.go
    /repository.go
  /notification    # Notification domain
    /handler.go
    /service.go
    /repository.go
/pkg
  /middleware       # Shared middleware
  /database        # Database utilities

Each domain module has clear boundaries. Dependencies flow inward. Modules communicate through well-defined interfaces. You get 80% of the modularity benefits of microservices with none of the distributed systems tax.

This is what I call a “modular monolith,” and it’s the architecture I recommend for most projects we work on.

When To Actually Split

There are legitimate reasons to extract a service. But they should be driven by real constraints, not architectural fashion:

Independent Scaling Needs

Your image processing burns 10x the CPU of your API? That’s a real reason to split it out. You can scale the image processor independently without over-provisioning your API servers.

Different Runtime Requirements

Your main app is a Go HTTP server, but your ML model needs Python with GPU access? Different deployment targets are a valid reason for separate services.

Team Autonomy at Scale

You have 100+ engineers and teams are stepping on each other’s toes with conflicting deployments? Service boundaries aligned with team boundaries make sense — but only at that scale.

Fault Isolation

A bug in your payment processing should not take down your product catalog. If a component’s failure would cascade to unrelated functionality, extracting it provides genuine value.

Compliance Boundaries

PCI DSS requires your payment handling to be in a separate, audited environment? That’s a hard requirement, not a preference.

The Decision Framework

Before splitting anything, ask yourself:

  1. What specific problem does this solve? If you can’t name a concrete, current problem, don’t split.
  2. Can I solve it within the monolith? Better module boundaries, background job queues, and read replicas solve most scaling issues without microservices.
  3. Am I prepared for the operational cost? New CI/CD pipeline, new monitoring, new deployment, new on-call rotation. All of it.
  4. Will this service be truly independent? If it needs synchronous calls to three other services to handle a request, it’s not independent. It’s a distributed monolith piece.

If you pass all four checks, go ahead and extract. If not, keep it in the monolith and revisit later.

The Pragmatic Path

Here’s what I actually recommend to most clients:

  1. Start with a modular monolith. Clean domain boundaries, clear interfaces, single deployment.
  2. Identify real pain points over time. Not theoretical ones — actual, measurable problems.
  3. Extract surgically. One service at a time, when there’s a clear benefit.
  4. Keep the monolith as the core. Most of your application logic probably belongs together. Let it stay together.

This isn’t sexy. It won’t get you speaking slots at KubeCon. But it works. It ships features. It lets small teams move fast. And it doesn’t require a platform team just to keep the lights on.

The Real Enemy

The monolith isn’t the enemy. Poorly structured code is the enemy. And you can write poorly structured code in a monolith or across 50 microservices. At least with the monolith, you only have to debug it in one place.

Build the simplest thing that works. Split when you must, not when you can. And never let an architecture diagram on a conference slide dictate your engineering decisions.

Pragmatism beats dogma. Every time.

Reader settings

Font size