Courses/C#/Domain-Driven Design

    Domain-Driven Design (Aggregates, Value Objects, Repositories)

    Model complex business domains using tactical DDD patterns — value objects that enforce rules, aggregates that protect consistency, and domain events that decouple side effects.

    What You'll Learn

    • • Value Objects — immutable, self-validating types (Money, Email)
    • • Aggregates — consistency boundaries with invariant protection
    • • Domain Events — decouple side effects from business logic
    • • Repository pattern in a DDD context

    🏦 Real-World Analogy

    A bank account is a perfect aggregate. The account (root) controls all access to transactions (children). You can't add a transaction without going through the account — it enforces rules like "balance can't go negative." Money is a value object: £10 is £10 regardless of which £10 note you have. Domain events are like receipts — they notify other systems (email, audit) that something happened.

    Value Objects

    Value objects have no identity — they're compared by value. They're immutable and self-validating, making invalid states impossible. Use C# record types for clean implementation.

    Value Objects — Money, Email, Address

    Create self-validating value objects with business logic built in.

    Try it Yourself »
    C#
    namespace MyApp.Domain.ValueObjects;
    
    // Value Object — no identity, compared by value
    // Immutable, self-validating, encapsulates business rules
    
    public record Money
    {
        public decimal Amount { get; }
        public string Currency { get; }
    
        public Money(decimal amount, string currency)
        {
            if (amount < 0) throw new DomainException("Amount cannot be negative");
            if (string.IsNullOrWhiteSpace(currency) || currency.Length != 3)
                throw new DomainException("Currency mu
    ...

    Aggregates & Aggregate Roots

    An aggregate is a cluster of objects treated as a single unit. The aggregate root is the only entry point — external code never modifies children directly. This protects invariants.

    Aggregate Root — BankAccount

    Build a bank account aggregate with deposits, withdrawals, and transfers.

    Try it Yourself »
    C#
    namespace MyApp.Domain.Aggregates;
    
    // Aggregate Root — consistency boundary
    // External code can only interact through the root
    public class BankAccount
    {
        public Guid Id { get; private set; }
        public string OwnerId { get; private set; } = "";
        public Money Balance { get; private set; } = new(0, "GBP");
        private readonly List<Transaction> _transactions = new();
        public IReadOnlyCollection<Transaction> Transactions => _transactions.AsReadOnly();
        private readonly List<IDomainEv
    ...

    Domain Events

    Domain events capture things that happened in the domain. They decouple side effects (emails, notifications, audit logs) from the core business logic. Dispatch them after the aggregate is saved.

    Domain Events & Dispatcher

    Publish domain events and handle them with decoupled event handlers.

    Try it Yourself »
    C#
    namespace MyApp.Domain.Events;
    
    // Domain Events — notify other parts of the system
    public interface IDomainEvent
    {
        DateTime OccurredAt { get; }
    }
    
    public record AccountOpenedEvent(Guid AccountId, string OwnerId) : IDomainEvent
    {
        public DateTime OccurredAt { get; } = DateTime.UtcNow;
    }
    
    public record FundsDepositedEvent(Guid AccountId, Money Amount) : IDomainEvent
    {
        public DateTime OccurredAt { get; } = DateTime.UtcNow;
    }
    
    public record FundsWithdrawnEvent(Guid AccountId, Money Amoun
    ...

    Pro Tip

    Keep aggregates small. A common mistake is creating huge aggregates with dozens of entities. If two concepts don't need to be consistent in the same transaction, they should be separate aggregates connected by ID references, not object references.

    Common Mistakes

    • • Using primitive types instead of value objects — string email allows invalid data
    • • Modifying aggregate children directly — bypasses invariant checks
    • • Dispatching events before saving — if the save fails, handlers ran for nothing

    Lesson Complete!

    You've mastered Domain-Driven Design patterns. Next, learn to build microservices with gRPC, messaging, and resilience.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service