Monolith to Microservices: A Practical Guide
Monolith to Microservices: A Practical Guide
Moving from a monolith to microservices: when it pays off, when it does not, the strangler-fig approach, data ownership.
Moving from a monolith to microservices pays off when separate teams block each other on one deployment, when parts of the system need to scale or fail independently, or when one component needs a different technology or release cadence from the rest. It does not pay off when the real problem is code quality, when one small team owns the whole system, or when there is no pipeline and no observability to run many services with. The approach that works for most organizations: fix the structure inside the monolith first (a modular monolith), then extract the few services that justify the cost, one at a time, with the strangler-fig pattern.

When not to move from monolith to microservices
Microservices are an organizational and operational trade: independent deployment and scaling in exchange for distributed-system complexity. The trade is bad in these situations.
- One team, one codebase. If the same five people change everything, services add network calls between code that used to be a method call, and nothing else.
- The pain is code quality. Tangled dependencies, no tests, a 40-minute build. Splitting a tangle across a network produces a distributed tangle. Refactor into modules first.
- No pipeline, no observability. One service deployed by hand is a chore; twelve are an outage. A working DevOps pipeline, central logging and tracing come before the first extraction.
- Unclear domain boundaries. If nobody can say which module owns “customer”, the service boundaries will be wrong, and wrong boundaries in microservices are far more expensive than wrong boundaries in a monolith.
- Uniform load. If everything scales together, a monolith on a scale set or an App Service plan scales just as well with none of the overhead.
- A platform with no upgrade path. Classic ASP or WebForms applications usually need a rebuild rather than a decomposition; see our guide to WebForms and Classic ASP modernization.
The signals that the trade is good: deployments queue behind other teams’ changes, one component (reporting, document generation, a public API) needs to scale or be isolated on its own, a part of the system has regulatory or security requirements the rest does not, or a specific capability needs a different stack.
Modular monolith first
A modular monolith is a single deployable application whose code is divided into modules with explicit boundaries: each module owns its data, exposes a small public contract, and is forbidden from reaching into another module’s internals. It gives most of the design benefits of microservices, clear ownership, enforced boundaries and testable seams, with one deployment, one database server and in-process calls.
In a .NET solution this means one project per module (Orders, Billing, Catalog), internal types by default with a public interface or a set of commands and events, an in-process mediator or event bus between modules, a separate schema per module in the same SQL Server database, and architecture tests (NetArchTest or ArchUnitNET) in the build that fail when a module references another’s internals. Teams working toward .NET Core microservices often find the modular monolith is where they should stop, because the boundaries are the hard part and the network is optional.
The payoff comes later: a module with its own schema, its own contract and no hidden coupling can be extracted into a service in days rather than months, and only if a real need appears.
Strangler-fig steps
The strangler-fig pattern replaces a system incrementally by routing traffic for one capability at a time to new code, until the old system has nothing left to do. For extracting services it runs like this.
- Put a router in front. A reverse proxy or API gateway (YARP for .NET, NGINX, Azure Application Gateway or Front Door, an AWS Application Load Balancer with path rules) receives all traffic and, for now, sends all of it to the monolith. Nothing changes for users.
- Pick the first candidate. Choose a capability at the edge of the system with few inbound dependencies and a clear reason to be separate: high change rate, a scaling profile unlike the rest, or a compliance boundary. Not the core order engine first.
- Build the service. An ASP.NET Core minimal API or worker in its own project, with its own pipeline, configuration, health checks and telemetry from day one. Keep the contract small and versioned.
- Add an anti-corruption layer. Where the new service must call into the monolith, or the monolith into it, do so through an explicit adapter rather than sharing classes, so the monolith’s model does not leak into the new code.
- Move the data for that capability. Give the service its own store (see the next section) and keep it in sync with the monolith’s tables during the transition.
- Shift traffic gradually. Route a percentage of requests, or a set of internal users, to the new service behind a feature flag. Compare results and errors against the old path before sending everything.
- Delete the old path. Remove the code and tables from the monolith once the service has carried all traffic through a full business cycle. A strangler that never deletes leaves two systems to maintain.
- Repeat, or stop. Extract the next candidate only if it has its own justification. A monolith with two or three services around it is a common and healthy end state.
Data
Data is where most microservice programs stall. The goal is a database per service, so each service can change its schema and scale its store without coordinating with the others. Getting there from a shared database happens in stages.
- Schema per module, shared server. Separate schemas inside the existing database, with cross-schema joins and foreign keys removed in favor of calls or events. This is the modular-monolith step and it costs no new infrastructure.
- Split the tables. Tables used by two modules are split by ownership; the non-owner keeps a read copy, refreshed by events or by change data capture (SQL Server CDC, Debezium), rather than a join.
- Synchronize during the transition. While both the monolith and the service write, use a transactional outbox so every write publishes an event reliably, and a reconciliation job that compares the two stores until the old path is deleted.
- Accept eventual consistency. A service that needs another’s data gets it from an event or a cached read model, slightly behind the source. Workflows that span services become sagas: a sequence of local transactions with compensating actions, not a distributed transaction.
- Rebuild reporting. Reports that joined everything in one database move to a read model or a warehouse fed by events, which is often the first visible benefit to the business.
Reference data (countries, product categories, tax codes) is the usual exception: publish it from one owner and cache it everywhere.
Ops cost
Each service adds a pipeline, a deployment target, configuration, secrets, dashboards, alerts and a place in the on-call rotation. Before the first extraction, the platform needs:
| Need | Typical choice for .NET teams |
|---|---|
| Runtime | Azure Container Apps or Amazon ECS for a handful of services; AKS or EKS when the count and isolation needs justify a cluster |
| Messaging | Azure Service Bus or Amazon SQS and SNS, through MassTransit or NServiceBus; RabbitMQ for self-hosted |
| Observability | OpenTelemetry traces, metrics and logs into Application Insights, Grafana or CloudWatch, with correlation IDs across services |
| Resilience | Timeouts, retries and circuit breakers through the .NET resilience extensions (Polly); health checks on every service |
| Local development | .NET Aspire or Docker Compose to run the set of services and dependencies on one machine |
| Contracts | Versioned APIs and events, consumer-driven contract tests in the pipeline |
| Security | Managed identities between services, a vault per environment, network policies, an API gateway for external traffic |
None of this is optional once services are in production, and all of it costs engineering time and cloud spend. That is the reason to extract only what earns it, and to keep the number of services small enough that one team can hold the whole picture. Microservices consulting that starts by proposing twenty services for a ten-person company is selling the platform, not solving the problem. Our broader comparison of legacy system modernization approaches places decomposition alongside the alternatives.
Where PS3G fits
PS3G does this work as part of application modernization: an assessment that maps your modules and data ownership, a roadmap that says which capabilities, if any, should become services and in what order, and incremental delivery behind a router with the pipeline, observability and data synchronization built in the first increment. For Microsoft stacks, our .NET migration and modernization team handles the move from .NET Framework to modern .NET as part of the same program, since a modular monolith on ASP.NET Core is usually the right first destination. We scope the work for organizations of every size and we say when a modular monolith is the better answer.
Related reading
- Legacy System Modernization: 6 Approaches Compared (and How to Pick)
- What Is a DevOps Pipeline? Stages, Tools and a Starter Setup
- ASP.NET WebForms and Classic ASP Modernization: Your Realistic Options
- Application modernization services from PS3G
Frequently asked questions
When is it worth moving from a monolith to microservices?
When independent deployment or scaling has a concrete value: teams waiting on each other’s releases, a component that needs its own scaling or isolation, a compliance boundary, or a capability that needs a different stack. If none of those apply, the cost of a distributed system outweighs the benefit, and a modular monolith delivers the structural improvements without the operational overhead.
What is a modular monolith?
A single deployable application divided into modules with enforced boundaries: each module owns its data and exposes a small contract, and the build fails if a module reaches into another’s internals. It keeps one deployment and one database server while giving clear ownership and testable seams. Modules extracted later become services quickly, because the boundary work is already done.
What is the strangler-fig pattern?
An incremental replacement technique: a router in front of the existing system sends traffic for one capability at a time to new code, while everything else still goes to the old system. Each capability is built, synchronized, tested with a share of traffic and then switched fully, after which the old code is deleted. The old system shrinks until it is gone or until what remains is fine as it is.
How do we split a shared database for microservices?
In stages: separate schemas per module on the same server, then split tables by ownership with read copies kept up to date by events or change data capture, then move the owning service to its own store with an outbox for reliable event publishing and a reconciliation job during the transition. Cross-service workflows become sagas with compensating actions rather than distributed transactions.
Do microservices require Kubernetes?
No. A few .NET services run well on Azure Container Apps, Amazon ECS on Fargate or Google Cloud Run, which provide containers, scaling, rollouts and service-to-service networking with far less to operate. Kubernetes becomes worthwhile when the number of services, the isolation requirements or the need for custom networking exceed what those platforms offer, and when the team has the skills to run it.
Talk to us about your project
Describe the monolith and what is slowing you down. We will reply with questions, then set up a short video meeting if it helps.
