Skip to content

An educational REST API demonstrating Domain-Driven Design (DDD) patterns, Clean Architecture, and .NET best practices using .NET and .NET Aspire.

Notifications You must be signed in to change notification settings

fkucukkara/domain-driven-design-playground

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

23 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

DDD Playground - Educational Showcase API

An educational REST API demonstrating Domain-Driven Design (DDD) patterns, Clean Architecture, and .NET best practices using .NET 10 and .NET Aspire.

.NET Aspire License

πŸ“‹ Table of Contents

🎯 Overview

This project is an educational showcase of DDD tactical patterns, Clean Architecture principles, and modern .NET development practices. It implements a simplified order management system - complex enough to demonstrate key concepts, simple enough to understand quickly.

Goals

  • βœ… DDD Tactical Patterns: Aggregates, Entities, Value Objects, Domain Services, Repository Pattern, Domain Events
  • βœ… Clean Architecture: Strict layer separation with proper dependency flow
  • βœ… Persistence Ignorance: Domain models completely separated from EF Core entities
  • βœ… CQRS Lite: Commands for writes, Queries for reads using MediatR
  • βœ… Minimal APIs: Modern ASP.NET Core endpoints with MapGroup and TypedResults
  • βœ… .NET Aspire: Cloud-native orchestration, observability, and developer experience

πŸ—οΈ Architecture

Layer Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Presentation Layer                    β”‚
β”‚              (DDDPlayground.ApiService)                  β”‚
β”‚         Minimal APIs β€’ OpenAPI β€’ HTTP Endpoints          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚ depends on
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Application Layer                      β”‚
β”‚              (DDDPlayground.Application)                 β”‚
β”‚       CQRS Handlers β€’ DTOs β€’ Use Case Orchestration      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚ depends on
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Domain Layer                         β”‚
β”‚                (DDDPlayground.Domain)                    β”‚
β”‚    Aggregates β€’ Entities β€’ Value Objects β€’ Services      β”‚
β”‚              (Zero Infrastructure Dependencies)           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β–²
                     β”‚ implements interfaces
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 Infrastructure Layer                     β”‚
β”‚             (DDDPlayground.Infrastructure)               β”‚
β”‚  EF Core β€’ PostgreSQL β€’ Repositories β€’ Persistence Modelsβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Three-Model Layer Separation

This project demonstrates true domain/persistence separation with three distinct model layers:

  1. Domain Models (DDDPlayground.Domain)

    • Pure business logic, zero persistence attributes
    • Rich domain model with encapsulation
    • Example: Order aggregate with private setters and business methods
  2. Persistence Models (DDDPlayground.Infrastructure/Persistence/Models)

    • Separate EF Core entities (e.g., OrderEntity, OrderItemEntity)
    • Optimized for database storage
    • Different class hierarchy from domain
  3. API Models (DDDPlayground.ApiService/Models)

    • Request/Response DTOs
    • Optimized for HTTP communication
    • Prevent over-posting

Manual Mappers in DDDPlayground.Infrastructure/Persistence/Mappers handle explicit transformation between layers.

πŸŽ“ Key Patterns Demonstrated

Domain-Driven Design

  • Aggregate Root: Order controls access to OrderItem entities
  • Value Objects: Money, OrderId, CustomerId with immutability and value equality
  • Domain Services: PricingService for discount calculation logic
  • Domain Events: OrderConfirmedEvent for in-memory event dispatching
  • Repository Pattern: IOrderRepository interface in domain, implementation in infrastructure

Clean Architecture

  • Dependency Inversion: Domain layer has zero dependencies
  • Persistence Ignorance: No EF Core attributes in domain models
  • Manual Mapping: Explicit ToDomain() and ToEntity() methods (no AutoMapper)
  • Use Case Orchestration: MediatR handlers coordinate workflows

Modern .NET Practices

  • Minimal APIs: MapGroup(), TypedResults, static handlers for performance
  • CQRS Lite: Separate commands (CreateOrderCommand) and queries (GetOrderQuery)
  • Unit of Work: Transaction boundaries per use case
  • .NET Aspire: Orchestration, PostgreSQL hosting, observability dashboard

πŸ› οΈ Technology Stack

| Category | Technology | |----------|-----------|| | Framework | .NET 10.0 | | Orchestration | .NET Aspire 13.1.0 | | API | ASP.NET Core Minimal APIs | | Database | PostgreSQL (Aspire-hosted) | | ORM | Entity Framework Core 10.0 | | CQRS | MediatR 13.1 | | Validation | FluentValidation 12.1 | | Documentation | OpenAPI/Swagger |

πŸš€ Getting Started

Prerequisites

Install .NET Aspire Workload

dotnet workload update
dotnet workload install aspire

Running the Application

Option 1: Visual Studio

  1. Open DDDPlayground.sln
  2. Set DDDPlayground.AppHost as the startup project
  3. Press F5 or click "Run"

Option 2: Command Line

# From solution root
cd src\DDDPlayground.AppHost

# Run the application
dotnet run

Accessing the Application

Once running, you'll see:

Database Migrations

The database is automatically created on first run. If you need to manage migrations manually:

# Add a new migration (from solution root)
dotnet ef migrations add <MigrationName> --project src/DDDPlayground.Infrastructure --startup-project src/DDDPlayground.ApiService

# Update database
dotnet ef database update --project src/DDDPlayground.Infrastructure --startup-project src/DDDPlayground.ApiService

πŸ“ Project Structure

DDDPlayground/
β”œβ”€β”€ .github/                          # GitHub Actions, issue templates
β”œβ”€β”€ DDDPlayground.AppHost/            # Aspire orchestration host
β”‚   └── AppHost.cs                    # Configures PostgreSQL, pgAdmin, API service
β”œβ”€β”€ DDDPlayground.ServiceDefaults/    # Aspire shared configuration
β”‚   └── Extensions.cs                 # Service defaults (telemetry, health checks)β”œβ”€β”€ BenchmarkSuite/                   # Performance benchmarks
β”‚   β”œβ”€β”€ BenchmarkSuite.csproj
β”‚   β”œβ”€β”€ DDDPlaygroundBenchmarks.cs
β”‚   └── Program.csβ”œβ”€β”€ DDDPlayground.Domain/             # Pure domain logic (ZERO dependencies)
β”‚   β”œβ”€β”€ Orders/
β”‚   β”‚   β”œβ”€β”€ Order.cs                  # Aggregate root with business logic
β”‚   β”‚   β”œβ”€β”€ OrderItem.cs              # Entity within aggregate
β”‚   β”‚   β”œβ”€β”€ OrderStatus.cs            # Enum
β”‚   β”‚   └── IOrderRepository.cs       # Repository interface
β”‚   β”œβ”€β”€ ValueObjects/
β”‚   β”‚   β”œβ”€β”€ Money.cs                  # Value object with operators
β”‚   β”‚   β”œβ”€β”€ OrderId.cs                # Strongly-typed ID
β”‚   β”‚   β”œβ”€β”€ CustomerId.cs             # Strongly-typed ID
β”‚   β”‚   β”œβ”€β”€ ProductId.cs              # Strongly-typed ID
β”‚   β”‚   └── Email.cs                  # Validated email
β”‚   β”œβ”€β”€ Services/
β”‚   β”‚   └── PricingService.cs         # Domain service
β”‚   β”œβ”€β”€ Events/
β”‚   β”‚   β”œβ”€β”€ OrderConfirmedEvent.cs    # Domain event
β”‚   β”‚   └── OrderCreatedEvent.cs      # Domain event
β”‚   └── Exceptions/
β”‚       └── DomainException.cs        # Base domain exception
β”œβ”€β”€ DDDPlayground.Application/        # Use case orchestration
β”‚   β”œβ”€β”€ Orders/
β”‚   β”‚   β”œβ”€β”€ CreateOrder/
β”‚   β”‚   β”‚   β”œβ”€β”€ CreateOrderCommand.cs # Command DTO
β”‚   β”‚   β”‚   └── CreateOrderHandler.cs # Command handler
β”‚   β”‚   β”œβ”€β”€ ConfirmOrder/
β”‚   β”‚   β”‚   β”œβ”€β”€ ConfirmOrderCommand.cs
β”‚   β”‚   β”‚   └── ConfirmOrderHandler.cs
β”‚   β”‚   β”œβ”€β”€ GetOrder/
β”‚   β”‚   β”‚   β”œβ”€β”€ GetOrderQuery.cs      # Query DTO
β”‚   β”‚   β”‚   └── GetOrderHandler.cs    # Query handler
β”‚   β”‚   β”œβ”€β”€ EventHandlers/            # Domain event handlers
β”‚   β”‚   β”‚   β”œβ”€β”€ OrderConfirmedEventHandler.cs
β”‚   β”‚   β”‚   └── OrderCreatedEventHandler.cs
β”‚   β”‚   β”œβ”€β”€ Notifications/            # MediatR notifications
β”‚   β”‚   └── OrderResponse.cs          # Response DTO with manual mapping
β”‚   β”œβ”€β”€ Common/
β”‚   β”‚   └── IUnitOfWork.cs            # Transaction boundary
β”‚   └── DependencyInjection.cs        # MediatR registration
β”œβ”€β”€ DDDPlayground.Infrastructure/     # Persistence implementation
β”‚   β”œβ”€β”€ Persistence/
β”‚   β”‚   β”œβ”€β”€ Models/                   # Separate persistence entities
β”‚   β”‚   β”‚   β”œβ”€β”€ OrderEntity.cs        # EF Core entity (NOT domain Order)
β”‚   β”‚   β”‚   └── OrderItemEntity.cs    # EF Core entity
β”‚   β”‚   β”œβ”€β”€ Mappers/                  # Manual domain ↔ persistence mapping
β”‚   β”‚   β”‚   β”œβ”€β”€ OrderMapper.cs        # ToEntity() / ToDomain()
β”‚   β”‚   β”‚   └── OrderItemMapper.cs
β”‚   β”‚   β”œβ”€β”€ Configurations/           # EF Core Fluent API
β”‚   β”‚   β”‚   β”œβ”€β”€ OrderEntityConfiguration.cs
β”‚   β”‚   β”‚   └── OrderItemEntityConfiguration.cs
β”‚   β”‚   β”œβ”€β”€ Repositories/
β”‚   β”‚   β”‚   └── OrderRepository.cs    # IOrderRepository implementation
β”‚   β”‚   β”œβ”€β”€ Migrations/               # EF Core migrations (empty - auto-migrate on startup)
β”‚   β”‚   β”œβ”€β”€ Extensions/               # DbContext extensions
β”‚   β”‚   β”œβ”€β”€ AppDbContext.cs           # DbContext with DbSet<OrderEntity>
β”‚   β”‚   └── AppDbContextFactory.cs    # Design-time DbContext factory
β”‚   └── DependencyInjection.cs        # Repository registration
β”œβ”€β”€ DDDPlayground.ApiService/         # HTTP API layer
β”‚   β”œβ”€β”€ Endpoints/
β”‚   β”‚   └── OrderEndpoints.cs         # Minimal API endpoints with MapGroup
β”‚   β”œβ”€β”€ Models/
β”‚   β”‚   └── CreateOrderRequest.cs     # API request DTOs
β”‚   β”œβ”€β”€ Program.cs                    # Aspire + Minimal API setup
β”‚   β”œβ”€β”€ appsettings.json              # Configuration
β”‚   └── DDDPlayground.ApiService.http # HTTP client test file
β”œβ”€β”€ .gitignore                        # Git ignore rules
β”œβ”€β”€ DDDPlayground.sln                 # Solution file
β”œβ”€β”€ MVP.md                            # Detailed MVP specification
└── README.md                         # This file

πŸ”Œ API Endpoints

Orders

All endpoints are prefixed with /api/orders.

Create Order

POST /api/orders
Content-Type: application/json

{
  "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "items": [
    {
      "productId": "8fa85f64-5717-4562-b3fc-2c963f66afa6",
      "quantity": 2,
      "unitPrice": 99.99,
      "currency": "USD"
    }
  ]
}
Response: 201 Created
{
  "id": "1fa85f64-5717-4562-b3fc-2c963f66afa6",
  "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "status": "Draft",
  "subtotalAmount": 199.98,
  "subtotalCurrency": "USD",
  "discountAmount": 0.0,
  "discountCurrency": "USD",
  "totalAmount": 199.98,
  "totalCurrency": "USD",
  "createdAt": "2025-11-01T12:00:00Z",
  "confirmedAt": null,
  "items": [...]
}

Get Order

GET /api/orders/{id}

Response: 200 OK (same structure as Create response)

Confirm Order

POST /api/orders/{id}/confirm

Response: 200 OK (returns updated order with status: "Confirmed" and confirmedAt timestamp)

Testing with HTTP Client

Use the included DDDPlayground.ApiService.http file with Visual Studio or VS Code REST Client extension.

πŸ“š Learning Resources

Key Files to Study

  1. Domain Model: src/DDDPlayground.Domain/Orders/Order.cs

    • Rich domain model with encapsulation
    • Business rules enforcement
    • Factory methods and reconstitution
  2. Persistence Separation: src/DDDPlayground.Infrastructure/Persistence/

    • Models/OrderEntity.cs - Separate EF entity
    • Mappers/OrderMapper.cs - Manual mapping
    • Configurations/OrderEntityConfiguration.cs - Fluent API
  3. CQRS Pattern: src/DDDPlayground.Application/Orders/

    • Commands vs. Queries
    • Handler responsibilities
    • DTO mapping
  4. Minimal APIs: src/DDDPlayground.ApiService/Endpoints/OrderEndpoints.cs

    • MapGroup organization
    • TypedResults for type safety
    • Static handlers for performance

Patterns Explained

Rich Domain Model

// ❌ Anemic (all properties public, no logic)
public class Order 
{ 
    public Guid Id { get; set; }
    public string Status { get; set; }
}

// βœ… Rich (encapsulated, with business logic)
public class Order 
{ 
    public OrderId Id { get; private set; }
    public OrderStatus Status { get; private set; }
    
    public void Confirm() 
    {
        if (Status != OrderStatus.Draft)
            throw new DomainException("Only draft orders can be confirmed");
        Status = OrderStatus.Confirmed;
    }
}

Persistence Ignorance

// ❌ Domain model with EF attributes
public class Order 
{
    [Key]
    public Guid Id { get; set; }
    
    [Column("order_status")]
    public string Status { get; set; }
}

// βœ… Separate domain and persistence models
// Domain: Order.cs (pure business logic)
public sealed class Order { /* ... */ }

// Persistence: OrderEntity.cs (EF Core entity)
public class OrderEntity { /* ... */ }

// Mapper: OrderMapper.cs (explicit transformation)
public static class OrderMapper 
{
    public static OrderEntity ToEntity(Order order) { /* ... */ }
    public static Order ToDomain(OrderEntity entity) { /* ... */ }
}

External Resources

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

About

An educational REST API demonstrating Domain-Driven Design (DDD) patterns, Clean Architecture, and .NET best practices using .NET and .NET Aspire.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages