Back to Blog
Architecture

Building Scalable Microservices with .NET: A Practical Guide

Learn how to design and implement scalable microservices architecture using .NET, Docker, and Kubernetes. We cover best practices, common pitfalls, and real-world examples.

December 10, 20243 min read
Building Scalable Microservices with .NET: A Practical Guide

Microservices architecture has become the go-to approach for building modern, scalable applications. In this article, we'll explore how to leverage .NET to build robust microservices that can handle enterprise-level demands.

Why Microservices?

Traditional monolithic applications, while simpler to develop initially, often become difficult to maintain and scale as they grow. Microservices offer several advantages:

  • Independent Deployment: Each service can be deployed independently
  • Technology Flexibility: Different services can use different technologies
  • Scalability: Scale individual services based on demand
  • Resilience: Failure in one service doesn't bring down the entire system

Getting Started with .NET Microservices

Project Structure

A well-organized microservices project typically follows this structure:

/src
  /Services
    /OrderService
      /OrderService.API
      /OrderService.Domain
      /OrderService.Infrastructure
    /InventoryService
      /InventoryService.API
      /InventoryService.Domain
      /InventoryService.Infrastructure
  /BuildingBlocks
    /EventBus
    /Common

Creating Your First Service

Let's start with a simple Order Service. First, create a new ASP.NET Core Web API project:

dotnet new webapi -n OrderService.API

Implementing the Domain Layer

The domain layer contains your business logic and entities:

public class Order
{
    public Guid Id { get; private set; }
    public string CustomerId { get; private set; }
    public List<OrderItem> Items { get; private set; }
    public OrderStatus Status { get; private set; }
    public DateTime CreatedAt { get; private set; }

    public Order(string customerId)
    {
        Id = Guid.NewGuid();
        CustomerId = customerId;
        Items = new List<OrderItem>();
        Status = OrderStatus.Pending;
        CreatedAt = DateTime.UtcNow;
    }

    public void AddItem(string productId, int quantity, decimal price)
    {
        Items.Add(new OrderItem(productId, quantity, price));
    }

    public void Confirm()
    {
        if (Status != OrderStatus.Pending)
            throw new InvalidOperationException("Order cannot be confirmed");

        Status = OrderStatus.Confirmed;
    }
}

Inter-Service Communication

Microservices need to communicate with each other. There are two main patterns:

Synchronous Communication (HTTP/gRPC)

For real-time requests, use HTTP or gRPC:

public class InventoryClient : IInventoryClient
{
    private readonly HttpClient _httpClient;

    public InventoryClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<bool> CheckAvailability(string productId, int quantity)
    {
        var response = await _httpClient.GetAsync(
            $"/api/inventory/{productId}/availability?quantity={quantity}");

        return response.IsSuccessStatusCode;
    }
}

Asynchronous Communication (Message Queues)

For event-driven communication, use message queues like RabbitMQ:

public class OrderCreatedEvent
{
    public Guid OrderId { get; set; }
    public string CustomerId { get; set; }
    public List<OrderItemDto> Items { get; set; }
    public DateTime CreatedAt { get; set; }
}

public class OrderCreatedEventHandler : IEventHandler<OrderCreatedEvent>
{
    private readonly IInventoryService _inventoryService;

    public async Task Handle(OrderCreatedEvent @event)
    {
        foreach (var item in @event.Items)
        {
            await _inventoryService.ReserveStock(item.ProductId, item.Quantity);
        }
    }
}

Containerization with Docker

Each microservice should be containerized for consistent deployment:

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["OrderService.API.csproj", "./"]
RUN dotnet restore
COPY . .
RUN dotnet build -c Release -o /app/build

FROM build AS publish
RUN dotnet publish -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "OrderService.API.dll"]

Orchestration with Kubernetes

Deploy your microservices to Kubernetes for production:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: myregistry/order-service:latest
          ports:
            - containerPort: 80
          resources:
            limits:
              memory: 256Mi
              cpu: 500m

Best Practices

  1. Design for Failure: Implement circuit breakers and retry policies
  2. Centralized Logging: Use tools like ELK stack or Application Insights
  3. API Gateway: Implement an API gateway for routing and authentication
  4. Health Checks: Implement health endpoints for monitoring
  5. Configuration Management: Use centralized configuration (e.g., Consul, Azure App Configuration)

Conclusion

Building microservices with .NET provides a robust foundation for scalable applications. By following these patterns and best practices, you can create maintainable, resilient systems that grow with your business needs.

At Kickoff Works, we specialize in designing and implementing microservices architectures. If you're looking to modernize your application or need help with your .NET projects, get in touch with us.


Have questions about microservices architecture? Feel free to reach out to our team for a consultation.

Share this article