DEV Community

mohamed Tayel
mohamed Tayel

Posted on

What is Clean Architecture: Part 11-Organizing the Code Using Features

As our applications grow and more functionality is added, organizing the codebase becomes increasingly important. Initially, it may be tempting to keep all commands and request handlers in a single folder, but this can quickly lead to clutter and inefficiency. In this article, I’ll demonstrate how to adopt a feature-based organization to improve code maintainability and separation of concerns.

What is Feature-Based Organization?

A feature-based organization is a vertical slice through the functionality of an application. Each feature represents a standalone unit of functionality. It aligns closely with the concept of a bounded context from Domain-Driven Design (DDD), where each feature encompasses everything required to implement that specific functionality, including commands, queries, view models, and even repositories.

Instead of scattering related code across multiple folders (like separating models, controllers, or services), a feature-based organization groups everything related to a particular feature into a single location. This structure enhances modularity and ensures that changes within a feature won’t affect other parts of the application.

Setting Up Feature Folders

In our example application, we’ll be working with Events, Categories, and Orders. To implement feature-based organization, we’ll introduce a Features folder, and under this, create a folder for each feature.

Features folder

Each feature folder will then contain subfolders for commands and queries, representing the business logic.

  • Commands: Responsible for actions like creating, updating, or deleting entities.
  • Queries: Focus on retrieving data or views of the data.

For example, within the Categories feature folder, we can have:

Categories feature folder

Example: Working with Categories

Let's walk through an example of how to organize the logic for handling Categories. In the Categories folder, we will add two queries:

  1. GetCategoriesListQuery: This retrieves a list of all categories.

GetCategoriesListQuery Folder

  1. GetCategoriesWithEventsQuery: This query fetches categories, along with associated events.

GetCategoriesWithEventsQuery folder

Step 1: Creating View Models

Each query will return a view model. Here’s an example for the GetCategoriesListQuery:

public class CategoriesListVm
{
    public int CategoryId { get; set; }
    public string Name { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

For the second query that includes events:

public class CategoryEventListVm
{
    public int CategoryId { get; set; }
    public string Name { get; set; }
    public List<CategoryEventDto> Events { get; set; }
}

public class CategoryEventDto
{
    public int EventId { get; set; }
    public string EventName { get; set; }
    public DateTime EventDate { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
Step 2: Writing the Queries

The queries encapsulate the logic to fetch the data:

public class GetCategoriesListQuery : IRequest<List<CategoriesListVm>> { }

public class GetCategoriesWithEventsQuery : IRequest<List<CategoryEventListVm>>
{
    public bool IncludeHistory { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
Step 3: Implementing Handlers

Each query will need a corresponding handler. For the GetCategoriesListQueryHandler, we fetch and map the list of categories:

public class GetCategoriesListQueryHandler : IRequestHandler<GetCategoriesListQuery, List<CategoriesListVm>>
{
    private readonly ICategoryRepository _categoryRepository;
    private readonly IMapper _mapper;

    public GetCategoriesListQueryHandler(ICategoryRepository categoryRepository, IMapper mapper)
    {
        _categoryRepository = categoryRepository;
        _mapper = mapper;
    }

    public async Task<List<CategoriesListVm>> Handle(GetCategoriesListQuery request, CancellationToken cancellationToken)
    {
        var categories = await _categoryRepository.GetAllCategoriesAsync();
        return _mapper.Map<List<CategoriesListVm>>(categories);
    }
}
Enter fullscreen mode Exit fullscreen mode

Similarly, for fetching categories with events:

public class GetCategoriesWithEventsQueryHandler : IRequestHandler<GetCategoriesWithEventsQuery, List<CategoryEventListVm>>
{
    private readonly ICategoryRepository _categoryRepository;
    private readonly IMapper _mapper;

    public GetCategoriesWithEventsQueryHandler(ICategoryRepository categoryRepository, IMapper mapper)
    {
        _categoryRepository = categoryRepository;
        _mapper = mapper;
    }

    public async Task<List<CategoryEventListVm>> Handle(GetCategoriesWithEventsQuery request, CancellationToken cancellationToken)
    {
        var categories = await _categoryRepository.GetCategoriesWithEventsAsync(request.IncludeHistory);
        return _mapper.Map<List<CategoryEventListVm>>(categories);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Using AutoMapper for Mapping

Finally, we need to ensure AutoMapper is aware of how to map between Category and our view models. This is done by configuring the mappings in our MappingProfile:

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<Category, CategoriesListVm>();
        CreateMap<Category, CategoryEventListVm>();
        CreateMap<Event, CategoryEventDto>();
    }
}
Enter fullscreen mode Exit fullscreen mode

With these steps completed, the functionality for working with Categories is now neatly encapsulated within the Categories folder.

Benefits of Feature-Based Organization

By organizing our application based on features, we gain several benefits:

  • Modularity: Each feature is self-contained, making it easier to manage and maintain.
  • Scalability: As the application grows, we can easily add new features without cluttering the codebase.
  • Encapsulation: Changes to one feature are less likely to impact others, reducing the risk of bugs and regressions.

Conclusion

In this article, we explored how to organize our application by features. By following this approach, we can keep our codebase clean, modular, and scalable, making it easier to maintain as new functionality is added. Whether you’re working with events, categories, orders, or other features, this method will help you stay organized and focused on each vertical slice of functionality.

Top comments (0)