원클릭으로
ddd-patterns
Use when modeling domain logic with aggregates, value objects, domain events, or bounded contexts.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when modeling domain logic with aggregates, value objects, domain events, or bounded contexts.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use when designing gRPC services, proto files, or adding gRPC-Web or JSON transcoding.
Use when writing async code, propagating CancellationTokens, or fixing async/await pitfalls.
Use when applying or enforcing C# coding style — namespaces, sealed classes, var usage, XML docs.
Use when applying modern C# idioms — records, pattern matching, primary constructors, collection expressions.
Use when registering services, choosing lifetimes, or implementing DI patterns like decorator or keyed services.
Use when selecting or implementing design patterns in C# — factory, builder, strategy, decorator, or mediator.
| name | ddd-patterns |
| description | Use when modeling domain logic with aggregates, value objects, domain events, or bounded contexts. |
| metadata | {"category":"architecture","agent":"dotnet-architect"} |
| when_to_use | When implementing DDD tactical patterns like aggregates, value objects, or domain events |
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
public readonly record struct CustomerId(Guid Value)
{
public static CustomerId New() => new(Guid.NewGuid());
}
// Prevents accidental: GetOrder(customerId) when OrderId expected
public sealed record Money(decimal Amount, string Currency)
{
public static Money Zero(string currency) => new(0, currency);
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new DomainException("Cannot add different currencies");
return this with { Amount = Amount + other.Amount };
}
public Money Multiply(int factor) =>
this with { Amount = Amount * factor };
}
public sealed record Address(
string Street, string City, string State, string ZipCode, string Country)
{
// Value objects validate on construction
public Address
{
if (string.IsNullOrWhiteSpace(Street))
throw new DomainException("Street is required");
if (string.IsNullOrWhiteSpace(City))
throw new DomainException("City is required");
}
}
public abstract class AggregateRoot<TId> where TId : struct
{
public TId Id { get; protected set; }
private readonly List<IDomainEvent> _domainEvents = [];
public IReadOnlyList<IDomainEvent> DomainEvents =>
_domainEvents.AsReadOnly();
protected void RaiseDomainEvent(IDomainEvent domainEvent) =>
_domainEvents.Add(domainEvent);
public void ClearDomainEvents() => _domainEvents.Clear();
}
public interface IAggregateRoot { }
public interface IDomainEvent : INotification
{
DateTimeOffset OccurredAt { get; }
}
public sealed class Order : AggregateRoot<OrderId>, IAggregateRoot
{
public CustomerId CustomerId { get; private set; }
public Money Total { get; private set; } = Money.Zero("USD");
public OrderStatus Status { get; private set; } = OrderStatus.Draft;
private readonly List<OrderLine> _lines = [];
public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
private Order() { } // EF Core
// Factory method — the only way to create an Order
public static Order Create(CustomerId customerId)
{
var order = new Order
{
Id = OrderId.New(),
CustomerId = customerId
};
order.RaiseDomainEvent(new OrderCreatedEvent(order.Id));
return order;
}
public void AddLine(ProductId productId, int quantity, Money unitPrice)
{
Guard.Against.NegativeOrZero(quantity);
if (Status != OrderStatus.Draft)
throw new DomainException("Can only add lines to draft orders");
var line = new OrderLine(productId, quantity, unitPrice);
_lines.Add(line);
RecalculateTotal();
}
public void RemoveLine(ProductId productId)
{
var line = _lines.FirstOrDefault(l => l.ProductId == productId)
?? throw new DomainException("Line not found");
_lines.Remove(line);
RecalculateTotal();
}
public void Submit()
{
if (Status != OrderStatus.Draft)
throw new DomainException("Only draft orders can be submitted");
if (_lines.Count == 0)
throw new DomainException("Cannot submit empty order");
Status = OrderStatus.Submitted;
RaiseDomainEvent(new OrderSubmittedEvent(Id, Total));
}
public void Cancel(string reason)
{
if (Status == OrderStatus.Shipped)
throw new DomainException("Cannot cancel shipped orders");
Status = OrderStatus.Cancelled;
RaiseDomainEvent(new OrderCancelledEvent(Id, reason));
}
private void RecalculateTotal()
{
Total = _lines.Aggregate(
Money.Zero("USD"),
(sum, line) => sum.Add(line.LineTotal));
}
}
public sealed class OrderLine
{
public Guid Id { get; private set; } = Guid.NewGuid();
public ProductId ProductId { get; private set; }
public int Quantity { get; private set; }
public Money UnitPrice { get; private set; }
public Money LineTotal => UnitPrice.Multiply(Quantity);
private OrderLine() { } // EF Core
internal OrderLine(ProductId productId, int quantity, Money unitPrice)
{
ProductId = productId;
Quantity = quantity;
UnitPrice = unitPrice;
}
}
public abstract record DomainEvent : IDomainEvent
{
public DateTimeOffset OccurredAt { get; } = DateTimeOffset.UtcNow;
}
public sealed record OrderCreatedEvent(OrderId OrderId) : DomainEvent;
public sealed record OrderSubmittedEvent(
OrderId OrderId, Money Total) : DomainEvent;
public sealed record OrderCancelledEvent(
OrderId OrderId, string Reason) : DomainEvent;
public sealed class DomainException : Exception
{
public DomainException(string message) : base(message) { }
public DomainException(string message, Exception inner) : base(message, inner) { }
}
// Each bounded context has its own models
// OrderManagement context
namespace {Company}.{Domain}.OrderManagement.Domain;
public sealed class Order : AggregateRoot<OrderId> { /* ... */ }
// Shipping context — different model for the same concept
namespace {Company}.{Domain}.Shipping.Domain;
public sealed class Shipment : AggregateRoot<ShipmentId>
{
public Guid OrderReference { get; private set; } // not OrderId
}
// Anti-corruption layer translates between contexts
public sealed class ShippingAntiCorruptionLayer(IOrderService orderService)
{
public async Task<ShipmentRequest> TranslateOrderAsync(
Guid orderId, CancellationToken ct)
{
var order = await orderService.GetOrderSummaryAsync(orderId, ct);
return new ShipmentRequest(
OrderReference: order.Id,
Address: MapToShippingAddress(order.ShippingAddress));
}
}
Guid everywhere instead of typed IDsList<T> instead of IReadOnlyList<T>AggregateRoot, Entity, ValueObject base classesGuid)IDomainEvent or DomainEvent typesCreate, Register)Guard.Against or similar invariant enforcementAggregateRoot<TId>, IDomainEvent| Concept | When to Use |
|---|---|
| Value Object | Immutable, identity-less (Money, Address, DateRange) |
| Entity | Has identity, mutable within aggregate boundary |
| Aggregate Root | Consistency boundary, entry point for changes |
| Domain Event | Side effect that other aggregates need to know about |
| Strongly-typed ID | Every aggregate root and key entity ID |
| Factory Method | When creation involves invariants or events |