Skip to content

Domain Develop

project-spec -> architecture-design -> domain-develop -> application-develop -> adapter-develop -> observability-develop -> test-develop

  • If 00-project-spec.md generated by the project-spec skill exists, it is automatically read to confirm Aggregate candidates and business rules.
  • If 01-architecture-design.md generated by the architecture-design skill exists, it is read to confirm folder structure and naming conventions.
  • If prerequisite documents are missing, the user is asked directly.

When writing domain code following DDD tactical design, there are many repeating patterns. The Create/Validate/CreateFromValidated triple factory for Value Objects, event publishing for Aggregate Roots, the Fin<Unit> return pattern for command methods — all follow identical structures per building block.

The /domain-develop skill automates this repetition. When you convey domain requirements in natural language, it generates code, unit tests, and documentation matching Functorium framework patterns in 5 steps.

StepTaskDeliverable
1Requirements analysisDomain model analysis table, folder structure
2Code generationVO, Entity, Aggregate, Event, Error, Spec, Service
3Unit test generationT1_T2_T3 naming convention, Shouldly verification
4Build/test verificationdotnet build + dotnet test passing
5Document generation (optional)Markdown design documents
Building BlockBase ClassDescription
Simple Value ObjectSimpleValueObject<T>Single primitive value wrapping
Composite Value ObjectValueObjectMultiple VO combination
Union Value ObjectUnionValueObject / UnionValueObject<TSelf>Allowed state combinations, state transitions
EntityEntity<TId>Child entity within an Aggregate
Aggregate RootAggregateRoot<TId>Transaction boundary
Domain EventDomainEventState change notification
Domain ErrorDomainErrorType.CustomBusiness rule violation
SpecificationExpressionSpecification<T>Query/search conditions
Domain ServiceIDomainServiceCross-Aggregate pure logic
RepositoryIRepository<T, TId>Persistence interface
PatternUsage
Fin<T> success/failureresult.IsSucc, result.IsFail
Value extractionresult.ThrowIfFail()
Success returnunit (using static LanguageExt.Prelude;)
Failure returnDomainError.For<T>(new ErrorRecord(), id, message)
EntityId generation{Type}Id.New() (Ulid-based)
Parallel validation composition(Fin1, Fin2).Apply((v1, v2) => ...)
State transitionTransitionFrom<TFrom, TTo>(mapper)

When managing Child Entity collections within an Aggregate, use the IReadOnlyList<T> public + List<T> internal pattern.

private readonly List<OrderItem> _items = new();
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
public Fin<Unit> AddItem(OrderItem item)
{
_items.Add(item);
AddDomainEvent(new ItemAddedEvent(Id, item.Id));
return unit;
}
public Fin<Unit> RemoveItem(OrderItemId itemId)
{
var item = _items.FirstOrDefault(i => i.Id == itemId);
if (item is null) return unit; // Idempotency: removing a non-existent item succeeds
_items.Remove(item);
AddDomainEvent(new ItemRemovedEvent(Id, itemId));
return unit;
}

Observability propagation attributes are applied to Request/Response DTOs in the Application layer. These attributes reflect the ctx.* propagation strategy designed in the observability-develop skill into code.

AttributePurposeExample
[CtxRoot]Promote to ctx.{field} root level[CtxRoot] string CustomerId
[CtxTarget(CtxPillar.All)]Propagate to specific Pillar[CtxTarget(CtxPillar.All)] string CustomerTier
[CtxIgnore]Exclude from all Pillars[CtxIgnore] string InternalMemo
/domain-develop Product aggregate with ProductName (max 100 chars), ProductPrice (positive decimal)

Invoking /domain-develop without arguments starts the skill in interactive mode, collecting requirements through conversation.

  1. Present analysis results — Shows identification results of Aggregates, Value Objects, Events, Errors, etc. in a table
  2. User confirmation — Proceed to code generation after confirming the analysis results
  3. Code + test generation — Generates code and tests per building block
  4. Build/test verification — Runs dotnet build and dotnet test to confirm passing

Example 1: Basic — Aggregate and Value Object

Section titled “Example 1: Basic — Aggregate and Value Object”

This is the most basic Aggregate pattern. It creates two Simple Value Objects wrapping single primitive values and an Aggregate Root that has them as properties. This structure is the starting point for Functorium domain development.

/domain-develop Product aggregate with ProductName (max 100 chars), ProductPrice (positive decimal).
Create, UpdateName, UpdatePrice command methods
Building BlockTypeDescription
Simple VOProductNameSimpleValueObject<string>, 100 char limit
Simple VOProductPriceSimpleValueObject<decimal>, positive validation
AggregateProductAggregateRoot<ProductId>, 3 command methods
Domain EventCreatedEvent, NameUpdatedEvent, PriceUpdatedEventState change notification
Unit Tests~36VO validation + Aggregate commands + FinApply

Simple Value Object — Single primitive value wrapping, Create/Validate/CreateFromValidated triple factory:

public sealed class ProductName : SimpleValueObject<string>
{
public const int MaxLength = 100;
private ProductName(string value) : base(value) { }
public static Fin<ProductName> Create(string? value) =>
CreateFromValidation(Validate(value), v => new ProductName(v));
public static Validation<Error, string> Validate(string? value) =>
ValidationRules<ProductName>
.NotNull(value)
.ThenNotEmpty()
.ThenMaxLength(MaxLength)
.ThenNormalize(v => v.Trim());
}

Command methodFin<Unit> return, state change + event publishing:

public Fin<Unit> UpdateName(ProductName newName, DateTime now)
{
Name = newName;
UpdatedAt = now;
AddDomainEvent(new NameUpdatedEvent(Id, newName));
return unit;
}

Example 2: Intermediate — Union Type and Child Entity

Section titled “Example 2: Intermediate — Union Type and Child Entity”

This adds three patterns to Example 1. Union Value Object that encodes state transitions as types, Composite Value Object that combines multiple VOs, and Child Entity that belongs within an Aggregate. When these three patterns combine, rules like “only Pending orders can be confirmed” or “shipping address can only be created when all 4 fields are valid” can be guaranteed at compile time.

/domain-develop Order aggregate with OrderStatus union
(Pending -> Confirmed -> Shipped -> Cancelled state transitions),
ShippingAddress composite VO (Street, City, State, ZipCode),
OrderItem child entity (ProductName, Quantity, UnitPrice).
Confirm and Ship command methods
Building BlockTypeDescription
Union VOOrderStatusUnionValueObject<OrderStatus>, 4 states, transition methods
Composite VOShippingAddressValueObject, 4 sub-VO combination
Child EntityOrderItemEntity<OrderItemId>, managed through parent Aggregate
AggregateOrderAggregateRoot<OrderId>, state transition-based commands

Union Value Object state transitionTransitionFrom allows only permitted transitions, others return InvalidTransition error:

[UnionType]
public abstract partial record OrderStatus : UnionValueObject<OrderStatus>
{
public sealed record Pending(DateTime CreatedAt) : OrderStatus;
public sealed record Confirmed(DateTime ConfirmedAt) : OrderStatus;
public sealed record Shipped(DateTime ShippedAt) : OrderStatus;
public sealed record Cancelled(DateTime CancelledAt) : OrderStatus;
private OrderStatus() { }
public Fin<Confirmed> Confirm(DateTime now) =>
TransitionFrom<Pending, Confirmed>(
_ => new Confirmed(now));
public Fin<Shipped> Ship(DateTime now) =>
TransitionFrom<Confirmed, Shipped>(
_ => new Shipped(now));
}

Composite Value Object — Parallel composition of child VO Validate via .Apply() to accumulate errors:

public sealed class ShippingAddress : ValueObject
{
public Street Street { get; }
public City City { get; }
public State State { get; }
public ZipCode ZipCode { get; }
private ShippingAddress(Street street, City city, State state, ZipCode zipCode)
{
Street = street;
City = city;
State = state;
ZipCode = zipCode;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
yield return City;
yield return State;
yield return ZipCode;
}
public static Validation<Error, (string, string, string, string)> Validate(
string? street, string? city, string? state, string? zipCode) =>
(Street.Validate(street), City.Validate(city),
State.Validate(state), ZipCode.Validate(zipCode))
.Apply((s, c, st, z) => (s, c, st, z));
public static Fin<ShippingAddress> Create(
string? street, string? city, string? state, string? zipCode) =>
CreateFromValidation<ShippingAddress, (string, string, string, string)>(
Validate(street, city, state, zipCode),
v => new ShippingAddress(
Street.CreateFromValidated(v.Item1),
City.CreateFromValidated(v.Item2),
State.CreateFromValidated(v.Item3),
ZipCode.CreateFromValidated(v.Item4)));
}

Child Entity — Inherits Entity<TId>, managed through parent Aggregate without event publishing:

[GenerateEntityId]
public sealed class OrderItem : Entity<OrderItemId>
{
public ProductName ProductName { get; }
public Quantity Quantity { get; private set; }
public UnitPrice UnitPrice { get; }
private OrderItem(OrderItemId id, ProductName name, Quantity qty, UnitPrice price)
: base(id)
{
ProductName = name;
Quantity = qty;
UnitPrice = price;
}
public static OrderItem Create(ProductName name, Quantity qty, UnitPrice price) =>
new(OrderItemId.New(), name, qty, price);
}

Example 3: Advanced — Domain Service and Specification

Section titled “Example 3: Advanced — Domain Service and Specification”

Logic that cannot be resolved by a single Aggregate emerges. Specification encapsulates query conditions as objects, and Domain Service contains pure logic spanning multiple Aggregates. These two patterns handle cross-cutting concerns while respecting Aggregate boundaries.

/domain-develop Inventory aggregate with StockQuantity VO (non-negative int),
LowStockThreshold VO (positive int).
InventoryLowStockSpec specification (stock at or below threshold),
InventoryTransferService domain service (source -> target stock transfer).
Restock, Transfer command methods
Building BlockTypeDescription
Simple VOStockQuantitySimpleValueObject<int>, 0 or greater
Simple VOLowStockThresholdSimpleValueObject<int>, positive
SpecificationInventoryLowStockSpecExpressionSpecification<Inventory>, stock <= threshold
Domain ServiceInventoryTransferServiceIDomainService, returns failure on insufficient stock
AggregateInventoryAggregateRoot<InventoryId>, Restock/Deduct commands

Specification — Converts VO to primitive to capture in Expression closure, enabling EF Core query translation:

public sealed class InventoryLowStockSpec : ExpressionSpecification<Inventory>
{
public LowStockThreshold Threshold { get; }
public InventoryLowStockSpec(LowStockThreshold threshold) =>
Threshold = threshold;
public override Expression<Func<Inventory, bool>> ToExpression()
{
int thresholdValue = Threshold;
return inventory => inventory.StockQuantity <= thresholdValue;
}
}

Domain Service — Stateless pure function, expressing cross-Aggregate logic as Fin<Unit>:

public sealed class InventoryTransferService : IDomainService
{
public sealed record InsufficientStock : DomainErrorType.Custom;
public Fin<Unit> Transfer(
Inventory source, Inventory target, StockQuantity amount, DateTime now)
{
if (source.StockQuantity < amount)
return DomainError.For<InventoryTransferService>(
new InsufficientStock(),
source.Id.ToString(),
$"Insufficient stock: current {(int)source.StockQuantity}, requested {(int)amount}");
source.Deduct(amount, now);
target.Restock(amount, now);
return unit;
}
}

Example 4: Practical — Complete Contact Domain Implementation

Section titled “Example 4: Practical — Complete Contact Domain Implementation”

All patterns from the previous three examples are combined in a single domain. The business requirements from the Designing with Types example are passed directly as a prompt to auto-generate the complete domain model. 9 Value Objects, 2 Unions, 1 Child Entity, 1 Aggregate, 1 Domain Service, 1 Specification — plus 114 unit tests, all created with a single skill invocation.

/domain-develop Contact aggregate -- implement the contact management domain.
## Contact Information Structure
- Personal name: First Name (required), Last Name (required), Middle Initial (optional)
- Email address: Standard email format
- Postal address: Address (Street), City, State Code (2 uppercase letters), Zip Code (5-digit number)
- Notes: Free-form text, 500 characters or less
## Business Rules
1. Data validity: First/last name 50 chars or less, email standard format, state code 2 uppercase letters, zip code 5-digit number
2. Contact method: At least one required (email only / postal address only / both). A contact without any contact method cannot exist
3. Email verification: Unverified -> Verified one-way transition. Verification timestamp recorded. Re-verification of already verified email is not allowed
4. Contact lifecycle: Name change, soft delete (deleter + timestamp), restoration possible. Modifying a deleted contact is not allowed. Delete/restore are idempotent
5. Note management: Add/remove possible, not allowed for deleted contacts, removing a non-existent note is idempotent
6. Email uniqueness: No duplicates allowed, excluding self
## Impossible States
- Verified status without an email
- Contact without any contact method
- Verified -> Unverified reversal
- Performing actions on a deleted contact
- Duplicate contacts with the same email

This prompt generates the same domain model as the Designing with Types example:

Building BlockTypeDescription
Simple VO (9)FirstName, LastName, MiddleInitial, EmailAddress, Street, City, StateCode, ZipCode, NoteContentPrimitive value validation
Composite VO (2)PersonalName, PostalAddressVO combination
Union VO (2)ContactInfo (email only/postal only/both), EmailStatus (unverified/verified)Allowed state combinations, one-way transition
Child EntityContactNoteNote management
AggregateContactCreate, UpdateName, VerifyEmail, SoftDelete, Restore, AddNote, RemoveNote
Domain ServiceContactEmailUniquenessServiceEmail uniqueness verification
SpecificationContactByEmailSpecEmail-based query
Unit Tests~1146 business rule groups + 10 scenarios fully verified

This example is not simple CRUD. The rule “a contact without any contact method cannot exist” is encoded as a Union type, “verification is one-way” as a state transition, and “modifying a deleted contact is not allowed” as an Aggregate guard condition — each encoded into the type system. To see how invalid states are blocked at compile time, refer to the full design process in the Designing with Types example.