Banking System with DDD
Expert Project — Aggregates, Value Objects, Domain Events, CQRS
🧠 Project Overview
Build a banking system using Domain-Driven Design. Practice aggregates that protect business invariants, value objects for type safety, and domain events for decoupled side effects.
🎯 Requirements
- • Open Current and Savings accounts with minimum deposit rules
- • Deposit, withdraw, and transfer between accounts
- • Transaction history with complete audit trail
- • Account freezing/closing with state validation
- • Domain events for notifications and audit logging
- • Full unit test coverage of domain logic
Step 1: Aggregate Root
BankAccount Aggregate
Aggregate root with factory method, invariant protection, and domain events.
// Domain/Aggregates/BankAccount.cs
public class BankAccount
{
public Guid Id { get; private set; }
public string OwnerId { get; private set; } = "";
public string OwnerName { get; private set; } = "";
public AccountType Type { get; private set; }
public Money Balance { get; private set; } = new(0, "GBP");
public AccountStatus Status { get; private set; }
private readonly List<Transaction> _transactions = new();
public IReadOnlyCollection<Transaction> Transactions
...Step 2: Transfer Use Case
Transfer Funds Handler
Orchestrate a transfer between two accounts with atomic persistence.
// Application/UseCases/TransferFundsHandler.cs
public class TransferFundsHandler
{
private readonly IAccountRepository _repo;
private readonly IUnitOfWork _uow;
private readonly IDomainEventDispatcher _events;
public TransferFundsHandler(IAccountRepository repo,
IUnitOfWork uow, IDomainEventDispatcher events)
{
_repo = repo; _uow = uow; _events = events;
}
public async Task HandleAsync(TransferCommand cmd)
{
var source = await _repo.GetB
...Step 3: Domain Tests
BankAccount Unit Tests
Test all domain rules: deposits, withdrawals, transfers, and freezing.
// Tests/BankAccountTests.cs
using Xunit;
public class BankAccountTests
{
[Fact]
public void Open_WithSufficientDeposit_CreatesAccount()
{
var account = BankAccount.Open("user-1", "Alice",
AccountType.Current, new Money(500, "GBP"));
Assert.Equal(500m, account.Balance.Amount);
Assert.Equal(AccountStatus.Active, account.Status);
Assert.Single(account.Transactions);
}
[Fact]
public void Open_SavingsBelowMinimum_Throws()
{
...Challenge Complete Checklist
- ☐ All account operations enforce business rules
- ☐ Transfers are atomic (both accounts updated or neither)
- ☐ Domain events trigger notifications
- ☐ Full unit test coverage of aggregate logic
- ☐ Published to GitHub with architecture diagram