<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ProG Coder</title>
    <description>The latest articles on DEV Community by ProG Coder (@progcoder).</description>
    <link>https://dev.to/progcoder</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3712585%2Fb7a6e08b-c5b1-43b3-ad83-2b4db4625f41.png</url>
      <title>DEV Community: ProG Coder</title>
      <link>https://dev.to/progcoder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/progcoder"/>
    <language>en</language>
    <item>
      <title>Building a Production-Grade Microservices E-Commerce Platform with .NET</title>
      <dc:creator>ProG Coder</dc:creator>
      <pubDate>Thu, 15 Jan 2026 14:30:04 +0000</pubDate>
      <link>https://dev.to/progcoder/building-a-production-grade-microservices-e-commerce-platform-with-net-h5n</link>
      <guid>https://dev.to/progcoder/building-a-production-grade-microservices-e-commerce-platform-with-net-h5n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A deep dive into Clean Architecture, Vertical Slices, YARP, and Event-Driven Design.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Microservices are challenging. They promise scalability and agility, but often deliver complexity and distributed headaches. Over the past few months, I’ve been building E-Commerce Platform, a comprehensive e-commerce reference architecture, to demonstrate how to tame this complexity using modern .NET technologies.&lt;/p&gt;

&lt;p&gt;In this post, I’ll walk you through the architecture, the design choices, and the code that powers this system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The High-Level Architecture
&lt;/h2&gt;

&lt;p&gt;The platform isn’t just a “Hello World” demo; it’s designed to mimic real-world requirements. It is composed of 9 independent microservices (Catalog, Basket, Order, Inventory, etc.) and utilizes a Polyglot Persistence strategy — meaning I use the right database for the right job (PostgreSQL, MongoDB, SQL Server, Elasticsearch, and Redis).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fhuynxtb%2Fprogcoder-shop-microservices%2Frefs%2Fheads%2Fmain%2Fassets%2Fimgs%2Fmicroservices_architecture.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fhuynxtb%2Fprogcoder-shop-microservices%2Frefs%2Fheads%2Fmain%2Fassets%2Fimgs%2Fmicroservices_architecture.png" alt="Microservices Architecture" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Core technologies include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;.NET 8 &amp;amp; Minimal APIs for high-performance services.&lt;/li&gt;
&lt;li&gt;YARP (Yet Another Reverse Proxy) as the sophisticated API Gateway.&lt;/li&gt;
&lt;li&gt;RabbitMQ &amp;amp; MassTransit for robust asynchronous messaging.&lt;/li&gt;
&lt;li&gt;Grpc for low-latency inter-service communication.&lt;/li&gt;
&lt;li&gt;IdentityServer/Keycloak for centralized authentication.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Vertical Slice Architecture: The “Secret Sauce”
&lt;/h2&gt;

&lt;p&gt;One of the biggest pitfalls in .NET development is over-engineered layering (Controller -&amp;gt; Service -&amp;gt; Manager -&amp;gt; Repository -&amp;gt; DAO…). It scatters logic across 5 different files for a single feature.&lt;/p&gt;

&lt;p&gt;For this project, I adopted Vertical Slice Architecture (VSA). Instead of organizing by technical layers, I organize by Features.&lt;/p&gt;

&lt;p&gt;A “Feature” contains everything needed to execute a specific business request: the API endpoint, the request/response DTOs, the validation logic, and the handler.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqawqpqxmll7q0xzcq7p2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqawqpqxmll7q0xzcq7p2.png" alt="Vertical Slice Architecture" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Spotlight: Creating a Product
&lt;/h2&gt;

&lt;p&gt;Here is what the &lt;code&gt;CreateProduct&lt;/code&gt; feature looks like in the Catalog Service. Notice how the Command, Validator, and Handler live together. The request logic is cohesive, not scattered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public record CreateProductCommand(CreateProductDto Dto, Actor Actor) : ICommand&amp;lt;Guid&amp;gt;;

public class CreateProductCommandValidator : AbstractValidator&amp;lt;CreateProductCommand&amp;gt;
{
    public CreateProductCommandValidator()
    {
        RuleFor(x =&amp;gt; x.Dto).NotNull();
        RuleFor(x =&amp;gt; x.Dto.Name).NotEmpty().WithMessage(MessageCode.ProductNameIsRequired);
        RuleFor(x =&amp;gt; x.Dto.Price).GreaterThan(1);
    }
}

public class CreateProductCommandHandler(
    IMapper mapper,
    IDocumentSession session,
    IMinIOCloudService minIO,
    ISender sender) : ICommandHandler&amp;lt;CreateProductCommand, Guid&amp;gt;
{
    public async Task&amp;lt;Guid&amp;gt; Handle(CreateProductCommand command, CancellationToken cancellationToken)
    {
        var dto = command.Dto;
        await session.BeginTransactionAsync(cancellationToken);

        // Domain Logic: Create Entity
        var entity = ProductEntity.Create(
            id: Guid.NewGuid(),
            name: dto.Name!,
            sku: dto.Sku!,
            // ... (other properties)
            price: dto.Price,
            performedBy: command.Actor.ToString());

        // External Infrastructure: Upload Images (MinIO)
        await UploadImagesAsync(dto.UploadImages, entity, cancellationToken);

        // Persistence: Store in Marten (PostgreSQL JSON)
        session.Store(entity);
        await session.SaveChangesAsync(cancellationToken);

        // Event: Publish domain event if needed
        if (entity.Published)
        {
            await sender.Send(new PublishProductCommand(entity.Id, command.Actor), cancellationToken);
        }

        return entity.Id;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach makes code navigation instant. If there’s a bug in “Create Product”, you know exactly where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gateway: YARP
&lt;/h2&gt;

&lt;p&gt;To expose these microservices to the frontend (React), I didn’t want to expose 9 different ports. I used YARP (Yet Another Reverse Proxy) to create a unified API Gateway.&lt;/p&gt;

&lt;p&gt;It handles routing, load balancing, and even authentication termination.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"ReverseProxy": {
  "Routes": {
    "catalog-route": {
      "ClusterId": "catalog-cluster",
      "Match": {
        "Path": "/catalog-service/{catch-all}"
      },
      "Transforms": [ { "PathPattern": "{catch-all}" } ]
    },
    // ... other routes for Basket, Order, etc.
  },
  "Clusters": {
    "catalog-cluster": {
      "Destinations": {
        "destination1": {
          "Address": "http://catalog-api:8080" // Internal Docker DNS
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Asynchronous Communication
&lt;/h2&gt;

&lt;p&gt;Services interact in two ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Synchronous (gRPC): For real-time data needs (e.g., Aggregator requests).&lt;/li&gt;
&lt;li&gt;2Asynchronous (Messaging): For side effects (e.g., “OrderPlaced” -&amp;gt; “SendEmail”).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I use RabbitMQ with MassTransit. The Outbox Pattern is implemented to ensure that a database transaction and a message publication happen atomically — no more “zombie” data if the message broker is down!&lt;/p&gt;

&lt;h2&gt;
  
  
  Intelligent CI/CD with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;In a microservices repo with 9+ services, you don’t want to rebuild everything when you only change one line in the Catalog service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfh1oywoqrx2ky0z7gjg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfh1oywoqrx2ky0z7gjg.png" alt="Github Actions Flows" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I implemented Path Filtering in GitHub Actions. The workflow detects exactly which service changed and only builds/tests that specific service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .github/workflows/_ci.yml
jobs:
  detect-changes:
    runs-on: ubuntu-latest
    steps:
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            catalog:
              - 'src/Services/Catalog/'
            basket:
              - 'src/Services/Basket/'
            # ... other services

  build-services:
    needs: detect-changes
    if: needs.detect-changes.outputs.catalog == 'true'
    # Only runs if Catalog service changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This saves massive amounts of CI minutes and speeds up feedback loops significantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability: Seeing Inside the Box
&lt;/h2&gt;

&lt;p&gt;With 9 microservices, you can’t just “tail the logs”. I set up a full OpenTelemetry observability stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logs: Serilog push to Loki (viewed in Grafana).&lt;/li&gt;
&lt;li&gt;Traces: OpenTelemetry pushes to Tempo.&lt;/li&gt;
&lt;li&gt;Metrics: Prometheus scrapes endpoints; visualized in Grafana.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every request generates a &lt;code&gt;TraceId&lt;/code&gt; that propagates through YARP to the downstream services (Catalog, Inventory, etc.), allowing me to visualize the entire request waterfall.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services.AddOpenTelemetry()
    .WithTracing(tracing =&amp;gt;
    {
        tracing.AddAspNetCoreInstrumentation()
               .AddHttpClientInstrumentation()
               .AddOtlpExporter(opt =&amp;gt; 
               {
                   opt.Endpoint = new Uri(otlpEndpoint);
               });
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fhuynxtb%2Fpublic-images%2Frefs%2Fheads%2Fmain%2Fprogcoder%2Fassets%2Fimgs%2Fgrafana_logs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fhuynxtb%2Fpublic-images%2Frefs%2Fheads%2Fmain%2Fprogcoder%2Fassets%2Fimgs%2Fgrafana_logs.png" alt="Logs" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fhuynxtb%2Fpublic-images%2Frefs%2Fheads%2Fmain%2Fprogcoder%2Fassets%2Fimgs%2Fgrafana_monitoring.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fhuynxtb%2Fpublic-images%2Frefs%2Fheads%2Fmain%2Fprogcoder%2Fassets%2Fimgs%2Fgrafana_monitoring.png" alt="Monitoring" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building microservices is about trade-offs. This project attempts to balance strict architectural purity with practical maintainability.&lt;/p&gt;

&lt;p&gt;👉 Check out the full source code on GitHub:&lt;br&gt;
&lt;a href="https://github.com/huynxtb/progcoder-shop-microservices" rel="noopener noreferrer"&gt;https://github.com/huynxtb/progcoder-shop-microservices&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you found this helpful, please give the repo a ⭐!&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>aspnet</category>
      <category>webapi</category>
    </item>
  </channel>
</rss>
