DEV Community

nikosst
nikosst

Posted on

A Simple Clean Architecture Structure for ASP.NET Core Projects

Many developers hear about Clean Architecture but struggle with one simple question:

How should I structure my projects?

In this article we will look at a simple and practical project structure for ASP.NET Core using the ideas from Clean Architecture, introduced by software engineer Robert C. Martin (Uncle Bob).

The goal is simple:

  • keep business logic independent
  • keep infrastructure replaceable
  • keep the system easy to maintain

Let’s break it down.


The 4 Project Structure

A very clean approach is to split the solution into four projects.

MyProject

MyProject.Domain
MyProject.Application
MyProject.Infrastructure
MyProject.API

Each project has a very specific responsibility.


1. Domain Layer (The Core of the System)

The Domain layer contains the business rules.

Nothing here should depend on frameworks, databases, or external services.

Typical folders:

Domain
├── Entities
├── ValueObjects
├── Enums
└── Exceptions

Example entity:

public class Order
{
    public Guid Id { get; private set; }

    public decimal Amount { get; private set; }

    public void Pay()
    {
        // business rule
    }
}
Enter fullscreen mode Exit fullscreen mode

Important rule:

The Domain layer should have zero dependencies.


2. Application Layer

The Application layer orchestrates the use cases of the system.

This is where we define interfaces for external dependencies such as repositories or services.

Example structure:

Application
├── Interfaces
├── Services
├── DTOs
├── Commands
└── Queries

Example interface:

public interface IOrderRepository
{
    Task<Order> GetByIdAsync(Guid id);
    Task SaveAsync(Order order);
}
Enter fullscreen mode Exit fullscreen mode

Application services use these interfaces without knowing how they are implemented.

public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }

    public async Task PayOrder(Guid id)
    {
        var order = await _repository.GetByIdAsync(id);

        order.Pay();

        await _repository.SaveAsync(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Infrastructure Layer

The Infrastructure layer contains technical implementations.

Examples:

  • database access
  • external APIs
  • email services
  • file storage

Example structure:

Infrastructure
├── Persistence
├── Services
└── External

Repository implementation:

public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;

    public OrderRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<Order> GetByIdAsync(Guid id)
    {
        return await _context.Orders.FindAsync(id);
    }

    public async Task SaveAsync(Order order)
    {
        _context.Update(order);
        await _context.SaveChangesAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice something important:

The Infrastructure layer depends on the Application layer, not the other way around.


4. API Layer

The API layer is the entry point of the system.

Typical structure:

API
├── Controllers
└── Program.cs

Example controller:

[ApiController]
[Route("orders")]
public class OrdersController : ControllerBase
{
    private readonly OrderService _service;

    public OrdersController(OrderService service)
    {
        _service = service;
    }

    [HttpPost("{id}/pay")]
    public async Task<IActionResult> Pay(Guid id)
    {
        await _service.PayOrder(id);

        return Ok();
    }
}
Enter fullscreen mode Exit fullscreen mode

Dependency Direction (The Most Important Rule)

The direction of dependencies should look like this:

API

Application

Domain

Infrastructure → Application

This rule ensures that business logic remains independent from frameworks and databases.


Dependency Injection

The concrete implementations are wired up in the API project:

builder.Services.AddScoped<IOrderRepository, OrderRepository>();

This allows the Application layer to stay clean and unaware of infrastructure details.


A Senior Developer Tip

As systems grow, many teams move from simple services to patterns like CQRS (Command Query Responsibility Segregation).

Instead of this:

Application
└── Services

you may see something like this:

Application
├── Commands
├── Queries
├── Handlers
└── Behaviors

This approach scales better in larger systems.


Final Thoughts

Clean Architecture is not about adding complexity.

It is about separating responsibilities so your system becomes:

  • easier to test
  • easier to maintain
  • easier to evolve

Start simple, keep the layers clean, and your architecture will scale naturally as your application grows.


nikosstit@gmail.com

Top comments (0)