MCPInstall

ddd

Domain-Driven Design tactical patterns for .NET applications. Covers aggregates, aggregate roots, value objects, domain events, domain services, strongly-typed IDs, and repository patterns for aggregate persistence. Load this skill when implementing DDD, working with aggregates, value objects, domain events, bounded contexts, or when the architecture-advisor recommends DDD + Clean Architecture. Pair with the clean-architecture skill.

Installs
2.9K
Last 24h
+55
Trending rank
#3148
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill ddd
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads ddd automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

Domain-Driven Design (DDD)

Core Principles

  1. Aggregates define consistency boundaries — An aggregate is a cluster of entities and value objects treated as a single unit for data changes. All invariants within an aggregate are enforced in a single transaction. Cross-aggregate consistency is eventual.
  2. Value objects over primitives — Replace primitive obsession with value objects. Money, EmailAddress, OrderNumber are not strings — they carry validation, equality, and behavior. Use C# records for immutable value objects.
  3. Domain events decouple side effects — When something meaningful happens in the domain (OrderPlaced, PaymentReceived), raise a domain event. Side effects (send email, update read model, notify another aggregate) subscribe to these events. The aggregate stays focused on its own rules.
  4. Aggregate root is the sole entry point — External code accesses an aggregate only through its root entity. Child entities are never loaded or modified independently. The root enforces all invariants for the entire aggregate.
  5. Repositories persist aggregates, not entities — One repository per aggregate root. The repository loads and saves the entire aggregate as a unit. No repository for child entities. The Infrastructure implementation uses DbContext internally — this is a DDD tactical pattern for aggregate boundaries, not a generic CRUD wrapper.

Patterns

Aggregate Root

The aggregate root owns all access to its children and enforces invariants:

// Domain/Orders/Order.cs
public sealed class Order : AggregateRoot
{
    private readonly List<OrderLine> _lines = [];

    private Order() { } // EF Core

    public OrderNumber Number { get; private set; } = null!;
    public CustomerId CustomerId { get; private set; }
    public Money Total { get; private set; } = Money.Zero("USD");
    public OrderStatus Status { get; private set; }
    public DateTimeOffset PlacedAt { get; private set; }
    public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();

    public static Order Place(CustomerId customerId, OrderNumber number, DateTimeOffset now)
    {
        var order = new Order
        {
            Id = Guid.CreateVersion7(),
            CustomerId = customerId,
            Number = number,
            Status = OrderStatus.Placed,
            PlacedAt = now
        };

        order.RaiseDomainEvent(new OrderPlaced(order.Id, customerId, now));
        return order;
    }

    public Result AddLine(ProductId productId, int quantity, Money unitPrice)
    {
        if (Status is not OrderStatus.Placed)
            return Result.Failure("Cannot modify a confirmed or cancelled order");

        if (quantity <= 0)
            return Result.Failure("Quantity must be positive");

        var existing = _lines.FirstOrDefault(l => l.ProductId == productId);
        if (existing is not null)
        {
            existing.IncreaseQuantity(quantity);
        }
        else
        {
            _lines.Add(new OrderLine(productId, quantity, unitPrice));
        }

        RecalculateTotal();
        return Result.Success();
    }

    public Result Confirm()
    {
        if (Status is not OrderStatus.Placed)
            return Result.Failure("Only placed orders can be confirmed");

        if (_lines.Count == 0)
            return Result.Failure("Cannot confirm an order with no lines");

        Status = OrderStatus.Confirmed;
        RaiseDomainEvent(new OrderConfirmed(Id));
        return Result.Success();
    }

    private void RecalculateTotal()
    {
        Total = _lines.Aggregate(Money.Zero(Total.Currency), (sum, line) => sum + line.Subtotal);
    }
}

Value Objects as Records

Use C# records for immutable value objects with structural equality:

// Domain/Common/Money.cs
public sealed record Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(amount);
        ArgumentException.ThrowIfNullOrWhiteSpace(currency);

        Amount = amount;
        Currency = currency.ToUpperInvariant();
    }

    public static Money Zero(string currency) => new(0, currency);

    public static Money operator +(Money left, Money right)
    {
        if (left.Currency != right.Currency)
            throw new InvalidOperationException($"Cannot add {left.Currency} and {right.Currency}");
        return new Money(left.Amount + right.Amount, left.Currency);
    }
}

// Other value objects (EmailAddress, OrderNumber, etc.) follow the same pattern:
// sealed record, constructor validation, no public setters

Strongly-Typed IDs with EF Core Converters

Prevent mixing up GUIDs from different entities:

// Domain/Common/StronglyTypedId.cs
public readonly record struct CustomerId(Guid Value)
{
    public static CustomerId New() => new(Guid.CreateVersion7());
    public override string ToString() => Value.ToString();
}

public readonly record struct ProductId(Guid Value)
{
    public static ProductId New() => new(Guid.CreateVersion7());
}

public readonly record struct OrderNumber(string Value)
{
    public override string ToString() => Value;
}

// Infrastructure/Persistence/Configurations/OrderConfiguration.cs
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.HasKey(o => o.Id);

        builder.Property(o => o.CustomerId)
            .HasConversion(id => id.Value, value => new CustomerId(value));

        builder.Property(o => o.Number)
            .HasConversion(n => n.Value, value => new OrderNumber(value))
            .HasMaxLength(50);

        builder.ComplexProperty(o => o.Total, money =>
        {
            money.Property(m => m.Amount).HasColumnName("Total").HasPrecision(18, 2);
            money.Property(m => m.Currency).HasColumnName("Currency").HasMaxLength(3);
        });

        builder.HasMany(o => o.Lines).WithOne().HasForeignKey("OrderId");
        builder.Navigation(o => o.Lines).AutoInclude();
    }
}

Domain Event Dispatching

Raise events in the aggregate, dispatch in SaveChangesAsync:

// Domain/Common/AggregateRoot.cs
public abstract class AggregateRoot : Entity
{
    private readonly List<IDomainEvent> _domainEvents = [];

    public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

    protected void RaiseDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent);

    public void ClearDomainEvents() => _domainEvents.Clear();
}

// INotification comes from the MIT-licensed Mediator package (not MediatR —
// see the packages rule). Use a plain marker interface if you don't use a mediator.
public interface IDomainEvent : INotification
{
    DateTimeOffset OccurredAt { get; }
}

// Domain/Orders/Events/OrderPlaced.cs
public sealed record OrderPlaced(Guid OrderId, CustomerId CustomerId, DateTimeOffset PlacedAt) : IDomainEvent
{
    public DateTimeOffset OccurredAt => PlacedAt;
}

// Infrastructure/Persistence/AppDbContext.cs — publisher injected via primary constructor
public class AppDbContext(DbContextOptions<AppDbContext> options, IPublisher publisher)
    : DbContext(options)
{
    public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        var aggregates = ChangeTracker.Entries<AggregateRoot>()
            .Where(e => e.Entity.DomainEvents.Count > 0)
            .Select(e => e.Entity)
            .ToList();

        var events = aggregates.SelectMany(a => a.DomainEvents).ToList();

        var result = await base.SaveChangesAsync(ct);

        foreach (var @event in events)
            await publisher.Publish(@event, ct);

        foreach (var aggregate in aggregates)
            aggregate.ClearDomainEvents();

        return result;
    }
}

Domain Services

For logic that does not belong to a single aggregate:

// Domain/Orders/Services/PricingService.cs
// Coordinates logic across aggregates — takes domain interfaces, returns value objects
public sealed class PricingService(IDiscountPolicy discountPolicy)
{
    public Money CalculatePrice(ProductId productId, int quantity, Money unitPrice, CustomerId customerId)
    {
        var subtotal = new Money(unitPrice.Amount * quantity, unitPrice.Currency);
        var discount = discountPolicy.GetDiscount(customerId, productId, quantity);
        return new Money(subtotal.Amount * (1 - discount), subtotal.Currency);
    }
}

Anti-patterns

Oversized Aggregates

// BAD — Customer aggregate owns everything the customer touches
public class Customer : AggregateRoot
{
    public List<Order> Orders { get; } = [];        // should be separate aggregate
    public List<Payment> Payments { get; } = [];     // should be separate aggregate
    public List<Address> Addresses { get; } = [];    // might be OK as child
    public ShoppingCart Cart { get; set; }            // should be separate aggregate
}

// GOOD — small, focused aggregates linked by ID
public class Customer : AggregateRoot
{
    public CustomerName Name { get; private set; }
    public EmailAddress Email { get; private set; }
    // Orders, Payments, Cart are separate aggregates referencing CustomerId
}

Domain Events for Intra-Aggregate Logic

// BAD — using events for logic within the same aggregate
order.RaiseDomainEvent(new OrderLineAdded(line));
// Then a handler recalculates the total... but you're in the same aggregate!

// GOOD — just call the method directly within the aggregate
_lines.Add(line);
RecalculateTotal();  // private method, no event needed

Value Objects with Identity

// BAD — value object with an Id (it's an entity then!)
public record Address
{
    public Guid Id { get; init; }  // value objects don't have identity
    public string Street { get; init; }
}

// GOOD — value objects are defined by their attributes, not an Id
public record Address(string Street, string City, string PostalCode, string Country);

Anemic Aggregates

// BAD — aggregate is just a data bag, service does all the work
public class Order : AggregateRoot
{
    public OrderStatus Status { get; set; }  // public setter!
    public List<OrderLine> Lines { get; set; } = [];
}

// Service directly manipulates order state
order.Status = OrderStatus.Confirmed;  // no invariant check!
order.Lines.Add(newLine);              // no validation!

// GOOD — aggregate encapsulates rules (see Aggregate Root pattern above)
order.Confirm();  // validates status, raises event
order.AddLine(productId, quantity, unitPrice);  // validates, recalculates

Decision Guide

ScenarioRecommendation
When to use DDDComplex domain with business rules that go beyond CRUD
When to use value objectsAny concept with validation rules or equality based on attributes, not identity
Aggregate sizeKeep small — typically 1 root entity + 0-3 child entities. Load the whole aggregate every time
Domain events vs integration eventsDomain events: within bounded context, same transaction. Integration events: cross-context, via message bus
Strongly-typed IDsAlways for aggregate root IDs that cross boundaries. Optional for child entity IDs
When NOT to use DDDSimple CRUD, settings, audit logs, read models — use plain entities
Repository vs DbContextRepository per aggregate root for complex aggregates; IAppDbContext for simpler queries
Domain servicesOnly when logic requires multiple aggregates or external data the aggregate should not know about

FAQ

What does the ddd skill do?

Domain-Driven Design tactical patterns for .NET applications. Covers aggregates, aggregate roots, value objects, domain events, domain services, strongly-typed IDs, and repository patterns for aggregate persistence. Load this skill when implementing DDD, working with aggregates, value objects, domain events, bounded contexts, or when the architecture-advisor recommends DDD + Clean Architecture. Pair with the clean-architecture skill.

How do I install ddd?

Run npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill ddd in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/ddd/ (personal) or .claude/skills/ddd/ (project).

Which AI agents can use ddd?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is ddd safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains ddd?

It is published by codewithmukesh in codewithmukesh/dotnet-claude-kit. It has 2,936 recorded installs.

More from codewithmukesh/dotnet-claude-kit

View all →

clean-architecture

codewithmukesh/dotnet-claude-kit

Clean Architecture for .NET applications. Covers the 4-project layout (Domain, Application, Infrastructure, Api), dependency inversion, use case handlers, domain entities with behavior, and infrastructure as a plugin. Load this skill when building a project with Clean Architecture, discussing layered architecture, dependency inversion, use cases, or when the architecture-advisor recommends Clean Architecture.

modern-csharp

codewithmukesh/dotnet-claude-kit

Modern C language features for .NET 10 and C 14. Covers primary constructors, collection expressions, the field keyword, extension members, records, pattern matching, spans, and raw string literals. Load this skill when writing any new C code, reviewing existing code for modernization, using "modern C", "C 14", "primary constructor", "collection expression", "records", "pattern matching", "span", "field keyword", or "extension members". Always loaded as the baseline for all agents.

Backend & APIs2.9K installs

vertical-slice

codewithmukesh/dotnet-claude-kit

Vertical Slice Architecture (VSA) for .NET applications — one of several supported architectures in dotnet-claude-kit. Covers feature folders, endpoint grouping, and handler patterns for Mediator, Wolverine, and raw handler classes. Load this skill when the architecture-advisor recommends VSA, when working in an existing VSA codebase, when adding features to a feature-folder project, or when discussing vertical slice patterns, feature folders, or handler patterns.

Backend & APIs2.9K installs

security-scan

codewithmukesh/dotnet-claude-kit

Deep security scanning for .NET applications across 6 layers: vulnerable packages, secrets detection, OWASP code patterns, auth configuration, CORS policy, and data protection. Produces severity-rated findings with specific remediation steps. Load this skill when: "security scan", "security audit", "check for vulnerabilities", "find secrets", "OWASP", "auth review", "CORS check", "security review", "penetration test prep", "CVE check", "vulnerability scan", "hardcoded password", "data protection", "security posture".

Security2.9K installs

serilog

codewithmukesh/dotnet-claude-kit

Structured logging with Serilog for .NET 10 applications. Covers two-stage bootstrap, appsettings configuration, enrichers, sinks, request logging, destructuring, and Serilog.Expressions. Load this skill when setting up Serilog, configuring log sinks, enrichers, or structured logging, or when the user mentions "Serilog", "structured logging", "log enrichment", "Seq", "LogContext", "UseSerilog", "WriteTo", "message template", "Serilog.Expressions", "request logging", "log sink", "rolling file", or "audit log".

DevOps & Cloud2.9K installs

docker

codewithmukesh/dotnet-claude-kit

Docker containerization for .NET 10 applications. Covers multi-stage builds, .NET container images, non-root user configuration, health checks, and .dockerignore. Load this skill when containerizing an application with a Dockerfile, optimizing image size, setting up Docker Compose for local development, or when the user mentions "Docker", "Dockerfile", "container", "docker-compose", "image", "multi-stage", "non-root", ".dockerignore", or "container health check". For Dockerfile-less SDK publishing (dotnet publish /t:PublishContainer), load the container-publish skill instead.

DevOps & Cloud2.9K installs

Popular code quality & workflow skills

View category →

grill-me

mattpocock/skills

A relentless interview to sharpen a plan or design.

Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.

handoff

mattpocock/skills

Compact the current conversation into a handoff document for another agent to pick up.

domain-modeling

mattpocock/skills

Build and sharpen a project's domain model. Use when discussing codebase terminology, writing or editing a GLOSSARY.md, or recording or editing an ADR.

codebase-design

mattpocock/skills

Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.

diagnosing-bugs

mattpocock/skills

Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow.