Most teams don't have scale problems. They have complexity problems. And splitting an application into twenty services before understanding the domain just multiplies that complexity.

In a lot of teams, actual feature work is maybe a third of the day. The rest goes to debugging network latency between services, figuring out why an event from one service didn't trigger another, managing separate CI/CD pipelines, writing HTTP clients just to fetch data from a database that sits three services away. This is the cost of distribution. We pay it assuming we're buying scale.

There's an alternative. David Heinemeier Hansson called it the Majestic Monolith. A single system, managed by a single team, deployable as a single unit, but architected with clear boundaries. The word "monolith" carries baggage in .NET circles. We hear it and picture spaghetti code, a big ball of mud where everything depends on everything.

But that's not the monolith's fault. That's the fault of how we've been organizing code.

The standard approach is horizontal. Controllers in one folder, services in another, repositories in a third. A single feature like placing an order gets scattered across five different places. To understand what happens when someone places an order, you're jumping between folders, tracing interfaces, holding the whole chain in your head.

Vertical slices flip this. Instead of organizing by technical concern, you organize by feature. Everything related to placing an order lives in one folder. The handler, the request, the validation, the persistence. If you want to delete that feature, you delete the folder. If you want to understand it, you read one folder.

This matters because working memory is finite. In a distributed system, understanding a single operation means holding the state of multiple services, a message queue, maybe a cache, all in your head at once. In a modular monolith, you open the handler and read the story top to bottom. One F5 to debug.

There's also the physics of it. Services talk over HTTP or gRPC. That means serialization, network hops, timeout handling. Modules in a monolith talk through method calls. Nanoseconds instead of milliseconds. No network errors to handle. Transactions that actually work.

The common objection is growth. What happens when the system gets too big? The assumption behind the question is that you'll eventually need to extract modules into separate services. But that's already accepting the microservices premise — that distribution is the destination and monoliths are just a waypoint. The simpler answer is horizontal scaling. Run multiple instances of the same application behind a load balancer. Add read replicas when the database becomes the bottleneck. Put a cache in front of the paths that get hit hardest. This isn't a stopgap. It's how some of the busiest systems on the internet actually work.

Stack Overflow serves millions of requests on a straightforward architecture — nine web servers, a couple of SQL Server clusters, a monolith on IIS. Shopify still runs a modular monolith. Basecamp still does.

The extraction argument assumes your modules will have wildly different scaling needs — ordering handling thousands of requests per second while catalog sits idle. In practice, load correlates more than we expect. When people buy more, they also browse more. And even when there's genuine imbalance, scaling the whole system and accepting some underutilization is often cheaper than paying the distributed systems tax.

The actual ceiling for most monoliths isn't performance. It's coordination. When you have hundreds of engineers and deployments start requiring cross-team meetings, splitting makes sense. But that's a people problem, not a systems problem. Most teams won't get there.

For most teams, the honest answer to "what if we outgrow it" is: you probably won't. And if you do, that's a good problem to solve with the information you'll have then, not the assumptions you're making now.