~/blog · Architecture

Migrating a .NET monolith to microservices: what I'd do differently

06 May, 2026 · 03 Mins read

The monolith was a ten-year-old ASP.NET application: order management, billing, catalog, and a sprawling admin module sharing one SQL Server database and one IIS deployment. Releases took a weekend. A bad migration in catalog could lock billing tables. The case for breaking it up was real, but the way we broke it up the first time made things worse before it made them better. Here is what I would do differently.

Extract one bounded context first, end to end

Our first instinct was to draw the whole target architecture on a whiteboard and start carving services in parallel. Three teams, three services, one shared database for “now”. Within two months we had three deployable units that were still coupled at the schema level, plus a new operational surface with none of the original benefits.

The version that actually worked started with billing only. We took the smallest bounded context with the clearest contract, gave it its own schema, and let it own its data behind a thin contract API. Everything else stayed in the monolith. That single extraction taught us more about our real coupling than the whiteboard ever did, and it shipped value because billing was the team that needed independent release cadence the most.

If I were starting again, I would resist the urge to plan more than one extraction at a time. Pick the context with the strongest case for autonomy and prove the seam works in production for a quarter before touching the next one.

Don’t split the database eagerly

The biggest mistake was splitting the database before splitting the code. We thought a clean schema separation would force the boundaries. What it actually did was push every join into application code, mostly poorly, and create a class of bugs that did not exist before: cross-service consistency failures during ordinary deploys.

The order I would follow now is module first, schema second, database third. Use schemas inside the same database as a soft boundary while you stabilize the module’s contract. Only promote to a separate database when you have a real reason: independent scaling, different durability requirements, or a compliance boundary. “It feels cleaner” is not a reason.

Use IdentityServer (Duende) and Aspire from day one

Two pieces of infrastructure earned their place quickly. The first was a real OIDC provider. We standardized on Duende Identity Server, which gave us one place to model clients, scopes, and token lifetimes. Trying to share authentication cookies across services was the kind of bad idea that looks fine on a Tuesday and breaks on a Friday night.

The second was .NET Aspire for the local developer environment. Before Aspire, every new engineer spent a day wiring connection strings, queues, and seed data. With an Aspire AppHost, F5 brought up the relevant services with their dependencies, and resource references were typed. It is a developer-environment tool, not a deployment tool, but it removed a category of friction that had been silently taxing every change.

Treat tracing as load-bearing, not optional

We added OpenTelemetry six months in, after the third incident where nobody could explain a 4-second tail latency. Adding tracing to a system that was not built for it is painful: instrumentation gaps, missing baggage, half-correlated logs.

If I were doing this again, distributed tracing would land in the first service extraction, before the second one. Spans across HTTP and message-broker hops, exemplars on metrics, and a single backend the on-call rotation actually opens. Without it, you are operating microservices on vibes.

Defer the gateway

We stood up an API gateway too early because the diagrams said so. For months it was a routing layer with no auth offload, no rate limiting worth the name, and an extra hop in every request. A gateway is justified the day you have a concrete cross-cutting requirement (centralized auth, throttling, request shaping). Until then, a reverse proxy and a service registry will do.

What I would tell past me

Extract one context, leave the database alone, ship auth and tracing as foundations, and only add gateways and brokers when the pain is concrete. The interesting part of microservices is not the architecture diagram. It is the operational maturity that has to grow underneath it.