Modernizing .NET Legacy Systems: Replatforming, Refactoring, or Rewriting?

Posted by Rodrigo Castro on July 18, 2025

Replatforming, Refactoring, or Rewriting?

Modernizing a legacy application (like a .NET monolith) typically follows one of three paths:

🏗 Replatforming

Moving the application with minimal code changes, primarily by changing the hosting model. A common example is lifting an app from on-premises (like IIS for example) to containers running in Cloud, like AWS Fargate or Azure Container Apps. Highlights:

  • Low effort;
  • Fast to deploy;
  • Ideal when the application still serves its purpose but the infrastructure is outdated.

🔧 Refactoring

Improving internal code structure without changing its behavior. This may include introducing dependency injection, separating layers, applying DDD, and modernizing data access. Highlights:

  • Medium effort;
  • Preserves business rules;
  • Improves maintainability and testability.

🔁 Rewriting

Rebuilding the system from scratch, often using modern tech stacks and practices. Highlights:

  • High risk and cost;
  • Useful when the legacy code is beyond repair;
  • Allows redesign with modern architectures.

AWS Well-Architected Framework

This evaluation also aligns with the AWS 5R migration strategy, which categorizes modernization paths as Rehost, Replatform, Repurchase, Refactor, or Retire. Our fictional case focuses mainly on Replatforming (by containerizing the system and modernizing the hosting) and Refactoring (by redesigning the architecture and introducing clean separation of concerns), with a few parts being Rewritten to support modern patterns like multi-tenancy.

These efforts also reflect the pillars of the AWS Well-Architected Framework, especially Operational Excellence, Reliability, Performance Efficiency, and Cost Optimization, as well as the Microsoft Azure Well-Architected Framework, which emphasizes similar pillars such as Scalability, Resiliency, and DevOps alignment.

By following a structured and incremental modernization plan, we not only improve code and infrastructure, but also align the system with cloud-native architecture principles validated by leading cloud providers.

AWS Tools for .NET Modernization

Amazon provides several tools to help with .NET modernization:

✅ AWS App2Container

  • Scans .NET (and Java) applications running on IIS;
  • Generates Dockerfiles and deployment manifests;
  • Supports ECS, EKS, and Fargate.

🔍 AWS Application Discovery Service

  • Collects data from on-premises environments;
  • Useful for planning cloud migrations.

🧭 Porting Assistant for .NET

  • Analyzes .NET Framework apps;
  • Identifies APIs incompatible with .NET 6/8;
  • Estimates porting effort.

These tools can be combined to analyze, port, and containerize .NET apps.

From Monolith to Microservices

Breaking a monolith into microservices involves more than splitting projects. Key steps include:

  • Identifying bounded contexts using DDD (refer here if want to explore DDD);
  • Splitting services by business domain (e.g., Orders, Payments, Users);
  • Defining communication contracts (REST, gRPC, etc.);
  • Adopting asynchronous communication (e.g., SQS, SNS, EventBridge);

Example: Monolith running on IIS > Clean DDD + Microservices > Deployed in AWS Fargate

Clean Architecture & Domain-Driven Design

Clean Architecture separates:

  • Domain Layer: Entities, Value Objects, Aggregates
  • Application Layer: Use Cases, Services
  • Infrastructure Layer: Database, APIs, Messaging

DDD helps align code with business logic and reduces technical debt over time.

🔧 Fictional Case: Modernizing a Legacy Web + Batch System

Let’s imagine a legacy enterprise application built on .NET Framework (old version), hosted on IIS per Customer, with a backend component running as a scheduled task in Windows Task Scheduler. The system was originally designed for a single-tenant environment, tightly coupled to infrastructure assumptions. As part of a modernization initiative, the goals include:

  • Updating .NET and all dependencies to the latest supported versions;
  • Migrating to a container-based architecture;
  • Introducing support for multi-tenancy;
  • Decoupling infrastructure concerns from business logic;
  • Preparing for cloud-native deployment and scalability.

Step-by-step modernization:

  1. Upgrade .NET Framework to 4.8
    The latest version compatible with .NET Standard, ensuring existing partners can continue running the application on-premises while we modernize the architecture.

  2. Decouple business logic from the database
    Remove all Stored Procedures and Triggers, replacing them with C# logic in the repository and/or service layer to centralize domain behavior in the application codebase.

  3. Clean up MVC Controllers
    Enforce separation of concerns by:
    • Injecting only the required services into each controller,
    • Eliminating direct access to repositories or business logic from controller actions,
    • Ensuring all business logic lives in dedicated services.
  4. Introduce a REST API backend
    Refactor the architecture so the UI becomes a thin client that communicates exclusively with a RESTful API. All flows follow the pattern:
    UI → API Controller → Service → Repository → Database.

  5. Introduce multi-tenancy
    • Add a TenantId column to all relevant tables,
    • Implement a global query filter to automatically scope data by tenant,
    • Ensure all repository queries, validations, and listings enforce this boundary to prevent data leakage.
  6. Maintain and improve test coverage
    • Leverage the existing high unit test coverage,
    • Integrate automated coverage reporting into CI/CD to identify weak spots,
    • Reinforce TDD practices already in place to ensure all structural changes are properly tested.
  7. Improve deployment and traceability
    • Add version labels and digital signatures to build artifacts during CI/CD,
    • Apply checksum validation for JavaScript and static assets to prevent tampering,
    • Refactor the installer to simplify deployments, updates, and tenant onboarding,
    • Provide a CLI tool to export and import customer data, allowing seamless migration from on-premises to the cloud.
  8. Containerize the application
    Package both the API and backend workers into Docker images to standardize and simplify deployment across environments.

  9. Migrate to .NET 8 (Core)
    Move away from .NET Framework Standard to take advantage of modern performance, scalability, and cross-platform capabilities.

  10. Break down services into domains
    Separate responsibilities into clear functional areas:
    • Data Ingestion
    • Data Processing
    • Permissions & Authentication
    • Data Export
    • Billing & Usage
    • End-Customer API
    • SPA UI (React.js)

We are executing this transformation in an agile and incremental manner while continuing to deliver new features regularly. By the time we reach step 8, we expect to be able to onboard and test small customers in a shared cloud environment. Once validated, we will proceed with a full modernization, including the launch of a hosted cloud offering. Partners will then be invited to export their data from the on-premises version and import it into the cloud, enabling a smooth transition.

After these steps, we expect all (or the majority of) end-customers to be onboarded into the cloud. During phase 10, we will also evaluate the continued use of SQL Server — exploring cloud-native database options and the use of services like S3 or Azure Blob Storage for domain-specific storage. Additionally, we plan to offload common infrastructure components by adopting managed cloud services — for example, replacing internal authentication with an identity provider, or substituting our mail delivery logic with a managed email service.

Expected Outcomes

  • Gradually modernize the application while continuing to deliver new features
  • Seamlessly migrate selected customers to a shared cloud environment
  • Refactor, rearchitect, and rewrite parts of the system where it brings long-term value
  • Decompose large units of work into smaller services, leveraging serverless functions when appropriate
  • Introduce CDN, WAF, and throttling to support multiple tiers of service, enabling new revenue streams by offering different cloud flavours (Silver, Gold, or Platinum) based on performance, storage, and feature sets
  • Reduce licensing dependencies and overall infrastructure costs
  • Use spot instances to run scheduled jobs and overnight reports at lower cost
  • Eliminate large “export all” style reports by introducing targeted filtering and data scoping
  • Enforce data pagination on all views and reporting endpoints
  • Modernize the user interface using React.js for a better user experience and component reuse

Conclusion

Choosing between replatforming, refactoring, or rewriting depends on your current architecture, business needs, and resources. With the help of cloud-native tools and design patterns like DDD and Clean Architecture, you can successfully evolve legacy .NET applications into modern systems that are easier to maintain, monitor, and operate in the cloud.

Throughout this journey, there’s also room to explore enhancements such as:

  • Applying CQRS by splitting infrastructure for read and write operations;
  • Offloading reporting and user data to cloud storage (e.g., ready-to-consume JSON documents in S3 or Blob Storage);
  • Embracing eventual consistency for data that doesn’t require strong transactional guarantees (e.g., profiles, dashboards);
  • Implementing user behavior monitoring to understand usage patterns and improve UX;
  • Using feature flags to gradually roll out new functionality and enable beta testing;
  • Supporting A/B testing to validate changes with real users before full deployment;
  • Defining CI/CD promotion stairs (e.g., Dev → QA → PreProd → Prod) to ensure safe and continuous upgrades across environments.

Would you modernize or rewrite your current .NET monolith? Let’s connect and discuss!