Skip to main content

Overview

Clean Architecture is an architectural pattern that emphasizes separation of concerns, independence of frameworks, and testability. Intent Architect provides comprehensive support for Clean Architecture through its modular system, generating code that naturally follows these principles.

Key Benefits

  • Separation of Concerns: Clear boundaries between business logic and infrastructure
  • Framework Independence: Core business logic doesn’t depend on external frameworks
  • Testability: Business logic can be tested without UI, database, or external dependencies
  • Maintainability: Changes in one layer don’t cascade to others

The Layers

Intent Architect organizes your application into distinct layers, each with specific responsibilities:

Domain Layer

The innermost layer containing business logic and domain entities. Generated Artifacts:
  • Domain entities (via Intent.Entities module)
  • Value objects (via Intent.ValueObjects module)
  • Domain events (via Intent.DomainEvents module)
  • Domain services (via Intent.DomainServices module)
Key Characteristics:
  • No dependencies on other layers
  • Contains business rules and invariants
  • Framework-agnostic
[IntentManaged(Mode.Merge, Signature = Mode.Fully)]
public class Order
{
    public Guid Id { get; set; }
    public string OrderNumber { get; set; }
    public DateTime OrderDate { get; set; }
    public OrderStatus Status { get; private set; }
    
    private readonly List<OrderItem> _items = new();
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();

    // Business logic encapsulated in the domain
    public void AddItem(Product product, int quantity)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Cannot add items to a confirmed order");
        
        var item = new OrderItem(product.Id, quantity, product.Price);
        _items.Add(item);
    }

    public void Confirm()
    {
        if (_items.Count == 0)
            throw new InvalidOperationException("Cannot confirm an empty order");
        
        Status = OrderStatus.Confirmed;
    }
}

Application Layer

Orchestrates the flow of data and coordinates domain objects to perform use cases. Generated Artifacts:
  • Command/Query handlers (via Intent.Application.MediatR module)
  • Application services (via Intent.Application.ServiceImplementations module)
  • DTOs (via Intent.Application.Dtos module)
  • Validation logic (via Intent.Application.FluentValidation module)
  • Repository interfaces (via Intent.Entities.Repositories.Api module)
Key Characteristics:
  • Depends only on the Domain layer
  • Defines repository interfaces (not implementations)
  • Contains application-specific business rules
  • Orchestrates use cases
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IProductRepository _productRepository;

    public CreateOrderCommandHandler(
        IOrderRepository orderRepository,
        IProductRepository productRepository)
    {
        _orderRepository = orderRepository;
        _productRepository = productRepository;
    }

    public async Task<Guid> Handle(
        CreateOrderCommand request, 
        CancellationToken cancellationToken)
    {
        // Application layer orchestrates the use case
        var order = new Order
        {
            OrderNumber = GenerateOrderNumber(),
            OrderDate = DateTime.UtcNow,
            CustomerId = request.CustomerId
        };

        foreach (var item in request.Items)
        {
            var product = await _productRepository.FindByIdAsync(
                item.ProductId, 
                cancellationToken);
            
            if (product == null)
                throw new NotFoundException($"Product {item.ProductId} not found");
            
            order.AddItem(product, item.Quantity);
        }

        _orderRepository.Add(order);
        return order.Id;
    }
}

Infrastructure Layer

Implements interfaces defined in the Application layer and provides access to external concerns. Generated Artifacts:
  • Repository implementations (via Intent.EntityFrameworkCore.Repositories module)
  • DbContext and configurations (via Intent.EntityFrameworkCore module)
  • External service integrations (via Intent.Integration.HttpClients module)
  • Message bus implementations (via Intent.Eventing.* modules)
Key Characteristics:
  • Depends on Application and Domain layers
  • Contains framework-specific code
  • Implements data access and external integrations
  • Plugin architecture for different technologies
[IntentManaged(Mode.Merge, Signature = Mode.Fully)]
public class OrderRepository : RepositoryBase<Order, Order, ApplicationDbContext>, IOrderRepository
{
    public OrderRepository(ApplicationDbContext dbContext) : base(dbContext)
    {
    }

    public async Task<Order?> FindByIdAsync(
        Guid id, 
        CancellationToken cancellationToken = default)
    {
        return await FindAsync(x => x.Id == id, cancellationToken);
    }

    public async Task<List<Order>> FindByCustomerIdAsync(
        Guid customerId,
        CancellationToken cancellationToken = default)
    {
        return await FindAllAsync(
            x => x.CustomerId == customerId,
            cancellationToken);
    }
}

Presentation Layer

Handles external requests and presents data to users or external systems. Generated Artifacts:
  • REST API controllers (via Intent.AspNetCore.Controllers module)
  • GraphQL endpoints (via Intent.HotChocolate.GraphQL module)
  • gRPC services (via Intent.AspNetCore.Grpc module)
  • Blazor components (via Intent.Blazor module)
Key Characteristics:
  • Depends on Application layer
  • Handles HTTP requests/responses
  • Dispatches to application layer handlers
  • Manages authentication and authorization
[ApiController]
public class OrdersController : ControllerBase
{
    private readonly ISender _mediator;

    public OrdersController(ISender mediator)
    {
        _mediator = mediator;
    }

    [HttpPost("api/orders")]
    [ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)]
    public async Task<ActionResult<Guid>> CreateOrder(
        [FromBody] CreateOrderCommand command,
        CancellationToken cancellationToken)
    {
        var orderId = await _mediator.Send(command, cancellationToken);
        return CreatedAtAction(nameof(GetOrderById), new { id = orderId }, orderId);
    }

    [HttpGet("api/orders/{id}")]
    [ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)]
    public async Task<ActionResult<OrderDto>> GetOrderById(
        [FromRoute] Guid id,
        CancellationToken cancellationToken)
    {
        var order = await _mediator.Send(new GetOrderByIdQuery(id), cancellationToken);
        return Ok(order);
    }
}

Dependency Flow

In Clean Architecture, dependencies point inward toward the domain:
The Dependency Rule: Source code dependencies must point only inward, toward higher-level policies. The Domain layer has no dependencies, while outer layers depend on inner layers.

Project Structure

Intent Architect typically generates the following project structure:
MyApplication/
├── MyApplication.Domain/              # Domain Layer
│   ├── Entities/
│   ├── ValueObjects/
│   ├── DomainEvents/
│   └── Services/
├── MyApplication.Application/         # Application Layer
│   ├── Commands/
│   ├── Queries/
│   ├── Dtos/
│   ├── Interfaces/                   # Repository interfaces
│   └── Common/
├── MyApplication.Infrastructure/      # Infrastructure Layer
│   ├── Persistence/
│   │   ├── DbContexts/
│   │   ├── Configurations/
│   │   └── Repositories/
│   ├── Services/
│   └── DependencyInjection.cs
└── MyApplication.Api/                 # Presentation Layer
    ├── Controllers/
    ├── Configuration/
    └── Program.cs

Module Configuration

Essential Modules for Clean Architecture

Domain Layer

  • Intent.Entities
  • Intent.ValueObjects
  • Intent.DomainEvents
  • Intent.DomainServices

Application Layer

  • Intent.Application.MediatR
  • Intent.Application.Dtos
  • Intent.Application.FluentValidation
  • Intent.Entities.Repositories.Api

Infrastructure Layer

  • Intent.EntityFrameworkCore
  • Intent.EntityFrameworkCore.Repositories
  • Intent.Infrastructure.DependencyInjection

Presentation Layer

  • Intent.AspNetCore.Controllers
  • Intent.AspNetCore.Controllers.Dispatch.MediatR
  • Intent.AspNetCore

Best Practices

The Domain layer should have no dependencies on infrastructure concerns:
  • No ORM attributes on entities
  • No HTTP/API concerns
  • No framework-specific code
  • Only business logic and domain rules
Repository and service interfaces belong in the Application layer:
  • Application layer defines the contract
  • Infrastructure layer provides the implementation
  • Enables testing with mocks/stubs
  • Maintains dependency inversion
Never expose domain entities directly through APIs:
  • Map entities to DTOs using AutoMapper or Mapperly
  • DTOs can have different structures than entities
  • Protects domain model from API changes
  • Prevents over-posting vulnerabilities
Take advantage of Intent’s merge strategies:
  • Business logic methods use Mode.Merge for manual control
  • Infrastructure code uses Mode.Fully for full automation
  • Customize behavior through decorators and extensions

Testing Strategy

Unit Testing Domain Logic

public class OrderTests
{
    [Fact]
    public void AddItem_WhenOrderIsConfirmed_ThrowsException()
    {
        // Arrange
        var order = new Order();
        order.Confirm();
        var product = new Product { Id = Guid.NewGuid(), Price = 10.0m };

        // Act & Assert
        Assert.Throws<InvalidOperationException>(
            () => order.AddItem(product, 1));
    }
}

Integration Testing Application Layer

public class CreateOrderCommandHandlerTests
{
    [Fact]
    public async Task Handle_ValidCommand_CreatesOrder()
    {
        // Arrange
        var orderRepository = new Mock<IOrderRepository>();
        var productRepository = new Mock<IProductRepository>();
        var handler = new CreateOrderCommandHandler(
            orderRepository.Object,
            productRepository.Object);

        // Act
        var result = await handler.Handle(
            new CreateOrderCommand { /* ... */ },
            CancellationToken.None);

        // Assert
        orderRepository.Verify(x => x.Add(It.IsAny<Order>()), Times.Once);
    }
}

CQRS

Command Query Responsibility Segregation

DDD

Domain-Driven Design principles

Repository

Data access abstraction

Additional Resources

Build docs developers (and LLMs) love