Step 1: Initializing the Order Service Project Structure

Setting up the Order Service as a standalone .NET 8 Web API project inside your ECommApp solution follows clean architecture boundaries, separating domain logic from infrastructure and API concerns.

  • Create the core solution and the initial Order Service project folder structure:

    dotnet new sln -n ECommApp
    dotnet new webapi -n OrderService -f net8.0
    dotnet sln add OrderService/OrderService.csproj
  • Inside the OrderService project, organize your folders to prepare for CQRS (Commands, Queries, Domain, and Infrastructure):

    • Domain: Entities, Value Objects, Domain Events.

    • Application: Handlers (using MediatR), Validators, DTOs.

    • Infrastructure: Entity Framework Core persistence, database contexts, and message publishers.

Step 2: Integrating LocalStack & AWS API Gateway Architecture

Because you want to leverage LocalStack for your AWS services (such as API Gateway, SNS/SQS for event distribution, and DynamoDB or RDS PostgreSQL), your local Docker environment needs a clear configuration file.

  1. Create a docker-compose.yml file at your solution root to spin up LocalStack alongside your database:

    YAML

    version: '3.8'
    services:
      localstack:
        image: localstack/localstack:latest
        ports:
          - "4566:4566"
        environment:
          - SERVICES=apigateway,sns,sqs,dynamodb
          - AWS_DEFAULT_REGION=us-east-1
        volumes:
          - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
          - "/var/run/docker.sock:/var/run/docker.sock"
    
  2. Start the container via terminal:

    Bash

    docker-compose up -d
    

Where to Create Models for Clean Architecture

Because this project follows Clean Architecture for your enterprise microservices masterclass, you should not dump everything into the root of OrderService. Instead, organize your code by separating responsibilities.

Since you currently have a single Web API project template (OrderService), you can start by structuring clear folders inside it, or later split them into dedicated class library projects:

  • Domain Models / Entities: Create a folder named Domain (or inside a Domain sub-project later). This is where your core business objects live (e.g., Order, OrderItem) with private setters and rich domain behavior, completely free of database or API framework dependencies.

  • Application DTOs & Handlers: Create an Application folder to hold your CQRS commands, queries, validators, and data transfer objects.

  • Infrastructure: Create an Infrastructure folder for Entity Framework Core database contexts, configurations, and external integrations (like LocalStack SQS/SNS publishers).

  • API / Presentation: The root Controllers or Minimal API endpoints (Program.cs) handle incoming HTTP requests.


Class Libraries

Turning those subfolders into independent .csproj class libraries within your ECommApp solution creates physical project boundaries rather than just logical ones.

In your current setup, everything lives inside the single OrderService web project. While convenient for starting out, it has a structural downside: code inside the Domain folder can technically reference ASP.NET Core libraries or database contexts because they share the same project scope.

Splitting them into separate class library projects changes how the compiler enforces architecture rules:

  • Enforced Compilation Boundaries: The Domain project can be configured to have zero external dependencies (no Entity Framework, no MediatR, and no web frameworks). If you accidentally try to write database-specific code inside the Domain layer, the compiler will instantly throw an error.
  • Targeted Unit Testing: You can write fast, pure unit tests for your Domain and Application projects without needing to spin up a Web API host, mock HTTP contexts, or configure web servers.
  • Modular Scalability: As your microservice grows, isolated class libraries make it cleaner to extract logic, share event contracts across multiple services, or swap out infrastructure components (like changing database providers) without touching core business rules.

For now, keeping them as folders inside your single project is a practical way to get your environment verified and your initial code running. As we build out the full CQRS handlers and event pipelines, we can easily refactor those folders into dedicated class libraries.


For a production-grade microservice using Clean Architecture and CQRS, keeping everything in a single project will quickly cause tight coupling. Splitting your solution into separate =class libraries= is the proper approach:

  • OrderService.Domain: Core business entities, value objects, and domain events.
  • OrderService.Application: CQRS commands, queries, MediatR handlers, and validators.
  • OrderService.Infrastructure: EF Core persistence, database contexts, and message brokers (LocalStack SQS/SNS).
  • OrderService.API: Controllers or Minimal API endpoints (the presentation layer).

The Java / Maven Equivalent

Splitting a solution into independent .csproj class libraries is the exact structural equivalent of a multi-module Maven project (<modules> in a parent pom.xml).

Instead of having a single monolithic project, you define multiple modules—such as order-domain, order-application, and order-infrastructure—where each project compiles down to its own separate .jar equivalent (a class library .dll), and Maven/dotnet enforces strict compile-time boundaries between them.


Production-Grade Event Design: Interface vs. Record

Using both an interface and a record represents the gold standard for enterprise architectures.

  • The Interface (IIntegrationEvent or IDomainEvent): Acts as a marker or contract interface, allowing your event dispatcher, outbox pattern processors, and message publishers to handle any event polymorphically without knowing its concrete type.

  • The Record (OrderPlacedEvent): Implements that interface using a C# record. Records provide built-in value-based equality, immutability (init-only properties), and concise syntax, which are essential when serializing events across a distributed event bus like LocalStack SNS/SQS.

namespace OrderService.Domain.Events;
 
public interface IIntegrationEvent
{
    Guid EventId { get; }
    DateTime OccurredOn { get; }
}
 
public record OrderPlacedEvent(
    Guid OrderId,
    Guid CustomerId,
    decimal TotalAmount,
    DateTime OccurredOn
) : IIntegrationEvent
{
    Guid EventId { get; init; } = Guid.NewGuid();
}