Introduction
Clean Architecture, popularized by Robert C. Martin (Uncle Bob), is a software design philosophy that emphasizes separation of concerns and independence of frameworks, UI, and databases. The Ordering service in AspNetRun demonstrates a full implementation of Clean Architecture combined with Domain-Driven Design.
Service Location : src/Services/Ordering/This service handles order processing, order history, and order lifecycle management with complex business rules.
The Clean Architecture Layers
Project Structure
The Ordering service is organized into four distinct projects:
Ordering/
├── Ordering.Domain/ # Core business logic
│ ├── Abstractions/
│ │ ├── Aggregate.cs # Base aggregate root
│ │ ├── Entity.cs # Base entity
│ │ ├── IAggregate.cs
│ │ └── IDomainEvent.cs
│ ├── Models/
│ │ ├── Order.cs # Order aggregate root
│ │ ├── OrderItem.cs # Order entity
│ │ └── Customer.cs
│ ├── ValueObjects/
│ │ ├── Address.cs
│ │ ├── Payment.cs
│ │ ├── OrderId.cs
│ │ └── CustomerId.cs
│ ├── Events/
│ │ ├── OrderCreatedEvent.cs
│ │ └── OrderUpdatedEvent.cs
│ └── Enums/
│ └── OrderStatus.cs
│
├── Ordering.Application/ # Use cases and business rules
│ ├── Orders/
│ │ ├── Commands/
│ │ │ ├── CreateOrder/
│ │ │ │ ├── CreateOrderCommand.cs
│ │ │ │ └── CreateOrderHandler.cs
│ │ │ ├── UpdateOrder/
│ │ │ └── DeleteOrder/
│ │ ├── Queries/
│ │ │ ├── GetOrders/
│ │ │ │ ├── GetOrdersQuery.cs
│ │ │ │ └── GetOrdersHandler.cs
│ │ │ ├── GetOrdersByCustomer/
│ │ │ └── GetOrdersByName/
│ │ └── EventHandlers/
│ │ ├── Domain/
│ │ │ └── OrderCreatedEventHandler.cs
│ │ └── Integration/
│ │ └── BasketCheckoutEventHandler.cs
│ ├── Data/
│ │ └── IApplicationDbContext.cs
│ ├── Dtos/
│ └── Extensions/
│
├── Ordering.Infrastructure/ # External concerns
│ ├── Data/
│ │ ├── ApplicationDbContext.cs
│ │ ├── Configurations/ # EF Core entity configurations
│ │ ├── Interceptors/
│ │ │ ├── AuditableEntityInterceptor.cs
│ │ │ └── DispatchDomainEventsInterceptor.cs
│ │ ├── Extensions/
│ │ │ └── DatabaseExtensions.cs
│ │ └── Migrations/
│ └── DependencyInjection.cs
│
└── Ordering.API/ # Presentation layer
├── Endpoints/
│ ├── CreateOrder.cs
│ ├── GetOrders.cs
│ ├── UpdateOrder.cs
│ └── DeleteOrder.cs
├── Program.cs
└── DependencyInjection.cs
Layer Responsibilities
1. Domain Layer (Core)
The Heart of the Application Contains all business logic, domain models, and business rules. This layer has no dependencies on other layers.
Aggregate Root Example
The Order aggregate is the entry point for all order-related operations:
src/Services/Ordering/Ordering.Domain/Models/Order.cs
namespace Ordering . Domain . Models ;
public class Order : Aggregate < OrderId >
{
private readonly List < OrderItem > _orderItems = new ();
public IReadOnlyList < OrderItem > OrderItems => _orderItems . AsReadOnly ();
public CustomerId CustomerId { get ; private set ; } = default ! ;
public OrderName OrderName { get ; private set ; } = default ! ;
public Address ShippingAddress { get ; private set ; } = default ! ;
public Address BillingAddress { get ; private set ; } = default ! ;
public Payment Payment { get ; private set ; } = default ! ;
public OrderStatus Status { get ; private set ; } = OrderStatus . Pending ;
public decimal TotalPrice
{
get => OrderItems . Sum ( x => x . Price * x . Quantity );
private set { }
}
// Factory method enforces invariants
public static Order Create (
OrderId id ,
CustomerId customerId ,
OrderName orderName ,
Address shippingAddress ,
Address billingAddress ,
Payment payment )
{
var order = new Order
{
Id = id ,
CustomerId = customerId ,
OrderName = orderName ,
ShippingAddress = shippingAddress ,
BillingAddress = billingAddress ,
Payment = payment ,
Status = OrderStatus . Pending
};
// Raise domain event
order . AddDomainEvent ( new OrderCreatedEvent ( order ));
return order ;
}
// Business logic encapsulated in aggregate
public void Add ( ProductId productId , int quantity , decimal price )
{
ArgumentOutOfRangeException . ThrowIfNegativeOrZero ( quantity );
ArgumentOutOfRangeException . ThrowIfNegativeOrZero ( price );
var orderItem = new OrderItem ( Id , productId , quantity , price );
_orderItems . Add ( orderItem );
}
public void Remove ( ProductId productId )
{
var orderItem = _orderItems . FirstOrDefault ( x => x . ProductId == productId );
if ( orderItem is not null )
{
_orderItems . Remove ( orderItem );
}
}
}
Key Characteristics:
Private setters protect invariants
Factory method Create() ensures valid construction
Business logic methods (Add, Remove) maintain consistency
Domain events communicate changes
Read-only collection prevents external modification
Value Objects
Value Objects represent concepts with no identity, compared by value:
src/Services/Ordering/Ordering.Domain/ValueObjects/Address.cs
namespace Ordering . Domain . ValueObjects ;
public record Address
{
public string FirstName { get ; } = default ! ;
public string LastName { get ; } = default ! ;
public string ? EmailAddress { get ; } = default ! ;
public string AddressLine { get ; } = default ! ;
public string Country { get ; } = default ! ;
public string State { get ; } = default ! ;
public string ZipCode { get ; } = default ! ;
protected Address () { }
private Address ( string firstName , string lastName , string emailAddress ,
string addressLine , string country , string state , string zipCode )
{
FirstName = firstName ;
LastName = lastName ;
EmailAddress = emailAddress ;
AddressLine = addressLine ;
Country = country ;
State = state ;
ZipCode = zipCode ;
}
// Factory method with validation
public static Address Of ( string firstName , string lastName , string emailAddress ,
string addressLine , string country , string state , string zipCode )
{
ArgumentException . ThrowIfNullOrWhiteSpace ( emailAddress );
ArgumentException . ThrowIfNullOrWhiteSpace ( addressLine );
return new Address ( firstName , lastName , emailAddress ,
addressLine , country , state , zipCode );
}
}
Strongly-Typed IDs
Prevent primitive obsession by wrapping IDs in types:
src/Services/Ordering/Ordering.Domain/ValueObjects/OrderId.cs
namespace Ordering . Domain . ValueObjects ;
public record OrderId
{
public Guid Value { get ; }
private OrderId ( Guid value ) => Value = value ;
public static OrderId Of ( Guid value )
{
ArgumentNullException . ThrowIfNull ( value );
if ( value == Guid . Empty )
{
throw new DomainException ( "OrderId cannot be empty." );
}
return new OrderId ( value );
}
}
Domain Events
Communicate state changes within the domain:
src/Services/Ordering/Ordering.Domain/Events/OrderCreatedEvent.cs
namespace Ordering . Domain . Events ;
public record OrderCreatedEvent ( Order order ) : IDomainEvent ;
2. Application Layer
Use Cases and Business Workflows Orchestrates domain objects to fulfill use cases. Depends on Domain layer but not on Infrastructure.
Commands (Write Operations)
Commands represent intentions to change state:
src/Services/Ordering/Ordering.Application/Orders/Commands/CreateOrder/CreateOrderCommand.cs
using BuildingBlocks . CQRS ;
using FluentValidation ;
namespace Ordering . Application . Orders . Commands . CreateOrder ;
public record CreateOrderCommand ( OrderDto Order )
: ICommand < CreateOrderResult >;
public record CreateOrderResult ( Guid Id );
public class CreateOrderCommandValidator : AbstractValidator < CreateOrderCommand >
{
public CreateOrderCommandValidator ()
{
RuleFor ( x => x . Order . OrderName )
. NotEmpty (). WithMessage ( "Name is required" );
RuleFor ( x => x . Order . CustomerId )
. NotNull (). WithMessage ( "CustomerId is required" );
RuleFor ( x => x . Order . OrderItems )
. NotEmpty (). WithMessage ( "OrderItems should not be empty" );
}
}
Command Handlers
Execute commands and coordinate domain objects:
src/Services/Ordering/Ordering.Application/Orders/Commands/CreateOrder/CreateOrderHandler.cs
namespace Ordering . Application . Orders . Commands . CreateOrder ;
public class CreateOrderHandler ( IApplicationDbContext dbContext )
: ICommandHandler < CreateOrderCommand , CreateOrderResult >
{
public async Task < CreateOrderResult > Handle (
CreateOrderCommand command ,
CancellationToken cancellationToken )
{
// Create Order entity from command object
var order = CreateNewOrder ( command . Order );
// Save to database
dbContext . Orders . Add ( order );
await dbContext . SaveChangesAsync ( cancellationToken );
// Return result
return new CreateOrderResult ( order . Id . Value );
}
private Order CreateNewOrder ( OrderDto orderDto )
{
var shippingAddress = Address . Of (
orderDto . ShippingAddress . FirstName ,
orderDto . ShippingAddress . LastName ,
orderDto . ShippingAddress . EmailAddress ,
orderDto . ShippingAddress . AddressLine ,
orderDto . ShippingAddress . Country ,
orderDto . ShippingAddress . State ,
orderDto . ShippingAddress . ZipCode );
var billingAddress = Address . Of (
orderDto . BillingAddress . FirstName ,
orderDto . BillingAddress . LastName ,
orderDto . BillingAddress . EmailAddress ,
orderDto . BillingAddress . AddressLine ,
orderDto . BillingAddress . Country ,
orderDto . BillingAddress . State ,
orderDto . BillingAddress . ZipCode );
var newOrder = Order . Create (
id : OrderId . Of ( Guid . NewGuid ()),
customerId : CustomerId . Of ( orderDto . CustomerId ),
orderName : OrderName . Of ( orderDto . OrderName ),
shippingAddress : shippingAddress ,
billingAddress : billingAddress ,
payment : Payment . Of (
orderDto . Payment . CardName ,
orderDto . Payment . CardNumber ,
orderDto . Payment . Expiration ,
orderDto . Payment . Cvv ,
orderDto . Payment . PaymentMethod )
);
foreach ( var orderItemDto in orderDto . OrderItems )
{
newOrder . Add (
ProductId . Of ( orderItemDto . ProductId ),
orderItemDto . Quantity ,
orderItemDto . Price );
}
return newOrder ;
}
}
Queries (Read Operations)
Queries retrieve data without side effects:
src/Services/Ordering/Ordering.Application/Orders/Queries/GetOrders/GetOrdersQuery.cs
using BuildingBlocks . Pagination ;
namespace Ordering . Application . Orders . Queries . GetOrders ;
public record GetOrdersQuery ( PaginationRequest PaginationRequest )
: IQuery < GetOrdersResult >;
public record GetOrdersResult ( PaginatedResult < OrderDto > Orders );
src/Services/Ordering/Ordering.Application/Orders/Queries/GetOrders/GetOrdersHandler.cs
namespace Ordering . Application . Orders . Queries . GetOrders ;
public class GetOrdersHandler ( IApplicationDbContext dbContext )
: IQueryHandler < GetOrdersQuery , GetOrdersResult >
{
public async Task < GetOrdersResult > Handle (
GetOrdersQuery query ,
CancellationToken cancellationToken )
{
var pageIndex = query . PaginationRequest . PageIndex ;
var pageSize = query . PaginationRequest . PageSize ;
var totalCount = await dbContext . Orders . LongCountAsync ( cancellationToken );
var orders = await dbContext . Orders
. Include ( o => o . OrderItems )
. OrderBy ( o => o . OrderName . Value )
. Skip ( pageSize * pageIndex )
. Take ( pageSize )
. ToListAsync ( cancellationToken );
return new GetOrdersResult (
new PaginatedResult < OrderDto >(
pageIndex ,
pageSize ,
totalCount ,
orders . ToOrderDtoList ()));
}
}
Domain Event Handlers
React to domain events for side effects:
src/Services/Ordering/Ordering.Application/Orders/EventHandlers/Domain/OrderCreatedEventHandler.cs
using MassTransit ;
using Microsoft . FeatureManagement ;
namespace Ordering . Application . Orders . EventHandlers . Domain ;
public class OrderCreatedEventHandler
( IPublishEndpoint publishEndpoint ,
IFeatureManager featureManager ,
ILogger < OrderCreatedEventHandler > logger )
: INotificationHandler < OrderCreatedEvent >
{
public async Task Handle (
OrderCreatedEvent domainEvent ,
CancellationToken cancellationToken )
{
logger . LogInformation ( "Domain Event handled: {DomainEvent}" ,
domainEvent . GetType (). Name );
if ( await featureManager . IsEnabledAsync ( "OrderFullfilment" ))
{
var orderCreatedIntegrationEvent = domainEvent . order . ToOrderDto ();
await publishEndpoint . Publish (
orderCreatedIntegrationEvent ,
cancellationToken );
}
}
}
Dependency Registration
src/Services/Ordering/Ordering.Application/DependencyInjection.cs
namespace Ordering . Application ;
public static class DependencyInjection
{
public static IServiceCollection AddApplicationServices (
this IServiceCollection services ,
IConfiguration configuration )
{
// Register MediatR with behaviors
services . AddMediatR ( config =>
{
config . RegisterServicesFromAssembly ( Assembly . GetExecutingAssembly ());
config . AddOpenBehavior ( typeof ( ValidationBehavior <,>));
config . AddOpenBehavior ( typeof ( LoggingBehavior <,>));
});
services . AddFeatureManagement ();
services . AddMessageBroker ( configuration , Assembly . GetExecutingAssembly ());
return services ;
}
}
3. Infrastructure Layer
External Concerns Implements interfaces defined in Domain/Application layers. Handles databases, external APIs, file systems, etc.
Entity Framework DbContext
namespace Ordering . Infrastructure . Data ;
public class ApplicationDbContext : DbContext , IApplicationDbContext
{
public ApplicationDbContext ( DbContextOptions < ApplicationDbContext > options )
: base ( options ) { }
public DbSet < Order > Orders => Set < Order >();
public DbSet < OrderItem > OrderItems => Set < OrderItem >();
public DbSet < Customer > Customers => Set < Customer >();
protected override void OnModelCreating ( ModelBuilder builder )
{
builder . ApplyConfigurationsFromAssembly ( Assembly . GetExecutingAssembly ());
base . OnModelCreating ( builder );
}
}
Domain Event Dispatcher
Automatically dispatches domain events when saving:
src/Services/Ordering/Ordering.Infrastructure/Data/Interceptors/DispatchDomainEventsInterceptor.cs
using MediatR ;
using Microsoft . EntityFrameworkCore . Diagnostics ;
namespace Ordering . Infrastructure . Data . Interceptors ;
public class DispatchDomainEventsInterceptor ( IMediator mediator )
: SaveChangesInterceptor
{
public override async ValueTask < InterceptionResult < int >> SavingChangesAsync (
DbContextEventData eventData ,
InterceptionResult < int > result ,
CancellationToken cancellationToken = default )
{
await DispatchDomainEvents ( eventData . Context );
return await base . SavingChangesAsync ( eventData , result , cancellationToken );
}
public async Task DispatchDomainEvents ( DbContext ? context )
{
if ( context == null ) return ;
// Get all aggregates with domain events
var aggregates = context . ChangeTracker
. Entries < IAggregate >()
. Where ( a => a . Entity . DomainEvents . Any ())
. Select ( a => a . Entity );
// Collect all domain events
var domainEvents = aggregates
. SelectMany ( a => a . DomainEvents )
. ToList ();
// Clear events from aggregates
aggregates . ToList (). ForEach ( a => a . ClearDomainEvents ());
// Publish events via MediatR
foreach ( var domainEvent in domainEvents )
await mediator . Publish ( domainEvent );
}
}
Dependency Registration
src/Services/Ordering/Ordering.Infrastructure/DependencyInjection.cs
namespace Ordering . Infrastructure ;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructureServices (
this IServiceCollection services ,
IConfiguration configuration )
{
var connectionString = configuration . GetConnectionString ( "Database" );
// Register interceptors
services . AddScoped < ISaveChangesInterceptor , AuditableEntityInterceptor >();
services . AddScoped < ISaveChangesInterceptor , DispatchDomainEventsInterceptor >();
// Configure DbContext with interceptors
services . AddDbContext < ApplicationDbContext >(( sp , options ) =>
{
options . AddInterceptors ( sp . GetServices < ISaveChangesInterceptor >());
options . UseSqlServer ( connectionString );
});
services . AddScoped < IApplicationDbContext , ApplicationDbContext >();
return services ;
}
}
4. API Layer (Presentation)
User Interface / HTTP API Exposes functionality via HTTP endpoints. Uses Minimal APIs with Carter for clean endpoint organization.
Minimal API Endpoint
src/Services/Ordering/Ordering.API/Endpoints/CreateOrder.cs
namespace Ordering . API . Endpoints ;
public record CreateOrderRequest ( OrderDto Order );
public record CreateOrderResponse ( Guid Id );
public class CreateOrder : ICarterModule
{
public void AddRoutes ( IEndpointRouteBuilder app )
{
app . MapPost ( "/orders" , async ( CreateOrderRequest request , ISender sender ) =>
{
// Map request to command
var command = request . Adapt < CreateOrderCommand >();
// Send command via MediatR
var result = await sender . Send ( command );
// Map result to response
var response = result . Adapt < CreateOrderResponse >();
return Results . Created ( $"/orders/ { response . Id } " , response );
})
. WithName ( "CreateOrder" )
. Produces < CreateOrderResponse >( StatusCodes . Status201Created )
. ProducesProblem ( StatusCodes . Status400BadRequest )
. WithSummary ( "Create Order" )
. WithDescription ( "Create Order" );
}
}
Program.cs
src/Services/Ordering/Ordering.API/Program.cs
using Ordering . API ;
using Ordering . Application ;
using Ordering . Infrastructure ;
using Ordering . Infrastructure . Data . Extensions ;
var builder = WebApplication . CreateBuilder ( args );
// Add services to the container
builder . Services
. AddApplicationServices ( builder . Configuration )
. AddInfrastructureServices ( builder . Configuration )
. AddApiServices ( builder . Configuration );
var app = builder . Build ();
// Configure the HTTP request pipeline
app . UseApiServices ();
if ( app . Environment . IsDevelopment ())
{
await app . InitialiseDatabaseAsync ();
}
app . Run ();
Dependency Flow
The Dependency Rule : Source code dependencies must point inward toward the domain.
Domain has no dependencies (pure business logic)
Application depends only on Domain
Infrastructure depends on Domain and Application
API depends on Application
Benefits of Clean Architecture
Testability Business logic can be tested without databases or frameworks
Maintainability Clear separation makes code easier to understand and modify
Framework Independence Not locked into any specific framework or library
Database Independence Can switch databases without affecting business logic
Scalability Each layer can scale independently
Team Collaboration Teams can work on different layers with minimal conflicts
DDD Principles Learn about Domain-Driven Design patterns used in the Domain layer
CQRS Pattern Understand how CQRS is implemented in the Application layer
Vertical Slice Compare with the simpler Vertical Slice pattern in Catalog service
Microservices Pattern See how Clean Architecture fits into the microservices context