Modernizing the Monolith: Migration Strategy & .NET 10 Architecture — Part 1
If you are reading this, you are likely staring at a legacy ASP.NET monolith with server-rendered Razor pages, feeling a mix of pride for…
Modernizing the Monolith: Migration Strategy & .NET 10 Architecture — Part 1

Modernizing the Monolith: Migration Strategy & .NET 10 Architecture — Part 1
If you are reading this, you are likely staring at a legacy ASP.NET monolith with server-rendered Razor pages, feeling a mix of pride for its longevity and anxiety about its future. It has served your business well, but the cracks are showing: scaling is expensive, deployments are risky, a single bug in the shopping cart module can bring down the entire inventory system, and your UX team is begging for a modern single-page application experience.
I recently led the migration of a high-volume Point-of-Sale (POS) and e-commerce platform from a monolithic application to a distributed microservices architecture on Azure, leveraging the latest .NET 10 and a brand new React 19 frontend. This article provides the roadmap we followed, focusing on the commercial realities and technical bottlenecks you will face during this dual transformation.
This document is the third installment in a comprehensive five-part series on modernizing a monolithic application. It details the journey from containerization to cloud deployment, building on the foundational strategies and deep dives from the previous parts.
Here’s a quick overview of the entire series:
**Modernizing the Monolith: Migration Strategy & .NET 10 Architecture — Part 1**
Focus: The initial blueprint for breaking down the monolith, including architectural design principles and setting up a .NET 10 foundation.
**Modernizing the Monolith: State, Async & Microservices Deep Dive — Part 2**
Focus: A deep dive into the core technical challenges of microservices, such as managing state, implementing asynchronous communication patterns, and designing the service boundaries.
**Modernizing the Monolith: Docker, CI/CD & Azure Cloud Deployment — Part 3**
Focus: The practical steps to package, deploy, and scale the newly created microservices. This document covers multi-stage Dockerfiles, CI/CD pipelines in Azure DevOps, and deployment strategies on Azure (AKS and Container Apps)
**Modernizing the Monolith: Kubernetes on AKS Production Guide — Part 4**
Focus: A production-focused guide to managing the microservices on Azure Kubernetes Service (AKS), covering topics like networking, security, and operational best practices.
Modernizing the Monolith: React 19 Security, Performance & Best Practices — Part 5 — Comming soon
Focus: The final piece, modernizing the front-end with React 19, ensuring it is secure, performant, and integrates seamlessly with the new backend architecture.
The Starting Point: The Legacy Monolith
Let’s set the scene. The existing system is a typical beast built on the Microsoft stack:
- Presentation Layer: ASP.NET Web Forms/MVC with Razor views, jQuery sprinkles, and server-rendered HTML.
- Tech Stack: .NET Framework 4.7.2.
- Data Layer: A single SQL Server database accessed via Entity Framework 6.x (Database-First).
- State Management: Reliance on In-Process Sessions and Cookies for user cart, authentication, and UI state.
- Integration: Exposes SOAP/ASMX endpoints for POS terminals. External partners connect via a VPN tunnel.
- Deployment: Deployed as a single unit on IIS (Web Server) or an Azure VM.
The Bottlenecks & Commercial Drivers
Before writing a single line of new code, we identified the critical pain points that justified the migration:
- Deployment Risk: “Hotfix Tuesdays” became a nightmare. A tiny change to the Returns Policy required a full regression test of the entire site, including UI and backend.
- Scalability Inefficiency: We couldn’t scale the POS module independently during peak retail hours without also scaling the entire admin UI and frontend assets.
- State Management Hell: Sticky sessions required by the Session object made resilient cloud deployments difficult. A pod failure meant lost user sessions and abandoned carts.
- External Integration Friction: The VPN requirement for third-party logistics partners was a security and management overhead. They needed modern, secure REST APIs.
- UI Limitations: The server-rendered Razor pages created a poor mobile experience, required full page refreshes, and made our developers miserable with jQuery spaghetti code.
- Vendor Lock-in: The .NET Framework version prevented us from using the latest cloud-native libraries and performance features.
The Roadmap: A Phased Migration Strategy with Zero Downtime
We adopted the Strangler Fig Pattern combined with a Micro-frontend approach. We didn’t rewrite; we gradually replaced pieces of the monolith while keeping the site live. Here is the comprehensive roadmap:
Phase 0: Foundation & Anti-Corruption Layer (Weeks 1–4)
- Setup Azure infrastructure and resource groups.
- Create a new
**Facade** API (BFF - Backend for Frontend) that sits in front of the monolith. - Establish the React 19 build pipeline and component library.
Phase 1: State Extraction & Authentication Modernization (Weeks 5–8)
- Move Sessions out-of-process to Redis Cache.
- Migrate from cookie-based Forms Authentication to JWT-based authentication.
- Make all state interactions explicitly RESTful instead of relying on server-side session magic.
Phase 2: UI Decomposition with React 19 (Weeks 9–16)
- Identify the first UI module (e.g., Product Catalog) to rewrite in React.
- Serve React components alongside existing Razor pages using micro-frontend techniques.
- Establish the BFF pattern for API composition.
Phase 3: Capability Extraction (Weeks 17–24)
- Extract the first backend bounded context (e.g., Product Catalog) as a .NET 10 service.
- Point the React UI to the new service via the BFF.
- Introduce the SAGA pattern for distributed transactions.
Phase 4: Data Separation & Event-Driven Communication (Weeks 25–32)
- Split the monolithic database into domain-specific databases.
- Implement event-driven synchronization using Azure Service Bus.
- Migrate additional domains (Orders, Inventory, Customers).
Phase 5: Full Orchestration & Decommission (Weeks 33–40)
- Route all traffic to new services.
- Turn off the monolith and celebrate.
The “Why” Behind Our Architectural Choices
To illustrate, let’s look at a core business process: User Authentication and Shopping Cart Management.
State Management Transformation: From Sessions to RESTful JWT
The Problem: The legacy app used HttpContext.Session to store the user's cart. This required sticky sessions (a nightmare in the cloud) and mixed UI state with business state.
The Solution: We moved to a stateless JWT authentication with a RESTful cart API. The cart becomes a resource, not a session variable.
Existing Code (Monolith — .NET Framework 4.7):
// Login - Sets cookie and session
public ActionResult Login(string username, string password)
{
// Validate user
if (IsValidUser(username, password))
{
FormsAuthentication.SetAuthCookie(username, false);
Session["Cart"] = new List<CartItem>(); // Initialize cart in session
Session["LastActivity"] = DateTime.Now;
return RedirectToAction("Index", "Home");
}
return View();
}
// Add to Cart - Relies on Session
public ActionResult AddToCart(int productId)
{
var cart = Session["Cart"] as List<CartItem>;
if (cart == null)
{
cart = new List<CartItem>();
Session["Cart"] = cart;
}
cart.Add(new CartItem { ProductId = productId, Quantity = 1 });
// Return the updated cart partial view
return PartialView("_CartWidget", cart);
}
Refined Code (React 19 + .NET 10 — RESTful + JWT):
First, the .NET 10 JWT setup:
// Program.cs - .NET 10 JWT Configuration
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
// Extract token from cookie or Authorization header
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["access_token"];
return Task.CompletedTask;
}
};
});
Now, the RESTful Cart API (.NET 10):
// CartController.cs - .NET 10 Minimal API with RESTful endpoints
app.MapGroup("/api/cart")
.MapCartEndpoints()
.RequireAuthorization();
public static class CartEndpoints
{
public static RouteGroupBuilder MapCartEndpoints(this RouteGroupBuilder group)
{
group.MapGet("/", async (ICartRepository cartRepo, ClaimsPrincipal user) =>
{
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var cart = await cartRepo.GetCartAsync(userId);
return Results.Ok(cart ?? new CartDto());
});
group.MapPost("/items", async (AddItemRequest request,
ICartRepository cartRepo,
ClaimsPrincipal user) =>
{
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var cart = await cartRepo.AddItemAsync(userId, request.ProductId, request.Quantity);
// Return 201 with location header and the updated cart
return Results.Created($"/api/cart", cart);
});
group.MapDelete("/items/{productId}", async (int productId,
ICartRepository cartRepo,
ClaimsPrincipal user) =>
{
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
await cartRepo.RemoveItemAsync(userId, productId);
return Results.NoContent(); // RESTful - DELETE returns 204
});
return group;
}
}
And the React 19 frontend consuming this RESTful API:
// CartContext.tsx - React 19 with modern state management
import React, { createContext, useContext, useOptimistic, useTransition } from 'react';
interface CartItem {
productId: number;
name: string;
price: number;
quantity: number;
}
interface CartContextType {
cart: { items: CartItem[]; total: number };
addToCart: (productId: number, quantity: number) => Promise<void>;
removeFromCart: (productId: number) => Promise<void>;
isPending: boolean;
}
const CartContext = createContext<CartContextType | null>(null);
export const CartProvider = ({ children }: { children: React.ReactNode }) => {
const [cart, setCart] = React.useState<{ items: CartItem[]; total: number }>({ items: [], total: 0 });
const [isPending, startTransition] = useTransition();
// React 19 useOptimistic for instant UI updates
const [optimisticCart, addOptimisticUpdate] = useOptimistic(
cart,
(state, action: { type: 'add' | 'remove'; productId: number; quantity?: number }) => {
if (action.type === 'add') {
const existingItem = state.items.find(i => i.productId === action.productId);
if (existingItem) {
return {
...state,
items: state.items.map(i =>
i.productId === action.productId
? { ...i, quantity: i.quantity + (action.quantity || 1) }
: i
),
total: state.total + (action.quantity || 1) * 100 // placeholder price
};
}
// Add new item logic...
}
return state;
}
);
const addToCart = async (productId: number, quantity: number) => {
// Optimistic update
addOptimisticUpdate({ type: 'add', productId, quantity });
// Actual API call
const response = await fetch('/api/cart/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId, quantity }),
credentials: 'include' // Include cookies for JWT
});
if (response.ok) {
const updatedCart = await response.json();
startTransition(() => {
setCart(updatedCart); // Replace with actual server state
});
}
};
return (
<CartContext.Provider value={{ cart: optimisticCart, addToCart, removeFromCart, isPending }}>
{children}
</CartContext.Provider>
);
};
// CartWidget.tsx - React 19 Server Component compatible
export function CartWidget() {
const { cart, isPending } = useContext(CartContext)!;
return (
<div className={`cart-widget ${isPending ? 'opacity-50' : ''}`}>
<button onClick={() => {/* open cart drawer */}}>
Cart ({cart.items.reduce((sum, i) => sum + i.quantity, 0)} items)
</button>
<div className="cart-total">${cart.total.toFixed(2)}</div>
</div>
);
}
Zero Downtime: The Micro-frontend Approach
We couldn’t afford to rewrite the entire UI at once. We used Module Federation (via Webpack 5) to serve React 19 components inside the existing Razor pages.
Step 1: The Bootstrap Page
<!-- Existing Razor View (_Layout.cshtml) -->
<!DOCTYPE html>
<html>
<head>
<title>My Site</title>
<!-- Legacy CSS -->
<link rel="stylesheet" href="/legacy/styles.css" />
<!-- New React component loader -->
<script src="/dist/react-bootstrap.js"></script>
</head>
<body>
<header>
<!-- Legacy header with Razor -->
<div class="legacy-nav">@Html.Partial("_Navigation")</div>
<!-- New React Cart Widget takes over this div -->
<div id="react-cart-widget"
data-user="@Json.Encode(Model.UserInfo)"
data-cart="@Json.Encode(Model.CartData)">
</div>
</header>
<main>
@RenderBody() <!-- Existing Razor content -->
</main>
<!-- React mounting script -->
<script>
window.addEventListener('load', function() {
if (window.mountCartWidget) {
window.mountCartWidget('react-cart-widget');
}
});
</script>
</body>
</html>
Step 2: The React Bootstrap
// bootstrap.tsx - React 19 entry point
import React from 'react';
import { createRoot } from 'react-dom/client';
import { CartWidget } from './components/CartWidget';
import { CartProvider } from './context/CartContext';
// Expose mount functions globally
(window as any).mountCartWidget = (elementId: string) => {
const element = document.getElementById(elementId);
if (!element) return;
// Parse data attributes from the element
const initialCart = JSON.parse(element.dataset.cart || '{}');
const root = createRoot(element);
root.render(
<React.StrictMode>
<CartProvider initialCart={initialCart}>
<CartWidget />
</CartProvider>
</React.StrictMode>
);
};
This approach allowed us to incrementally replace UI components while maintaining a fully functional site. The React components communicated with the BFF API, while the legacy Razor pages continued talking to the monolith.
The Proposed Architecture (Mermaid Diagram)

The Proposed Architecture (Mermaid Diagram)
Here is how the new system is structured. We chose this design to ensure loose coupling, resilience, and a modern UI experience.
Why this Architecture?
- React 19 Frontend: Provides a responsive, mobile-first experience with features like Server Components and
useOptimisticfor instant UI updates. Hosted on Azure Static Web Apps or CDN for global low-latency delivery. - Backend for Frontend (BFF) Pattern: A dedicated .NET 10 API that aggregates data from multiple microservices specifically for the React UI. This prevents over-fetching and reduces client-side complexity.
- JWT with Redis: Authentication is stateless with JWT, but we cache user sessions and cart data in Redis for performance. The cart becomes a RESTful resource, not a session variable.
- Service Bus: We chose Azure Service Bus over direct HTTP calls to ensure reliability. If the Inventory service is down, the message remains queued until it recovers.
- Separate Cart Service: By extracting the cart into its own service with Redis persistence, we achieved high availability and removed the dependency on sticky sessions.
Tackling the Hard Parts: Backward Compatibility & Integration
- NuGet & Third-Party SDKs: We created an “Anti-Corruption Layer” (ACL). The new .NET 10 services do not reference old third-party DLLs directly. Instead, they talk to a small translation service that handles the legacy protocols or data formats.
- VPN & External Access: We replaced VPN access with Azure API Management. This provides a secure gateway with rate limiting, JWT validation, and transformation of modern REST calls to legacy SOAP calls for the monolith (during transition).
- Database Interop: During migration, data lived in two places. We used Azure Data Sync and Change Data Capture (CDC) to keep the new microservice databases eventually consistent with the legacy monolith’s database until the cutover.
- UI Coexistence: The Module Federation approach ensured that as we migrated features to React, the legacy Razor pages continued to function perfectly. We used feature flags to gradually roll out new React components.
.NET 10 and Azure: The Cloud-Native Sweet Spot
.NET 10 (and its associated EF Core 10) is a game-changer for this migration:
- Native AOT: Our POS microservices start in milliseconds, perfect for Azure Functions’ Consumption Plan.
- Built-in OpenTelemetry: .NET 10 has first-class support for OpenTelemetry. By simply adding a NuGet package, we got distributed tracing flowing into Application Insights, allowing us to track a request from the React UI through the BFF, through Service Bus, and across three different microservices.
- Azure AI Integration: We enabled Azure’s AI-powered detection to automatically alert us to anomaly detection in transaction failure rates. The React frontend also sends Real User Monitoring (RUM) data to Application Insights, giving us end-to-end visibility.
- React 19 + .NET 10: The combination of React Server Components (for fast initial loads) and .NET 10 minimal APIs (for backend efficiency) creates a modern, high-performance stack.
The Final Decision: Scaling Options
As a system architect, you have choices. Here are the three paths we evaluated for hosting the new .NET 10 services and React 19 frontend:
Frontend Hosting Options
OptionProsConsPick This If…Azure Static Web AppsGlobal CDN, integrated API support, automatic CI/CDLimited server-side rendering capabilitiesYour React app is fully client-side or uses Static Site GenerationAzure Storage Static Website + CDNCheap, simple, global distributionNo built-in authentication or API routingYou have a simple marketing site or documentationVercel/NetlifyExcellent React 19 support, serverless functionsOutside Azure ecosystem, compliance concernsYou want the best developer experience for React
Backend Hosting Options
Option 1: Azure App Service / Azure Functions (PaaS)
- Pros: Simplest operational overhead. Auto-scale rules are easy to set. Perfect for the Cart API which has predictable traffic spikes.
- Cons: Slow to scale up (minutes, not seconds). Less control over the underlying VM.
- Pick this if: You have a small DevOps team and want to focus on code, not infrastructure.
Option 2: Azure Kubernetes Service (AKS)
- Pros: Fine-grained control over scaling (Horizontal Pod Autoscaling based on custom metrics). Can run multiple services on one cluster, saving cost at scale. Best for the stateless Product API and the complex SAGA orchestrators.
- Cons: Steep learning curve; you must manage the cluster control plane.
- Pick this if: You have a dedicated platform team and need maximum flexibility.
Option 3: Container Apps (Azure Container Apps)
- Pros: The “sweet spot.” It gives you Kubernetes-style scaling (including scaling to zero based on KEDA) without the cluster management complexity. Perfect for our event-driven consumers (like the Inventory Reservation consumer triggered by Service Bus).
- Our Recommendation: We used a hybrid. Container Apps for the event-driven consumers, AKS for the stable, always-on APIs, and Azure Static Web Apps for the React frontend.
Conclusion
Migrating a .NET 4.7 monolith with Razor views to .NET 10 microservices and React 19 on Azure is not just a “lift and shift.” It is a complete business transformation. By strategically tackling state management with JWT and RESTful APIs, ensuring reliable communication with Service Bus, modernizing the UI with React 19, and embracing the strangler pattern with micro-frontends, we turned a fragile giant into a resilient, observable, and independently scalable ecosystem.
The journey requires discipline, especially regarding data consistency (Sagas are your friend) and UI coexistence (Module Federation is your savior), but the payoff — faster time-to-market, zero-downtime deployments, modern user experience, and optimal cloud spend — is immense.
Ready to start your own migration? The first step is to draw the boundaries around your first domain and your first UI component. Start with the Product Catalog and the Product Listing page — they’re usually the most independent and provide immediate visible value to stakeholders.
This is Part 4 of the “Modernizing the Monolith” series — a comprehensive guide to transforming legacy ASP.NET applications into cloud-native microservices
📚 Modernizing the Monolith Series 🔹 Modernizing the Monolith: Migration Strategy & .NET 10 Architecture — Part 1
🔹 Modernizing the Monolith: State, Async & Microservices Deep Dive — Part 2
🔹 Modernizing the Monolith: Docker, CI/CD & Azure Cloud Deployment — Part 3
🔹 Modernizing the Monolith: Kubernetes on AKS Production Guide — Part 4
🔹Modernizing the Monolith: React 19 Security, Performance & Best Practices — Part 5
Coming soon! Want it sooner? Let me know with a clap or comment below
� Questions? Drop a response — I read and reply to every comment. 📌 Save this story to your reading list — it helps other engineers discover it. 🔗 Follow me →
- **Medium** — mvineetsharma.medium.com
- **LinkedIn** — www.linkedin.com/in/vineet-sharma-architect
In-depth .NET, Node.js, Python, Cloud Architecture, and System Design. New articles weekly
메타데이터
- post_id
- 139f1dce67da
- slug
- modernizing-the-monolith-migration-strategy-net-10-architecture-part-1-139f1dce67da
- url
- https://blog.devgenius.io/modernizing-the-monolith-migration-strategy-net-10-architecture-part-1-139f1dce67da
- canonical_url
- https://blog.devgenius.io/modernizing-the-monolith-migration-strategy-net-10-architecture-part-1-139f1dce67da
- author_url
- https://medium.com/@mvineetsharma
- status
- ok
- fetched_at
- 2026-06-26 21:52:29