Scaling Enterprise .NET Applications with Microservices, Azure, and CI/CD

It was early Monday morning when the leadership team at a major healthcare company approached me with a daunting request: “We need to transition from a monolithic .NET application to a modern microservices architecture—yesterday!” As a Senior .NET Engineer, I knew this journey would be challenging. But I also knew it could revolutionize how we deliver mission-critical, high-performance, and secure applications on the Microsoft stack. In this post, I’ll share how we approached this transformation using ASP.NET Core, Domain-Driven Design (DDD), and Azure cloud services—all while juggling tight deadlines and strict regulatory requirements.


Understanding the Monolith

Our existing .NET Framework application had ballooned into a monolith:

  • Over 1 million lines of code in a single repository
  • Core business logic tightly coupled to data access
  • Performance bottlenecks in critical workflows
  • Difficult deployments—downtime was inevitable with each new release

We first conducted a detailed architecture assessment, identifying key domains (e.g., billing, patient records, authentication) and analyzing their interdependencies. This is where Domain-Driven Design gave us the blueprint for breaking things down logically.

namespace Healthcare.Billing.Domain
{
public class Invoice
{
public int InvoiceId { get; private set; }
public decimal Amount { get; private set; }
public DateTime DueDate { get; private set; }

public Invoice(int invoiceId, decimal amount, DateTime dueDate)
{
InvoiceId = invoiceId;
Amount = amount;
DueDate = dueDate;
}

// Domain-specific behavior
public void ApplyLateFee(decimal fee)
{
Amount += fee;
}
}
}

By separating our core domains and identifying bounded contexts, we mapped out a microservices strategy for high flexibility and maintainability.


Designing the Microservices Architecture

We used ASP.NET Core to build RESTful APIs for each domain:

  1. Billing Service (manages invoices, payments)
  2. Patient Service (handles patient records, demographics)
  3. Auth Service (authentication and authorization via OAuth2/JWT)

Each microservice had its own database—mostly SQL Server or Azure SQL—avoiding a shared database that could reintroduce tight coupling. For communication patterns, we mixed synchronous HTTP calls with asynchronous Azure Service Bus messaging to ensure resilience and scalability.


Infrastructure as Code & CI/CD Pipelines

With multiple microservices to deploy, we invested heavily in automation. We used Azure DevOps for our build and release pipelines:

trigger:
- main

pool:
vmImage: 'windows-latest'

steps:
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration Release'

- task: DotNetCoreCLI@2
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: '--configuration Release'

- task: Docker@2
inputs:
containerRegistry: 'MyContainerRegistry'
repository: 'billing-service'
command: 'buildAndPush'
Dockerfile: '**/Dockerfile'
tags: |
latest

- task: AzureWebApp@1
inputs:
azureSubscription: 'MyAzureSubscription'
appName: 'BillingServiceApp'
package: '$(Pipeline.Workspace)/drop/billing-service-latest.zip'

We also used Terraform for provisioning Azure resources, such as App Service plans, Key Vault for secrets, and load balancers. This “Infrastructure as Code” approach gave us consistency, repeatability, and the ability to spin up new environments quickly.


Performance & Database Optimization

One major concern was SQL Server performance. Shifting from a monolith to microservices doesn’t automatically solve database inefficiencies—you simply move them around if you aren’t careful.

  • Indexing and Query Tuning: We evaluated slow-running queries using execution plans and added or fine-tuned indexes.
  • Caching Layer: We integrated Redis to reduce load on our SQL databases and speed up frequently accessed data.
  • Distributed Tracing: We employed Application Insights to trace calls from the gateway to each microservice, pinpointing bottlenecks quickly.
SELECT TOP 10
SUBSTRING(qt.TEXT, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(qt.TEXT)
ELSE qs.statement_end_offset
END - qs.statement_start_offset)/2)+1) AS [QueryText],
qs.total_elapsed_time / qs.execution_count AS [AvgTime],
qs.total_logical_reads / qs.execution_count AS [AvgLogicalReads],
qs.execution_count,
qs.total_elapsed_time
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY [AvgTime] DESC;

This query helped us track down the most expensive queries, guiding us toward major improvements in response times.


Security and Compliance

Because we handled sensitive healthcare data, HIPAA compliance was non-negotiable. We took several measures:

  • OAuth2/JWT for secure API access
  • Azure Key Vault for rotating and storing secrets
  • HTTPS/TLS enforced across all services
  • Regular penetration tests and code reviews to maintain security standards

A Few Unexpected Roadblocks

No big project is without surprises. One microservice had to handle an extremely large file import from legacy systems—a feature that wasn’t obvious during initial planning. We tackled it by building a specialized Azure Function that handled file uploads asynchronously, storing them in Azure Blob Storage and dispatching processing tasks via a queue. This avoided timeouts and heavy resource usage in the main microservice.


The Final Outcome

After eight months, here’s how our .NET microservices architecture performed:

  • Zero downtime deployments thanks to rolling updates in Azure
  • 35% faster average response times due to database optimization and caching
  • Improved maintainability, with each domain now evolving independently
  • Stronger security posture aligned with healthcare industry regulations

Perhaps most importantly, our team can now deliver new features and bug fixes in a matter of hours—not weeks.


Key Takeaways

  1. Domain-Driven Design is your friend for decomposing monoliths: keep services focused, cohesive, and bounded.
  2. Automation is everything when scaling microservices: invest in CI/CD, Infrastructure as Code, and container orchestration.
  3. Performance Tuning is an ongoing practice: watch your queries, use caching, and adopt distributed tracing for deep insights.
  4. Security is paramount, especially in regulated industries: protect data in transit, at rest, and at every entry point.

Looking Ahead

There’s always more to learn in the .NET and Azure ecosystem. My next goals are:

  • Exploring .NET 8 to see how new features can further streamline microservices
  • Adopting serverless patterns with Azure Functions and Event Grid for specific workloads
  • Integrating machine learning for predictive analytics in mission-critical .NET applications

Building scalable .NET solutions isn’t just about writing better code. It’s about adopting the right architecture, optimizing databases, and leaning into DevOps from day one. The result is a suite of microservices that can adapt, scale, and secure your applications—no matter how demanding the environment becomes.

Until next time, keep coding, keep learning, and keep building applications that truly make a difference.