The technology industry has been thriving for the second half of the last century. It's still a critical industry in most countries worldwide, from developed to developing ones. In many cases, it’s the main criteria to assess the development and dynamism of a nation. Yet, leaving aside the flashiness of this industry, let’s dig deeper into its reality. When newer technologies are being developed daily, it produces plenty of young and immature systems. The question is, how do software engineers thrive in this fast-changing environment and create systems that withstand time as much as possible?
So far, many architectures have been invented to reduce software costs for clients and increase the software lifespan itself. It spans from multi-layered to multi-tiered ones, followed by a Domain Oriented Architecture. All that said, I want to mention a concept that is not new, but it is considered an effective approach for building most web applications these days. It is Domain-Driven Design (a.k.a DDD). Its combination, improvement, and outstanding development overcome shortcomings of popular old models, such as N-Tiers, MVC, MVP, MVVM.
In this blog post, I will explore some basic concepts of the DDD pattern and provide a practical example of how to apply DDD effectively in a typical .NET Core project.
What is Domain-Driven Design, and why do we need it?
In 2003, Eric Evan published the first book entitled "Tackling Complexity in the Heart of Software", which brought the first concepts to DDD. The pattern's popularity has exponentially increased since then. Many software development teams, businesses, or organizations have applied this model and achieved great success in software development. Several tech giants like Microsoft is not out of this trend, and they mentioned Domain-Driven Design as follows:
“Domain-driven design (DDD) advocates modeling based on the reality of business as relevant to your use cases. In the context of building applications, DDD talks about problems as domains. It describes independent problem areas as Bounded Contexts (each Bounded Context correlates to a microservice) and emphasizes a common language to talk about these problems. It also suggests many technical concepts and patterns, like domain entities with rich models (no anemic-domain model), value objects, aggregates, and aggregate root (or root entity) rules to support the internal implementation….”
DDD is an approach to business-focused software development. Problems and challenges that happen during software development and maintenance mostly come from the constant growth of the business. Thus, DDD helps closely connect the software development and the business model's evolution.
DDD helps to solve the problem of building complex systems. This pattern requires architects, developers, and domain experts to understand precisely the requirements first. Then, they define behaviors, understand rules, apply principles and business logic into the set of clauses (Abstractions, Interfaces, and so on). Next, engineers will implement them in other layers (e.g., Application Layer, Infrastructure layer). Nowadays, DDD is set as a standard to develop different popular architectures, such as Onion Architecture, Clean Architecture, Hexagonal Architecture, etc.
Before diving into the details, we will explain some advantages and disadvantages of Domain-Driven Design to assist you in understanding whether this model fits your project well or not.
Advantages and disadvantages of Domain-Driven Design
Advantages of DDD
- Loose coupling: The parts of the system will interact with each other through the definitions and principles laid down in the Core layer (interfaces, abstract classes, base classes, etc.). Implementations will be completed in the remaining layers. Setting up the implementation will be through DI (IoC, AutoFac) libraries. Therefore, teams can develop independently at the same time.
- Flexibility: The loose links and high-level definitions allow the team to enhance and adapt to new functional requirements more flexibly without considerable impact on the overall system.
- Testability: As mentioned above, separating the implementation from the interfaces defined in the Core layer, testing with mock data in a separate environment is allowed.
- Maintenance: DDD clearly divides functions among layers/tiers. Specifically, the Domain implements business logic, Infrastructure is in charge of data persistence, and the Application handles API and integration logic. Following this approach ultimately gives you chances to write cleaner and more reliable codes. Plus, your team can easily find code, limit its duplication and reduce maintenance time.
Disadvantages of DDD
- Domain expertise: DDD requires extensive domain expertise. It means that your team needs to have at least one domain expert. They will help you define all of the processes, procedures, and terminology of that domain.
- Low interactions: The loose connection among different parts requires the team to communicate and exchange regularly. So before applying the DDD approach, the team needs to discuss its principles in detail first.
- Development costs: Domain experts and the team have to implement a great deal of isolation and encapsulation within the domain model. This often results in a more extended development and duration that can come at a relatively high cost. Therefore, it is not well-suited for short-term projects or projects without a high domain complexity.
Layers in DDD
The architecture of DDD projects usually includes three main parts: Domain, Infrastructure, Application. Depending on the size of each project, we can arrange these parts in a project or separate them into different layers.
- Domain: A place to define logic concepts, principles, patterns, and behaviors of data, including domain validation, calculations, and expressions for system operations.
- Entities: POCO classes, construction, and model validation.
- Aggregate: The rules, computation, logic of domains, and related objects when updating the domain. According to Martin Fowler, an aggregate is a cluster of domain objects that can be treated as a single unit.
- Value objects: The value of an object related to Domain entities. In principle, ValueObjects have no identity, and once been initialized, will not be modified. They can be understood as immutable classes.
- Interfaces: They help define business behaviors, etc. Other layers will be responsible for implementing these definitions.
- Repository Interfaces/ServiceBase: The Interfaces of generic repositories, domain repositories, and services. Other layers will inherit and develop them.
- ILogger/DTOs/Exceptions: Notifications and information are transferred to other services.
- Others
- Application
- Mobile application
- Web MVC/API application
- Desktop application
- IoT
- Others services
- Infrastructure
- Repositories: Repositories will be implemented here, including GenericRepository and <Entity> Repository.
- Data access: Contexts and the API connections link to databases.
- SQL: ADO.NET, EntityFramework, Dapper, and ORM, etc.
- In-Memory stores.
- Caching, NoSQL, and so on.
- Data seeding
- Others:
- Logging.
- Cryptography.
- Etc.
DDD implementation in a .NET Core application with code examples
Let me illustrate a basic example that helps you understand this architecture clearly and know how to implement it effectively.
As mentioned earlier, a system with DDD pattern implementation usually consists of three main layers: Application, Domain, and Infrastructure and can be organized in a solution like below.
API Layer
This is the Application Layer and works as a gateway where applications (AL or Presentation Layer) interact with the system. This layer processes collected information from interactions between the application and end-users or third-party services. It receives requests and validates the input before sending them to the Domain for processing. API also provides responses to the client.
The below screenshot explains the detailed structure of the API layer.
Code examples
BaseService.cs
public class BaseService
{
public BaseService(IUnitOfWork unitOfWork)
{
UnitOfWork = unitOfWork;
}
protected internal IUnitOfWork UnitOfWork { get; set; }
}
UserService.cs
public class UserService : BaseService
{
public UserService(IUnitOfWork unitOfWork) : base(unitOfWork)
{
}
public async Task<AddUserResponse> AddNewAsync(AddUserRequest model)
{
// You can you some mapping tools as such as AutoMapper
var user = new User(model.UserName
, model.FirstName
, modelmentId.Value);
.LastName
, model.Address
, model.BirthDate
, model.Depart
var repository = UnitOfWork.AsyncRepository<User>();
await repository.AddAsync(user);
await UnitOfWork.SaveChangesAsync();
var response = new AddUserResponse()
{
Id = user.Id,
UserName = user.UserName
};
return response;
}
public async Task<AddPayslipResponse> AddUserPayslipAsync(AddPayslipRequest model)
{
var repository = UnitOfWork.AsyncRepository<User>();
var user = await repository.GetAsync(_ => _.Id == model.UserId);
if (user != null)
{
var payslip = user.AddPayslip(model.Date.Value
, model.WorkingDays.Value
, model.Bonus
, model.IsPaid.Value);
await repository.UpdateAsync(user);
await UnitOfWork.SaveChangesAsync();
return new AddPayslipResponse()
{
UserId = user.Id,
TotalSalary = payslip.TotalSalary
};
}
throw new Exception("User not found.");
}
public async Task<List<UserInfoDTO>> SearchAsync(GetUserRequest request)
{
var repository = UnitOfWork.AsyncRepository<User>();
var users = await repository
.ListAsync(_ => _.UserName.Contains(request.Search));
var userDTOs = users.Select(_ => new UserInfoDTO()
{
Address = _.Address,
BirthDate = _.BirthDate,
DepartmentId = _.DepartmentId,
FirstName = _.FirstName,
Id = _.Id,
LastName = _.LastName,
UserName = _.UserName
})
.ToList();
return userDTOs;
}
}
Domain Layer
This is the center of the system. The Domain handles most of the business logic of the system. This layer is also responsible for defining the concepts, behaviors, and rules. The remaining layers implement them.
Code examples
a. The Entities
Now, let's create two classes named User and Department in Domain.
User.cs
public partial class User : BaseEntity<int>
{
public User()
{
PaySlips = new HashSet<Payslip>();
}
public string UserName { get; private set; }
public string FirstName { get; private set; }
public string LastName { get; private set; }
public string Address { get; private set; }
public DateTime? BirthDate { get; private set; }
public int DepartmentId { get; private set; }
public float CoefficientsSalary { get; private set; }
public virtual Department Department { get; private set; }
public virtual ICollection<Payslip> PaySlips { get; private set; }
}
User.Aggregate.cs
public partial class User: IAggregateRoot
{
public User(string userName
, string firstName
, string lastName
, string address
, DateTime? birthDate
, int departmentId)
{
UserName = userName;
this.Update(
firstName
, lastName
, address
, birthDate
, departmentId
);
}
public void Update(string firstName
, string lastName
, string address
, DateTime? birthDate
, int departmentId)
{
FirstName = firstName;
LastName = lastName;
Address = address;
BirthDate = birthDate;
DepartmentId = departmentId;
}
public void AddDepartment(int departmentId)
{
DepartmentId = departmentId;
}
public Payslip AddPayslip(DateTime date
, float workingDays
, decimal bonus
, bool isPaid
)
{
// Make sure there's only one payslip per month
var exist = PaySlips.Any(_ => _.Date.Month == date.Month && _.Date.Year == date.Year);
if (exist)
throw new Exception("Payslip for this month already exist.");
var payslip = new Payslip(this.Id, date, workingDays, bonus);
if (isPaid)
{
payslip.Pay(this.CoefficientsSalary);
}
PaySlips.Add(payslip);
var addEvent = new OnPayslipAddedDomainEvent()
{
Payslip = payslip
};
AddEvent(addEvent);
return payslip;
}
}
Payslip.cs
public class Payslip : BaseEntity<int>
{
public Payslip(int userId
, DateTime date
, float workingDays
, decimal bonus)
{
UserId = userId;
Date = date;
WorkingDays = workingDays;
Bonus = bonus;
}
public PayslipValueObject Value;
public DateTime Date { get; private set; }
public float WorkingDays { get; private set; }
public bool IsPaid { get; private set; }
public DateTime? PaymentDate { get; private set; }
public int UserId { get; private set; }
public decimal TotalSalary { get; private set; }
public decimal Bonus { get; private set; }
public virtual User User { get; private set; }
public void Pay(
float coefficientsSalary
)
{
if (IsPaid)
throw new Exception("This Payslip has been paid.");
IsPaid = true;
Value = new PayslipValueObject(WorkingDays, coefficientsSalary, Bonus);
TotalSalary = Value.TotalSalary;
PaymentDate = DateTime.Now;
}
}
Department.cs
public partial class Department : BaseEntity<short>
{
public string Name { get; internal set; }
public string Description { get; internal set; }
public virtual ICollection<User> Users { get; internal set; }
}
Department.Aggregate.cs
public partial class Department: IAggregateRoot
{
public Department()
{
Users = new HashSet<User>();
}
public Department(string name
, string description) : this()
{
this.Update(name, description);
}
public void Update(string name
, string description)
{
Name = name;
Description = description;
}
}
b. The Generic repositories
IAsyncRepository.cs
public interface IAsyncRepository<T> where T : BaseEntity
{
Task<T> AddAsync(T entity);
Task<T> UpdateAsync(T entity);
Task<bool> DeleteAsync(T entity);
Task<T> GetAsync(Expression<Func<T, bool>> expression);
Task<List<T>> ListAsync(Expression<Func<T, bool>> expression);
}
IUnitOfWork.cs
public interface IUnitOfWork
{
{
Task<int> SaveChangesAsync();
IAsyncRepository<T> Repository<T>() where T : BaseEntity;
}
}
Infrastructure Layer
The Infrastructure of the system includes database, logging, and exceptions. This layer is the place to interact with the database. Through behaviors and rules, POCO classes have been defined in the Domain. This layer undertakes all operations related to the information storage of the system.
Code examples
UnitOfWork.cs
public class UnitOfWork : IUnitOfWork
{
private readonly EFContext _dbContext;
public UnitOfWork(EFContext dbContext)
{
_dbContext = dbContext;
}
public IAsyncRepository<T> AsyncRepository<T>() where T : BaseEntity
{
return new RepositoryBase<T>(_dbContext);
}
public Task<int> SaveChangesAsync()
{
return _dbContext.SaveChangesAsync();
}
}
RepositoryBase.cs
public class RepositoryBase<T> : IAsyncRepository<T> where T : BaseEntity
{
private readonly DbSet<T> _dbSet;
public RepositoryBase(EFContext dbContext)
{
_dbSet = dbContext.Set<T>();
}
public async Task<T> AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
return entity;
}
public Task<bool> DeleteAsync(T entity)
{
_dbSet.Remove(entity);
return Task.FromResult(true);
}
public Task<T> GetAsync(Expression<Func<T, bool>> expression)
{
return _dbSet.FirstOrDefaultAsync(expression);
}
public Task<List<T>> ListAsync(Expression<Func<T, bool>> expression)
{
return _dbSet.Where(expression).ToListAsync();
}
public Task<T> UpdateAsync(T entity)
{
_dbSet.Update(entity);
return Task.FromResult(entity);
}
}
UserRepository.cs
public class UserRepository : RepositoryBase<User>
, IUserRepository
{
public UserRepository(EFContext dbContext) : base(dbContext)
{
}
}
DepartmentRepository.cs
public class DepartmentRepository : RepositoryBase<Department>
, IDepartmentRepository
{
public DepartmentRepository(EFContext dbContext) : base(dbContext)
{
}
}
Now let’s move on to configure DI in startup.cs class and finish some other settings (e.g., connectionStrings, Db Migrations, and Services injection, etc.) to run the application.
Conclusion
In summary, DDD is a great pattern for systems with complex business logic, systems that require future maintenance and enhancement. It is not necessary to fully apply this pattern to the project, we need to make tradeoffs sometimes depending on particular situations in each project. Onion Architecture, Clean Architecture, and Hexagon Architecture are also good examples.
You can also refer to the source code here:
I hope this article inspires you to architect your project with DDD.
Happy coding, and have a nice day!
References
- David Laribee, Best Practice - An Introduction To Domain-Driven Design, www.docs.microsoft.com, 2009.
- Design a DDD-oriented microservice, www.docs.microsoft.com, 2021.
- Implementing Domain-Driven Design, www.docs.abp.io, 2021.
- Domain-driven design, https://en.wikipedia.org.