Clean Architecture: Introduction to the Project Structure
Overview
Clean Architecture is a powerful software design pattern that promotes a clear separation of concerns, making your application’s core business logic independent of external dependencies like databases, user interfaces, or frameworks. By following this architecture, systems become maintainable, testable, and adaptable, preparing them for both current demands and future growth.
In this article, we’ll explore the foundational concepts of Clean Architecture and how they’re implemented in our sample Contact Management Application, which is built as a .NET Core Web API. This application provides a practical and simplified example of Clean Architecture principles and is part of a series covering various aspects of this design.
1. Why Use Clean Architecture?
Clean Architecture is designed to address common challenges in software development:
- Maintainability: By separating concerns, each layer of the application can be modified independently without impacting others.
- Testability: Decoupling the business logic from external dependencies simplifies unit testing.
- Scalability: Clear boundaries allow for easy expansion or modification of system components (e.g., swapping databases or adding features).
- Flexibility: The core logic is independent of frameworks or infrastructure, ensuring the system remains adaptable.
These benefits make Clean Architecture an excellent choice for complex applications that require robustness and future scalability.
2. What is Clean Architecture?
At its core, Clean Architecture divides an application into concentric layers, with each layer having distinct responsibilities. These layers follow the Dependency Inversion Principle (DIP), meaning each layer only depends on the one directly inside it, with the business logic at the center. The typical layers are:
- Domain Layer (Core Business Logic): Contains essential business rules and core entities.
- Application Layer (Use Cases): Orchestrates workflows and application logic, handling the interaction between the domain and external systems.
- Infrastructure Layer (External Systems): Manages integrations with databases, file storage, or messaging systems.
- Presentation Layer (UI/Endpoints): Interfaces directly with the user or API consumers.
This structure keeps your core business rules isolated, which makes it easy to update any external systems without disrupting internal workflows.
The Dependency Inversion Principle (DIP) in Clean Architecture
The Dependency Inversion Principle is key to maintaining this layered structure. It states that:
- High-level modules (e.g., Application Layer) should not depend on low-level modules (e.g., Infrastructure Layer). Instead, both should depend on abstractions.
- Abstractions should not depend on details. Rather, the details (e.g., Infrastructure implementations) depend on abstractions (e.g., interfaces).
In Clean Architecture:
- Each layer depends only on the one directly inside it, allowing each layer to fulfill its responsibilities without depending on the details of others.
- High-level components (Application Layer) use interfaces or abstractions defined by inner layers (Domain Layer), while the Infrastructure Layer provides concrete implementations of these abstractions.
- This keeps dependencies flowing toward the center, with the Domain Layer remaining unaffected by external changes, and ensures the stability and resilience of core business logic even as external systems evolve.
By organizing responsibilities in this way, Clean Architecture provides a structure where core business rules and workflows are protected, isolated, and ready to adapt to changes without causing ripples throughout the system. This layered approach, along with DIP, keeps software systems scalable, testable, and maintainable for the long term.
However, traditional architectures often lead to tight coupling between layers, making them harder to scale or test independently. Clean Architecture’s layered approach solves this by isolating responsibilities.
Part of the Series
This article is part of a series covering the Contact Management Application, a sample project demonstrating Clean Architecture principles. Other articles include:
- Clean Architecture: Introduction to the Project Structure (You are here)
- Clean Architecture: Implementing AutoMapper for DTO Mapping and Audit Logging
- Clean Architecture: Validating Inputs with FluentValidation
- Clean Architecture: Dependency Injection Setup Across Layers
- Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC)
- Clean Architecture: Implementing Activity Logging with Custom Attributes
- Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions
- Clean Architecture: Using Dapper for Data Access and Repository Pattern
- Clean Architecture: Best Practices for Creating and Using DTOs in the API
- Clean Architecture: Error Handling and Exception Management in the API
- Clean Architecture: Dockerizing the .NET Core API and MS SQL Server
- Clean Architecture: Seeding Initial Data Using Docker Compose and SQL Scripts
3. Project Structure (Clean Architecture in the Contact Management Application)
The Contact Management Application follows Clean Architecture principles, creating a scalable, maintainable, and testable system by dividing it into four main layers:
Overview of the Project Structure
The project structure is organized into multiple layers, each handling a specific responsibility:
src
│
├── Contact.Api // API Layer (RESTful APIs)
├── Contact.Application // Application Layer (business use cases, services)
├── Contact.Domain // Domain Layer (business logic, entities)
├── Contact.Infrastructure// Infrastructure Layer (persistence, external systems)
└── Contact.Common // Shared utilities
3.1 Domain Layer (Core Business Logic)
- Purpose: The Domain Layer forms the core of the application, containing essential business logic, rules, and entities. It represents the fundamental behaviors and processes that define the application’s primary purpose.
- Contents: This layer includes business entities, value objects, and domain services that enforce critical rules, validations, and calculations.
- Independence: The Domain Layer is isolated from all other layers and external dependencies, remaining stable even when external systems, such as databases or APIs, are updated or replaced.
3.2 Application Layer (Use Cases)
- Purpose: Serving as an intermediary between the Domain Layer and external systems, the Application Layer defines workflows and coordinates the Domain Layer components to fulfill specific use cases.
- Contents: This layer includes application services, use cases, and data transfer objects (DTOs) that organize the flow of data and interactions within various scenarios.
- Role with Domain Layer: The Application Layer interacts with domain entities and services but does not alter core business logic. If any business rules or logic need modification, those changes occur in the Domain Layer, not here.
3.3 Infrastructure Layer (External Systems)
- Purpose: The Infrastructure Layer manages interactions with external systems, including databases, file storage, logging, and third-party services. It provides the implementations needed by the Domain and Application Layers for these external connections.
- Contents: This layer includes repositories, logging mechanisms, data access implementations (e.g., Entity Framework or Dapper), and third-party integrations.
- Interaction with Application Layer: The Infrastructure Layer fulfills interfaces and abstractions defined by the Domain and Application Layers. It does not contain business logic but instead provides necessary support for storing, retrieving, and processing data through external services.
3.4 Presentation Layer (UI/Endpoints)
- Purpose: The Presentation Layer serves as the system’s entry point, such as APIs in a backend service or user interfaces in a frontend. It gathers and interprets input from users or external systems and directs it to the Application Layer.
- Contents: This layer includes controllers, view models, UI components, and other elements that handle interactions with users or client systems.
- Interaction with Application Layer: The Presentation Layer communicates exclusively with the Application Layer, which in turn coordinates with the Domain and Infrastructure Layers to fulfill requests. This separation ensures clear boundaries and maintains the integrity of each layer.
4. Layer Breakdown
4.1. API Layer (Contact.Api)
The API Layer is the entry point for the system, handling incoming HTTP requests. It maps the requests to services in the Application Layer and returns appropriate responses. In this layer, DTOs (Data Transfer Objects) are used to abstract the underlying domain entities, ensuring separation between the client and the core domain.
Example: ContactPersonController
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ContactPersonController : ControllerBase
{
private readonly IContactPersonService _contactPersonService;
public ContactPersonController(IContactPersonService contactPersonService)
{
_contactPersonService = contactPersonService;
}
[HttpPost]
[ActivityLog("Creating new Contact")]
[AuthorizePermission("Contacts.Create")]
public async Task<IActionResult> Add(CreateContactPerson createContactPerson)
{
var createdContactPerson = await _contactPersonService.Add(createContactPerson);
return CreatedAtAction(nameof(GetById), new { id = createdContactPerson.Id }, createdContactPerson);
}
[HttpGet("{id}")]
[ActivityLog("Reading Contact By id")]
[AuthorizePermission("Contacts.Read")]
public async Task<IActionResult> GetById(Guid id)
{
var contactPerson = await _contactPersonService.FindByID(id);
if (contactPerson == null) return NotFound();
return Ok(contactPerson);
}
// Other actions...
}
This controller uses dependency injection to communicate with the Application Layer through the IContactPersonService
interface. It also maps incoming requests to DTOs, which are transformed to domain models inside the Application Layer.
Responsibilities of API Layer
- Handling HTTP requests and responses.
- Applying validation and formatting errors.
- Delegating business logic to the Application Layer.
- Utilizing custom attributes for authorization and activity logging (e.g.,
AuthorizePermission
andActivityLog
).
4.2. Application Layer (Contact.Application)
The Application Layer handles the core business logic and coordinates workflows. It acts as a middle layer between the API and Domain Layers. This layer uses AutoMapper to map DTOs to domain entities and vice versa, ensuring separation of concerns between the input/output data and the domain model.
Example: ContactPersonService
using AutoMapper;
using Contact.Application.Interfaces;
using Contact.Application.UseCases.ContactPerson;
using Contact.Domain.Entities;
using Contact.Domain.Interfaces;
namespace Contact.Application.Services;
public class ContactPersonService : GenericService<ContactPerson, ContactPersonResponse, CreateContactPerson, UpdateContactPerson>, IContactPersonService
{
public ContactPersonService(IGenericRepository<ContactPerson> repository, IMapper mapper, IUnitOfWork unitOfWork)
: base(repository, mapper, unitOfWork)
{
}
// Additional methods specific to ToDo can go here if needed
}
using AutoMapper;
using Contact.Application.Interfaces;
using Contact.Domain.Entities;
using Contact.Domain.Interfaces;
namespace Contact.Application.Services;
public class GenericService<TEntity, TResponse, TCreate, TUpdate>
: IGenericService<TEntity, TResponse, TCreate, TUpdate>
where TEntity : BaseEntity
where TResponse : class
where TCreate : class
where TUpdate : class
{
private readonly IGenericRepository<TEntity> _repository;
private readonly IMapper _mapper;
private readonly IUnitOfWork _unitOfWork;
public GenericService(IGenericRepository<TEntity> repository, IMapper mapper, IUnitOfWork unitOfWork)
{
_repository = repository;
_mapper = mapper;
_unitOfWork = unitOfWork;
}
public async Task<TResponse> Add(TCreate createDto)
{
using var transaction = _unitOfWork.BeginTransaction();
try
{
var entity = _mapper.Map<TEntity>(createDto);
var createdEntity = await _repository.Add(entity, transaction);
await _unitOfWork.CommitAsync();
return _mapper.Map<TResponse>(createdEntity); // Map to clean response DTO
}
catch
{
await _unitOfWork.RollbackAsync();
throw;
}
}
public async Task<TResponse> FindByID(Guid id)
{
var entity = await _repository.FindByID(id);
return _mapper.Map<TResponse>(entity); // Map to clean response DTO
}
public async Task<IEnumerable<TResponse>> FindAll()
{
var entities = await _repository.FindAll();
return _mapper.Map<IEnumerable<TResponse>>(entities); // Map to clean response DTOs
}
}
Responsibilities of the Application Layer
- Implements business logic and rules.
- Coordinates between controllers and repositories.
- Contains the DTOs (Data Transfer Objects) for create, update, and response.
- Utilizes AutoMapper to map DTOs to domain entities.
- Ensures validation through FluentValidation.
4.2.1 AutoMapper for DTO and Entity Mapping
AutoMapper is used to simplify the conversion of DTOs to domain entities and vice versa.
Example: AutoMapper Configuration
public class ContactPersonMappingProfile : Profile
{
public ContactPersonMappingProfile()
{
CreateMap<CreateContactPersonDto, ContactPerson>();
CreateMap<ContactPerson, ContactPersonResponse>();
}
}
4.3. Domain Layer (Contact.Domain)
The Domain Layer contains the core business logic and entities. It’s completely decoupled from external systems and ensures that the core logic remains isolated. All business rules reside in this layer.
Here’s an example of the ContactPerson entity:
Example: ContactPerson Entity
public class ContactPerson : BaseEntity
{
public string FirstName { get; set; }
public string LastName { get; set; }
public long Mobile { get; set; }
public string Email { get; set; }
}
This ContactPerson entity extends the BaseEntity class, which provides common properties like Id
, CreatedOn
, and UpdatedOn
. These entities represent the core business data and are used in business logic across the application.
4.4. Infrastructure Layer (Contact.Infrastructure)
The Infrastructure Layer implements external system integrations, like databases or logging services. This layer interacts with the Application Layer via interfaces, ensuring loose coupling.
Example: ContactPersonRepository
public class ContactPersonRepository : IContactPersonRepository
{
private readonly IDapperHelper _dapperHelper;
public ContactPersonRepository(IDapperHelper dapperHelper)
{
_dapperHelper = dapperHelper;
}
public async Task<ContactPerson> AddAsync(ContactPerson contact)
{
var sql = @"INSERT INTO Contacts (FirstName, LastName, Mobile, Email, CreatedOn)
VALUES (@FirstName, @LastName, @Mobile, @Email, @CreatedOn)";
await _dapperHelper.ExecuteAsync(sql, contact);
return contact;
}
}
This repository implementation uses Dapper to perform database operations, with the data being retrieved or persisted based on the domain entities.
5. Benefits of Clean Architecture
The Contact Management Application demonstrates several key benefits of Clean Architecture:
- Testability: The decoupling of the business logic from external systems makes unit testing easy.
- Scalability: Each layer is responsible for its own tasks, allowing you to modify one layer without affecting others.
- Maintainability: Clear separation of concerns means that changes to the UI or database don’t impact the core logic.
- Framework Independence: The core logic doesn’t depend on any specific framework, ensuring flexibility.
Conclusion
In this article, we’ve explored how Clean Architecture is applied to the Contact Management Application. We’ve seen how separating concerns into distinct layers ensures the application remains flexible, maintainable, and scalable. In the next articles, we’ll dive deeper into the Application Layer, including how to configure AutoMapper, manage validation with FluentValidation, and implement Activity Logging.
For a more detailed look at the project, you can access the full source code on GitHub:
GitHub Repository: Contact Management Application
In the next articles, we will dive deeper into specific areas like AutoMapper, FluentValidation, and Dapper, showcasing how they work together to support this architecture.
Get Involved!
- Explore the Code: Test the examples provided in the GitHub repository and share your insights in the comments.
- Follow Us: Stay updated on new developments by following the project on. GitHub.
- Subscribe: Sign up for our newsletter to receive tips on leveraging Azure and OpenAI for modern development.
- Join the Conversation: Have you faced challenges in improving or maintaining a project structure? Share your experiences and solutions in the comments below!
Discover more from Nitin Singh
Subscribe to get the latest posts sent to your email.
11 Responses
[…] Clean Architecture: Introduction to the Project Structure […]
[…] Clean Architecture: Introduction to the Project Structure […]
[…] Clean Architecture: Introduction to the Project Structure […]
[…] Clean Architecture: Introduction to the Project Structure […]
[…] Clean Architecture: Introduction to the Project Structure […]
[…] Clean Architecture: Introduction to the Project Structure […]
[…] Clean Architecture: Introduction to the Project Structure […]
[…] Clean Architecture: Introduction to the Project Structure […]
[…] Clean Architecture: Introduction to the Project Structure […]
[…] Clean Architecture: Introduction to the Project Structure […]
[…] Clean Architecture: Introduction to the Project Structure […]