원클릭으로
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 adding caching to .NET APIs or optimizing response times with distributed cache, output cache, or ETags.
Use when configuring API response formats, custom formatters, or Accept header handling.
Use when building controller-based REST APIs with action results, model binding, or MediatR integration.
Use when creating RESTful API controllers with MediatR dispatch and ProblemDetails error responses.
Use when designing gRPC services, proto files, or adding gRPC-Web or JSON transcoding.
Use when building minimal API endpoints with route groups, filters, or TypedResults.
| 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 |