Every few years, the enterprise software industry lands on a new architectural pattern that promises to fix the problems left behind by the previous one. For the past decade, that pattern has been microservices. The pitch is straightforward: decompose your monolith into small, independently deployable services, each owned by a small team, each scaling independently.
It is a seductive idea. And for a specific class of problems, it is the right solution. For most enterprise .NET codebases, it is not.
What is actually wrong with your monolith
When teams say their monolith is "too big" or "hard to change," they rarely mean the deployment unit is the problem. They mean the architecture inside the deployment unit is the problem. Tightly coupled business logic. Shared mutable state across domain boundaries. Database tables that ten different features write to. A service layer that has become a 4,000-line class with no coherent responsibility boundary.
Microservices do not fix any of that. They distribute it across a network. The same coupling that made the monolith hard to change becomes a chain of synchronous HTTP calls between services. The shared mutable state becomes an eventual consistency problem you now have to reason about explicitly. The 4,000-line service class becomes four 1,000-line service classes that call each other across process boundaries, each with its own failure mode.
What microservices actually introduce
For a team already struggling with a messy monolith, the operational complexity of a microservices architecture is not a reasonable trade. You are adding:
- Distributed transaction management. Operations that were a single database transaction become a saga that can partially fail. You need compensating transactions, idempotency guarantees, and a dead letter queue. Most teams underestimate this until they see it in production.
- Network latency as a design constraint. A function call that took microseconds is now an HTTP call that takes milliseconds. That changes what is feasible in a hot path. It also means every inter-service call is a failure point.
- Service discovery, load balancing, circuit breakers. None of these exist for free. They require infrastructure, operational knowledge, and ongoing maintenance.
- Distributed tracing and observability. A stack trace in a monolith tells you exactly what happened. A request that touches eight services requires a correlation ID threaded through every call and a centralised logging infrastructure to make sense of it.
None of these are insurmountable. Large teams with mature DevOps practices handle them routinely. But a five-person team maintaining a .NET Framework 4.x back-office system is not that team.
The actual fix: modular architecture inside the monolith
The solution to architectural debt is architectural clarity, not architectural distribution. A modular monolith with proper bounded contexts, enforced dependency rules, and Clean Architecture layering solves the real problems without adding the operational complexity of microservices.
In practice, this means:
- Bounded contexts as namespaces. Each major domain area (claims, policies, billing, agents) lives in its own module with its own models, its own service interfaces, and no direct references to internals of other modules. Cross-module communication goes through defined contracts.
- Dependency inversion at module boundaries. Modules depend on abstractions, not concrete implementations. This is the single most effective technique for reducing coupling in a .NET codebase.
- No shared data models. The most common source of coupling in enterprise .NET codebases is a shared entity model that every feature in the system writes to. Separate the read and write models. Accept that duplication at the boundary is cheaper than coupling through a shared schema.
- Event-based communication for cross-domain side effects. When the billing module needs to react to a policy change, it should subscribe to a domain event, not be called directly by the policy module. This decouples the modules without requiring a message broker.
This approach produces a codebase where individual modules can be extracted into microservices later if the business actually needs independent scaling. It also produces a codebase that is far more maintainable than either the original monolith or a premature microservices decomposition.
When microservices do make sense
There are real scenarios where microservices are the correct architectural choice. They are rarer than the hype suggests, but they exist:
- Different parts of the system have genuinely different scaling requirements that cannot be addressed at the infrastructure level.
- Different teams need to deploy independently and the coordination cost of doing so in a monolith has become the dominant engineering constraint.
- Different components have different operational requirements: some need different runtimes, different technology stacks, or different security isolation boundaries.
Notice that none of these are "the monolith is hard to change." That is an architecture problem that microservices will make worse, not better.
The practical path forward
If you are looking at a legacy .NET codebase and wondering whether to modernise it or decompose it, the order of operations matters.
First, fix the architecture inside the monolith. Identify the domain boundaries. Enforce them. Eliminate the shared mutable state. Introduce domain events for cross-boundary side effects. Write tests around the critical paths so you have a safety net for the refactoring work.
Second, migrate the runtime. Moving from .NET Framework 4.x to .NET 8 is a significant undertaking, but it is far simpler in a well-architected codebase than in a tightly coupled one. It also unlocks the performance improvements and ecosystem support that make the system easier to operate.
Third, evaluate whether microservices extraction is actually warranted. With clean module boundaries already in place, this extraction is mechanical if you need it. In most cases, you will find that a well-architected modular monolith is the right answer for the business context and team size.
Modernise the architecture before you modernise the deployment model. The sequence matters.