Skip to main content

ASP.NET Core Microservices Architecture: Complete Guide

Learn microservices architecture in ASP.NET Core — design, build, and deploy scalable .NET microservices with code examples. Start building today.

Microservices architecture has become the default answer for teams that need to ship features fast without stepping on each other's toes. Instead of one giant ASP.NET Core application where every deployment is a high-stakes event, you build a set of small, independently deployable services — each owning its own data, its own release cycle, and its own failure boundary. In this guide, you'll learn how to design a microservices architecture in ASP.NET Core, build two communicating services with runnable C# code, and deploy them at scale with Docker and Kubernetes. More importantly, you'll learn why each decision matters, because microservices done wrong are far more painful than a well-organized monolith.

What Is Microservices Architecture (and When Should You Use It)?

Microservices architecture is an approach to building software as a collection of small, autonomous services, each focused on a single business capability. An e-commerce system might have an Orders service, a Catalog service, a Payments service, and a Notifications service. Each one:

  • Runs in its own process and is deployed independently
  • Owns its own database — no other service touches its tables
  • Communicates over the network via HTTP/gRPC or asynchronous messaging
  • Can be scaled, rewritten, or even retired without touching the others

The honest trade-off: you're exchanging in-process method calls for network calls, and one database transaction for eventual consistency. That's a real cost. The rule of thumb most experienced .NET architects follow is: start with a well-modularized monolith, and split into microservices when team size or scaling requirements force you to. If you have three developers and one deployment a week, a monolith will almost certainly serve you better. If you have six teams shipping daily and one service needs 20x the compute of the others, microservices pay for themselves.

Designing Microservices in ASP.NET Core: Core Principles

1. Draw Boundaries Around Business Capabilities, Not Technical Layers

The single most common failure mode is splitting by technical layer ("the data service", "the business logic service"). That creates chatty services that must all change together — a distributed monolith. Instead, use Domain-Driven Design's bounded contexts: each service owns a complete vertical slice of one business capability. The Orders service owns order creation, order state, and order history — data, logic, and API together.

2. Database per Service

Each service gets its own database (or at minimum its own schema with enforced isolation). If two services share tables, you can never change one's schema without coordinating a joint deployment — which defeats the entire purpose. When Service A needs Service B's data, it asks B's API or subscribes to B's events. Yes, this means duplicating some data. That duplication is the price of independence, and it's usually worth paying.

3. Prefer Asynchronous Communication for Workflows

Synchronous HTTP calls between services create temporal coupling: if Payments is down, Orders can't complete checkout. For anything that doesn't need an immediate answer, publish an event to a message broker (RabbitMQ, Azure Service Bus, Kafka) and let consumers process it when they're ready. Reserve synchronous calls for genuine request/response needs — and protect them with timeouts and retries.

Building ASP.NET Core Microservices: A Practical Example

Let's build the skeleton of a real system: an Orders service that publishes an OrderPlaced event, and a Notifications service that consumes it. We'll use .NET 8 minimal APIs and RabbitMQ via MassTransit, the most widely used messaging library in the .NET ecosystem.

The Orders Service

Create the project and add packages:

// dotnet new webapi -n Orders.Api --use-minimal-apis
// dotnet add package MassTransit.RabbitMQ
// dotnet add package Microsoft.EntityFrameworkCore.SqlServer

Define the event contract in a shared library (contracts are the only thing services should share):

namespace Contracts;

public record OrderPlaced(
    Guid OrderId,
    string CustomerEmail,
    decimal TotalAmount,
    DateTime PlacedAtUtc);

Wire up the service in Program.cs:

using Contracts;
using MassTransit;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<OrdersDbContext>(opt =>
    opt.UseSqlServer(builder.Configuration.GetConnectionString("OrdersDb")));

builder.Services.AddMassTransit(x =>
{
    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host(builder.Configuration["RabbitMq:Host"] ?? "localhost");
        cfg.ConfigureEndpoints(context);
    });
});

var app = builder.Build();

app.MapPost("/orders", async (
    CreateOrderRequest request,
    OrdersDbContext db,
    IPublishEndpoint publisher) =>
{
    var order = new Order
    {
        Id = Guid.NewGuid(),
        CustomerEmail = request.CustomerEmail,
        TotalAmount = request.TotalAmount,
        Status = OrderStatus.Placed,
        PlacedAtUtc = DateTime.UtcNow
    };

    db.Orders.Add(order);
    await db.SaveChangesAsync();

    await publisher.Publish(new OrderPlaced(
        order.Id, order.CustomerEmail, order.TotalAmount, order.PlacedAtUtc));

    return Results.Created($"/orders/{order.Id}", new { order.Id });
});

app.Run();

public record CreateOrderRequest(string CustomerEmail, decimal TotalAmount);

Why publish an event instead of calling the Notifications API directly? Because tomorrow you'll add a Loyalty service and an Analytics service that also care about new orders. With events, they simply subscribe — the Orders service never changes. That's the open/closed principle applied to system architecture.

The Notifications Service (Consumer)

using Contracts;
using MassTransit;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMassTransit(x =>
{
    x.AddConsumer<OrderPlacedConsumer>();
    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host(builder.Configuration["RabbitMq:Host"] ?? "localhost");
        cfg.ConfigureEndpoints(context);
    });
});

var app = builder.Build();
app.Run();

public class OrderPlacedConsumer : IConsumer<OrderPlaced>
{
    private readonly ILogger<OrderPlacedConsumer> _logger;

    public OrderPlacedConsumer(ILogger<OrderPlacedConsumer> logger)
        => _logger = logger;

    public async Task Consume(ConsumeContext<OrderPlaced> context)
    {
        var order = context.Message;
        _logger.LogInformation(
            "Sending confirmation for order {OrderId} to {Email}",
            order.OrderId, order.CustomerEmail);

        // In production: call your email provider here.
        await Task.CompletedTask;
    }
}

Run RabbitMQ locally with one command — docker run -d -p 5672:5672 -p 15672:15672 rabbitmq:3-management — start both services, POST an order, and watch the Notifications service log the event. You now have two independently deployable services communicating asynchronously.

Add an API Gateway with YARP

Clients shouldn't know your internal topology. An API gateway gives them one URL and handles routing, auth, and rate limiting in one place. Microsoft's YARP (Yet Another Reverse Proxy) makes this trivial in ASP.NET Core:

// dotnet add package Yarp.ReverseProxy
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();
app.MapReverseProxy();
app.Run();

Routes live in appsettings.json, mapping /api/orders/* to the Orders cluster and /api/catalog/* to Catalog. Because it's config-driven, adding a new service never requires recompiling the gateway.

Make Synchronous Calls Resilient

When you do need service-to-service HTTP, never make a naked call. .NET 8's built-in resilience handler (built on Polly) adds retries, timeouts, and a circuit breaker in three lines:

// dotnet add package Microsoft.Extensions.Http.Resilience
builder.Services.AddHttpClient<CatalogClient>(client =>
        client.BaseAddress = new Uri("https://catalog-api"))
    .AddStandardResilienceHandler();

Why a circuit breaker? When a downstream service is dying, hammering it with retries makes things worse and ties up your own threads. The breaker fails fast, gives the sick service room to recover, and prevents one failure from cascading across the whole system.

Deploying ASP.NET Core Microservices at Scale

Containerize with Docker

Since .NET 8, you don't even need a Dockerfile — dotnet publish /t:PublishContainer builds an optimized image. If you prefer an explicit Dockerfile, use the standard multi-stage pattern with the mcr.microsoft.com/dotnet/aspnet:8.0 runtime image and run as a non-root user.

Orchestrate with Kubernetes

Kubernetes handles the operational heavy lifting: restarting crashed containers, rolling deployments, service discovery, and autoscaling. A minimal deployment for the Orders service:

// orders-deployment.yaml (YAML, shown here for completeness)
// apiVersion: apps/v1
// kind: Deployment
// metadata: { name: orders-api }
// spec:
//   replicas: 3
//   template:
//     spec:
//       containers:
//       - name: orders-api
//         image: registry.example.com/orders-api:1.4.0
//         resources:
//           requests: { cpu: "250m", memory: "256Mi" }
//         livenessProbe:
//           httpGet: { path: /healthz, port: 8080 }

Back that liveness probe with ASP.NET Core health checks:

builder.Services.AddHealthChecks()
    .AddDbContextCheck<OrdersDbContext>();

app.MapHealthChecks("/healthz");

Pair a HorizontalPodAutoscaler with CPU or queue-depth metrics and each service scales independently — the whole reason you split them in the first place. For local development, .NET Aspire is now the best-in-class way to orchestrate multiple services, RabbitMQ, and databases on your machine with F5 debugging, then generate deployment manifests for production.

Observability Is Non-Negotiable

In a monolith, a stack trace tells the whole story. In microservices, one user request may touch five services, and without correlation you're debugging blind. Add OpenTelemetry from day one:

builder.Services.AddOpenTelemetry()
    .WithTracing(t => t
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddSource("MassTransit")
        .AddOtlpExporter());

Export to Jaeger, Grafana Tempo, or Application Insights, and every request gets a distributed trace that follows it across service and queue boundaries.

Best Practices and Common Pitfalls in Microservices Architecture

  • Do: keep services independently deployable — if two services must always ship together, merge them.
  • Do: version your event contracts additively; never remove or rename fields consumers depend on.
  • Do: use the outbox pattern (MassTransit has built-in support) so saving the order and publishing the event succeed or fail together — otherwise a crash between SaveChangesAsync and Publish silently loses events.
  • Do: make consumers idempotent; message brokers guarantee at-least-once delivery, so duplicates will happen.
  • Don't: build a distributed monolith — long synchronous call chains (A calls B calls C calls D) multiply latency and failure probability at every hop.
  • Don't: share a database between services, ever. It's the most tempting shortcut and the most expensive one.
  • Don't: reach for distributed transactions. Use sagas — a sequence of local transactions with compensating actions — for workflows that span services.
  • Don't: start with 15 services. Start with two or three coarse-grained ones and split further only when a real pressure (team ownership, scaling, release cadence) demands it.

Conclusion: Key Takeaways

Microservices architecture in ASP.NET Core is a powerful tool for scaling both your system and your organization — but it's a trade, not a free upgrade. You gain independent deployment, targeted scaling, and team autonomy; you pay in network complexity, eventual consistency, and operational overhead. The .NET ecosystem in 2026 makes that price lower than ever: minimal APIs for lean services, MassTransit for messaging, YARP for gateways, built-in Polly resilience, OpenTelemetry for tracing, and .NET Aspire plus Kubernetes for orchestration.

Key takeaways:

  • Split by business capability (bounded contexts), never by technical layer.
  • Give every service its own database and communicate through APIs and events.
  • Prefer asynchronous messaging; protect unavoidable synchronous calls with retries and circuit breakers.
  • Use the outbox pattern and idempotent consumers to make messaging reliable.
  • Instrument everything with OpenTelemetry before you need it, not after.
  • Earn your microservices: start coarse, split under real pressure.

Clone the pattern above, swap the console log for a real email provider, and you have the foundation of a production-grade microservices architecture. Start small, measure everything, and let genuine scaling pain — not hype — drive every split.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...