-
-
Notifications
You must be signed in to change notification settings - Fork 694
Domain Driven Design Patterns
The following patterns are known to describe business solutions.
- Value Object
- Entity
- Aggregate Root
- Repository
- Use Case
- Bounded Context
- Entity Factory
- Domain Service
- Application Service
Encapsulate the tiny domain business rules. Structures that are unique by their properties and the whole object is immutable, once it is created its state can not change.
public readonly struct Name
{
private readonly string _text;
public Name(string text)
{
if (string.IsNullOrWhiteSpace(text))
throw new NameShouldNotBeEmptyException("The 'Name' field is required");
_text = text;
}
public override string ToString()
{
return _text;
}
}
Rules of thumb:
- The developer should make the Value Object serializable and deserializable.
- A Value Object can not reference an Entity or another mutable object.
Highly abstract and mutable objects unique identified by its IDs.
public abstract class Credit : ICredit
{
/// <summary>
/// Gets or sets Id.
/// </summary>
public CreditId Id { get; protected set; }
/// <summary>
/// Gets or sets Amount.
/// </summary>
public PositiveMoney Amount { get; protected set; }
/// <summary>
/// Gets Description.
/// </summary>
public string Description
{
get { return "Credit"; }
}
/// <summary>
/// Gets or sets Transaction Date.
/// </summary>
public DateTime TransactionDate { get; protected set; }
/// <summary>
/// Calculate the sum of positive amounts.
/// </summary>
/// <param name="amount">Positive amount.</param>
/// <returns>The positive sum.</returns>
public PositiveMoney Sum(PositiveMoney amount)
{
return this.Amount.Add(amount);
}
}
Rules of Thumb:
- Entities are mutable.
- Entities are highly abstract.
- Entities do not need to be serializable.
- The entity state should be encapsulated to external access.
Similar to Entities with the addition that Aggregate Root are responsible to keep the graph of objects consistent.
- Owns entities object graph.
- Ensure the child entities state are always consistent.
- Define the transaction scope.
public abstract class Account : IAccount
{
/// <summary>
/// Initializes a new instance of the <see cref="Account"/> class.
/// </summary>
protected Account()
{
this.Credits = new CreditsCollection();
this.Debits = new DebitsCollection();
}
/// <inheritdoc/>
public AccountId Id { get; protected set; }
/// <summary>
/// Gets or sets Credits List.
/// </summary>
public CreditsCollection Credits { get; protected set; }
/// <summary>
/// Gets or sets Debits List.
/// </summary>
public DebitsCollection Debits { get; protected set; }
/// <inheritdoc/>
public ICredit Deposit(IAccountFactory entityFactory, PositiveMoney amountToDeposit)
{
var credit = entityFactory.NewCredit(this, amountToDeposit, DateTime.UtcNow);
this.Credits.Add(credit);
return credit;
}
/// <inheritdoc/>
public IDebit Withdraw(IAccountFactory entityFactory, PositiveMoney amountToWithdraw)
{
if (this.GetCurrentBalance().LessThan(amountToWithdraw))
{
throw new MoneyShouldBePositiveException("Account has not enough funds.");
}
var debit = entityFactory.NewDebit(this, amountToWithdraw, DateTime.UtcNow);
this.Debits.Add(debit);
return debit;
}
/// <inheritdoc/>
public bool IsClosingAllowed()
{
return this.GetCurrentBalance().IsZero();
}
/// <inheritdoc/>
public Money GetCurrentBalance()
{
var totalCredits = this.Credits
.GetTotal();
var totalDebits = this.Debits
.GetTotal();
var totalAmount = totalCredits
.Subtract(totalDebits);
return totalAmount;
}
}
Rules of thumb:
- Protect business invariants inside Aggregate boundaries.
- Design small Aggregates.
- Reference other Aggregates by identity only.
- Update other Aggregates using eventual consistency.
Provides persistence capabilities to Aggregate Roots.
public sealed class CustomerRepository : ICustomerRepository
{
private readonly MangaContext _context;
public CustomerRepository(MangaContext context)
{
_context = context;
}
public async Task Add(ICustomer customer)
{
_context.Customers.Add((InMemoryDataAccess.Customer) customer);
await Task.CompletedTask;
}
public async Task<ICustomer> Get(Guid id)
{
Customer customer = _context.Customers
.Where(e => e.Id == id)
.SingleOrDefault();
return await Task.FromResult<Customer>(customer);
}
public async Task Update(ICustomer customer)
{
Customer customerOld = _context.Customers
.Where(e => e.Id == customer.Id)
.SingleOrDefault();
customerOld = (Customer) customer;
await Task.CompletedTask;
}
}
Rules of thumb:
- The repository is designed around the aggregate root.
- A repository for every entity is a code smell.
It is the application entry point for an user interaction. It accepts an input message, executes the algorithm then it should give the output message to the Output port.
public sealed class Withdraw : IUseCase
{
private readonly AccountService _accountService;
private readonly IOutputPort _outputPort;
private readonly IAccountRepository _accountRepository;
private readonly IUnitOfWork _unitOfWork;
/// <summary>
/// Initializes a new instance of the <see cref="Withdraw"/> class.
/// </summary>
/// <param name="accountService">Account Service.</param>
/// <param name="outputPort">Output Port.</param>
/// <param name="accountRepository">Account Repository.</param>
/// <param name="unitOfWork">Unit Of Work.</param>
public Withdraw(
AccountService accountService,
IOutputPort outputPort,
IAccountRepository accountRepository,
IUnitOfWork unitOfWork)
{
this._accountService = accountService;
this._outputPort = outputPort;
this._accountRepository = accountRepository;
this._unitOfWork = unitOfWork;
}
/// <summary>
/// Executes the Use Case.
/// </summary>
/// <param name="input">Input Message.</param>
/// <returns>Task.</returns>
public async Task Execute(WithdrawInput input)
{
try
{
var account = await this._accountRepository.Get(input.AccountId);
var debit = await this._accountService.Withdraw(account, input.Amount);
await this._unitOfWork.Save();
this.BuildOutput(debit, account);
}
catch (AccountNotFoundException notFoundEx)
{
this._outputPort.NotFound(notFoundEx.Message);
return;
}
catch (MoneyShouldBePositiveException outOfBalanceEx)
{
this._outputPort.OutOfBalance(outOfBalanceEx.Message);
return;
}
}
private void BuildOutput(IDebit debit, IAccount account)
{
var output = new WithdrawOutput(
debit,
account.GetCurrentBalance());
this._outputPort.Standard(output);
}
}
Rules of thumb:
- The use case implementation are close to a human readable language.
- Ideally a class has a single use case.
- Invokes transaction operations (eg. Unit Of Work).
It is a logical boundary, similar to a module in a system. In the Manga project the single Domain project is the single bounded context we designed.
Creates new instances of Entities and Aggregate Roots. Should be implemented by the Infrastructure layer.
public sealed class EntityFactory : IUserFactory, ICustomerFactory, IAccountFactory
{
public IAccount NewAccount(CustomerId customerId) => new Account(customerId);
public ICredit NewCredit(
IAccount account,
PositiveMoney amountToDeposit,
DateTime transactionDate) => new Credit(account, amountToDeposit, transactionDate);
public ICustomer NewCustomer(
SSN ssn,
Name name) => new Customer(ssn, name);
public IDebit NewDebit(
IAccount account,
PositiveMoney amountToWithdraw,
DateTime transactionDate) => new Debit(account, amountToWithdraw, transactionDate);
public IUser NewUser(CustomerId customerId, ExternalUserId externalUserId) => new User(customerId, externalUserId);
}
Useful functions cross Entities.
public class AccountService
{
private readonly IAccountFactory _accountFactory;
private readonly IAccountRepository _accountRepository;
/// <summary>
/// Initializes a new instance of the <see cref="AccountService"/> class.
/// </summary>
/// <param name="accountFactory">Account Factory.</param>
/// <param name="accountRepository">Account Repository.</param>
public AccountService(
IAccountFactory accountFactory,
IAccountRepository accountRepository)
{
this._accountFactory = accountFactory;
this._accountRepository = accountRepository;
}
/// <summary>
/// Open Checking Account.
/// </summary>
/// <param name="customerId">Customer Id.</param>
/// <param name="amount">Amount.</param>
/// <returns>IAccount created.</returns>
public async Task<IAccount> OpenCheckingAccount(CustomerId customerId, PositiveMoney amount)
{
var account = this._accountFactory.NewAccount(customerId);
var credit = account.Deposit(this._accountFactory, amount);
await this._accountRepository.Add(account, credit);
return account;
}
/// <summary>
/// Withdrawls from Account.
/// </summary>
/// <param name="account">Account.</param>
/// <param name="amount">Amount.</param>
/// <returns>Debit Transaction.</returns>
public async Task<IDebit> Withdraw(IAccount account, PositiveMoney amount)
{
var debit = account.Withdraw(this._accountFactory, amount);
await this._accountRepository.Update(account, debit);
return debit;
}
/// <summary>
/// Deposits into Account.
/// </summary>
/// <param name="account">Account.</param>
/// <param name="amount">Amount.</param>
/// <returns>Credit Transaction.</returns>
public async Task<ICredit> Deposit(IAccount account, PositiveMoney amount)
{
var credit = account.Deposit(this._accountFactory, amount);
await this._accountRepository.Update(account, credit);
return credit;
}
}
Rules of thumb:
- It should not call transaction methods, should not call a Unit Of Work method.
See Use Case.
- Value Object
- Entity
- Aggregate Root
- Repository
- Use Case
- Bounded Context
- Entity Factory
- Domain Service
- Application Service
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
- Swagger and API Versioning
- Microsoft Extensions
- Feature Flags
- Logging
- Data Annotations
- Authentication
- Authorization