ASP.NET Core  

Understanding Microservices with .NET Through an E-Commerce Example

Imagine you have an e-commerce website built with .NET. A small bug is found in the customer browsing feature, so you deploy a fix. The next day, you need to update the product page with another minor change.

In a traditional monolithic application, even small updates often require redeploying the entire application. As your system grows and releases become more frequent, deployments can become slower and riskier.

Microservices address this by splitting the application into independent services that can be developed, deployed, and scaled separately. In this article, I’ll show how to design a microservices architecture using .NET.

A conventional monolithic .Net website will look like this:

A conventional monolithic .NET application might organize the Customer, Inventory, Order, and Payment modules within a single solution and deploy them as one application. Although the code is separated into different controllers and layers, they are still part of the same deployable unit.

However, in a microservices architecture, each business domain is implemented as an independent service. Each service typically has its own project and its own database, allowing it to be developed, deployed, and scaled independently.

In Visual Studio, the solution may contain separate projects for Customer, Inventory, Order, and Payment services. During deployment, each API is published as its own service (for example, to IIS, Azure App Service, or containers).

The biggest advantage is independent deployment. If you need to modify the Customer service, you only redeploy that service instead of redeploying the entire application. This reduces deployment risk and allows different teams to work and release features independently.

In this article, I’ll demonstrate how services in a microservices architecture communicate with one another using REST APIs through HttpClient to implement common business workflows.

Our sample e-commerce application consists of four services:

  • Customer Service — Manages customer registration and information. It has its own database.

  • Inventory Service — Manages product information and stock levels. It has its own database, which stores product and inventory data.

  • Order Service — Handles order creation when a customer checks out. Before saving an order, it calls the Customer Service to verify that the customer exists and the Inventory Service to retrieve product information and calculate the order total.

  • Payment Service — Simulates payment processing. Before processing a payment, it calls the Order Service to validate the order, then forwards the request to a mock payment gateway.

The interaction among services are look like this:

Or more detailed flow.

By the end of this article, you’ll understand how these independent services communicate with each other to complete a typical e-commerce workflow while keeping each service loosely coupled and independently deployable.

Let’s walkthrough the code.

The Customer Service and Inventory Service are relatively straightforward. They expose standard REST APIs that perform basic CRUD operations on customers and products.

The Order Service is more interesting because it depends on information from other services. Before an order can be created, it needs to verify that the customer exists and that each product is a valid SKU with sufficient stock available.

   public async Task<CreateOrderResult> CreateOrder(CreateOrderRequest request)
   {
       //...code omitted
       
        //call customer service to get customer details
       var customer = await _customerClient.GetCustomerByIdAsync(request.CustomerId);
       if (customer == null)
       {
           return CreateOrderResult.NoCustomer($"Customer {request.CustomerId} does not exist.");
       }

       //...

       foreach (var item in request.Items)
       {
          //call inventory service to get product details
           var product = await _inventoryClient.GetProductAsync(item.ProductId);
           if (product == null)
          //...

Since the Order Service is isolated from the Customer and Inventory services, it cannot access their databases directly. Instead, it communicates with them through HTTP REST APIs.

To keep the code organized, the OrderService.API project contains an ExternalServices folder, which encapsulates all communication with external services. In this folder, we create dedicated CustomerClient and InventoryClient classes that wrap the HTTP requests using HttpClient. This approach keeps the business logic clean and makes it easier to maintain or replace external integrations in the future.

Next, configure the endpoints of the Customer and Inventory services in appsettings.json.


 "Services": {
   "CustomerService": "http://localhost:5108",
   "InventoryService": "http://localhost:5155"
 }

Using IHttpClientFactory is the recommended approach in ASP.NET Core because it manages the lifetime of HttpClient instances, avoids socket exhaustion, and centralizes the configuration of external service endpoints.


builder.Services.AddHttpClient("CustomerService", client =>
{
    client.BaseAddress = new Uri(builder.Configuration["Services:CustomerService"]!);
});

builder.Services.AddHttpClient("InventoryService", client =>
{
    client.BaseAddress = new Uri(builder.Configuration["Services:InventoryService"]!);
});

Finally, the CustomerClient and InventoryClient classes use these registered HttpClient instances to send HTTP requests to their respective services. These client classes encapsulate the communication logic, allowing the OrderService to focus on business logic without worrying about the implementation details of the HTTP calls

 public class CustomerClient : ICustomerClient
 {
     private readonly IHttpClientFactory _httpClientFactory;

     public CustomerClient(IHttpClientFactory httpClientFactory)
     {
         _httpClientFactory = httpClientFactory;
     }

     public async Task<CustomerSummary?>  GetCustomerByIdAsync(Guid id)
     {
         var customerClient = _httpClientFactory.CreateClient("CustomerService");
         var customerResponse = await customerClient.GetAsync($"api/Customers/GetCustomerById/{id}");

         if (!customerResponse.IsSuccessStatusCode)
         {
             return null;
         }

         return await customerResponse.Content.ReadFromJsonAsync<CustomerSummary>();
     }
 }
 public class InventoryClient : IInventoryClient
 {
     private readonly IHttpClientFactory _httpClientFactory;

     public InventoryClient(IHttpClientFactory httpClientFactory)
     {
         _httpClientFactory = httpClientFactory;
     }

     public async Task<ProductSummary?> GetProductAsync(Guid productId)
     {
         var client = _httpClientFactory.CreateClient("InventoryService");
         var response = await client.GetAsync($"api/Product/GetProductById/{productId}");
         if (!response.IsSuccessStatusCode)
         {
             return null;
         }

         return await response.Content.ReadFromJsonAsync<ProductSummary>();
     }
//....

Same thing in PaymentService, we need to call OrderService before proceed to

public async Task<CreatePaymentResult> CreatePaymentAsync(CreatePaymentRequest request)
  {
      var existingPayment = _db.Payments.FirstOrDefault(p => p.OrderId == request.OrderId);
           
      //....
      
      //call order service to get order details
      var order = await _orderClient.GetOrderAsync(request.OrderId);
     //...

How OrderClient in PaymentService send HTTP requests to its own service

public class OrderClient : IOrderClient
 {
     private readonly IHttpClientFactory _httpClientFactory;

     public OrderClient(IHttpClientFactory httpClientFactory)
     {
         _httpClientFactory = httpClientFactory;
     }

     public async Task<OrderSummary?> GetOrderAsync(Guid orderId)
     {
         var client = _httpClientFactory.CreateClient("OrderService");
         var response = await client.GetAsync($"api/Order/GetOrdersById/{orderId}");
         if (!response.IsSuccessStatusCode)
         {
             return null;
         }

         return await response.Content.ReadFromJsonAsync<OrderSummary>();
     }
//...

That’s all! This is a simple example of how services in a microservices architecture communicate with one another using REST APIs and HttpClient.

For this demo, I configured the Solution Properties in Visual Studio to start all four services simultaneously, making it easy to test the complete workflow on my local machine.

In a real production environment, each microservice would typically be deployed independently — for example, to separate IIS websites, Azure App Services, Docker containers, or Kubernetes pods. Each service would have its own endpoint, allowing it to be updated and scaled without affecting the other services.

Then we can perform standard CRUD in our e-commerce microservices

Create new customer

Create new product and update its quantity

As shown above, the product was created successfully. However, the initial stock quantity is still 0 because creating a product and managing inventory are separate responsibilities in our Inventory Service.

To add stock, we need to call the Increase Stock API to update the available quantity for this product.

Create Order

To test our order workflow, we first try creating an order with an invalid customer ID.

We can also test by requesting a purchase quantity that exceeds the available stock.

In both cases, the request fails with an appropriate error message. This confirms that the Order Service is successfully communicating with the Customer Service and Inventory Service to perform the required validation before creating an order.

A successful order creation

After providing valid customer information and sufficient stock quantity, the order is created successfully.

The inventory quantity is also reduced accordingly, which shows that the Order Service can successfully interact with the Inventory Service to complete the order workflow.

Create Payment

To test the payment workflow, we first try creating a payment request with an invalid order ID.

The request fails because the order does not exist. This confirms that the Payment Service is successfully communicating with the Order Service to validate the order before processing the payment.

After providing a valid order ID, the payment request can proceed successfully through our mock payment gateway.

In this article, we built a simple e-commerce microservices architecture using .NET, where each service is independently developed with its own database and communicates through REST APIs. We demonstrated how Customer, Inventory, Order, and Payment services work together to complete a business workflow while keeping each service loosely coupled. Although this is a simplified example, it provides the foundation for understanding how real-world microservices systems are designed and deployed.

You can download the complete demo project from my GitHub repository.