Skip to content

Domain Code Design

The rules defined in natural language in the Business Requirements were classified as invariants and type strategies were derived in the Type Design Decisions. This document maps those strategies to C# and Functorium DDD building blocks, examining the concrete code implementation of each pattern.

Design Decision -> C# Implementation Mapping

Section titled “Design Decision -> C# Implementation Mapping”
Design DecisionFunctorium TypeApplication ExampleGuaranteed Effect
Single value validation + normalizationSimpleValueObject<T> + Validate chainModelName, ModelVersion, EndpointUrlValidation at creation, Trim/SemVer normalization
Comparable value + rangeComparableSimpleValueObject<T>DriftThreshold, AssessmentScoreRange validation, domain properties
Smart Enum + domain propertiesSimpleValueObject<string> + HashMapRiskTier, IncidentSeverityOnly valid values allowed, domain rules embedded
Smart Enum + state transitionSimpleValueObject<string> + transition mapDeploymentStatus, IncidentStatusOnly allowed transitions possible
Aggregate Root dual factoryAggregateRoot<TId> + Create/CreateFromValidatedAIModel, ModelDeploymentSeparation of domain creation and ORM restoration
Child entityEntity<TId> + IReadOnlyListAssessmentCriterionCollection cannot be directly modified externally
Cross-Aggregate verificationIDomainServiceDeploymentEligibilityServiceFinT LINQ cross-verification
Domain events/errorsNested sealed recordRegisteredEvent, AlreadyDeletedCohesive within Aggregate

ModelName — String length validation + Trim normalization:

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

ModelVersion — SemVer regex validation:

public sealed partial class ModelVersion : SimpleValueObject<string>
{
private ModelVersion(string value) : base(value) { }
public static Fin<ModelVersion> Create(string? value) =>
CreateFromValidation(Validate(value), v => new ModelVersion(v));
public static Validation<Error, string> Validate(string? value) =>
ValidationRules<ModelVersion>
.NotNull(value)
.ThenNotEmpty()
.ThenNormalize(v => v.Trim())
.ThenMatches(SemVerPattern(), "Invalid SemVer format");
public static ModelVersion CreateFromValidated(string value) => new(value);
public static implicit operator string(ModelVersion version) => version.Value;
[GeneratedRegex(@"^\d+\.\d+\.\d+(-[\w.]+)?$")]
private static partial Regex SemVerPattern();
}

EndpointUrl — URI format custom validation:

public sealed class EndpointUrl : SimpleValueObject<string>
{
public sealed record InvalidUri : DomainErrorType.Custom;
private EndpointUrl(string value) : base(value) { }
public static Fin<EndpointUrl> Create(string? value) =>
CreateFromValidation(Validate(value), v => new EndpointUrl(v));
public static Validation<Error, string> Validate(string? value) =>
ValidationRules<EndpointUrl>
.NotNull(value)
.ThenNotEmpty()
.ThenNormalize(v => v.Trim())
.ThenMust(
v => Uri.TryCreate(v, UriKind.Absolute, out var uri)
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps),
new InvalidUri(),
v => $"Invalid endpoint URL format: '{v}'");
public static EndpointUrl CreateFromValidated(string value) => new(value);
}

ThenMust adds a custom validation condition to the chain. The InvalidUri error type is nested within the VO to clarify the error source.

2. ComparableSimpleValueObject + Range + Domain Properties

Section titled “2. ComparableSimpleValueObject + Range + Domain Properties”

DriftThreshold — Range 0.0~1.0:

public sealed class DriftThreshold : ComparableSimpleValueObject<decimal>
{
public const decimal MinValue = 0.0m;
public const decimal MaxValue = 1.0m;
private DriftThreshold(decimal value) : base(value) { }
public static Fin<DriftThreshold> Create(decimal value) =>
CreateFromValidation(Validate(value), v => new DriftThreshold(v));
public static Validation<Error, decimal> Validate(decimal value) =>
ValidationRules<DriftThreshold>
.Between(value, MinValue, MaxValue);
public static DriftThreshold CreateFromValidated(decimal value) => new(value);
}

AssessmentScore — Range 0~100 + passing threshold domain property:

public sealed class AssessmentScore : ComparableSimpleValueObject<int>
{
public const int MinValue = 0;
public const int MaxValue = 100;
public const int PassingThreshold = 70;
private AssessmentScore(int value) : base(value) { }
public static Fin<AssessmentScore> Create(int value) =>
CreateFromValidation(Validate(value), v => new AssessmentScore(v));
public static Validation<Error, int> Validate(int value) =>
ValidationRules<AssessmentScore>
.Between(value, MinValue, MaxValue);
public static AssessmentScore CreateFromValidated(int value) => new(value);
public bool IsPassing => Value >= PassingThreshold;
}

The IsPassing property embeds the domain rule (“70 or above passes”) in the value object. If this rule changes, only the PassingThreshold constant needs modification.

3. Smart Enum — RiskTier + Domain Properties

Section titled “3. Smart Enum — RiskTier + Domain Properties”

RiskTier is a Smart Enum pattern with domain properties (RequiresComplianceAssessment, IsProhibited) embedded.

public sealed class RiskTier : SimpleValueObject<string>
{
public sealed record InvalidValue : DomainErrorType.Custom;
public static readonly RiskTier Minimal = new("Minimal");
public static readonly RiskTier Limited = new("Limited");
public static readonly RiskTier High = new("High");
public static readonly RiskTier Unacceptable = new("Unacceptable");
private static readonly HashMap<string, RiskTier> All = HashMap(
("Minimal", Minimal), ("Limited", Limited),
("High", High), ("Unacceptable", Unacceptable));
private RiskTier(string value) : base(value) { }
public static Fin<RiskTier> Create(string value) =>
Validate(value).ToFin();
public static Validation<Error, RiskTier> Validate(string value) =>
All.Find(value)
.ToValidation(DomainError.For<RiskTier>(
new InvalidValue(), currentValue: value,
message: $"Invalid risk tier: '{value}'"));
public bool RequiresComplianceAssessment => this == High || this == Unacceptable;
public bool IsProhibited => this == Unacceptable;
}

RequiresComplianceAssessment and IsProhibited directly encode business rules in the type. ComplianceAssessment.Create() uses riskTier.RequiresComplianceAssessment to determine whether to create additional criteria, and DeploymentEligibilityService uses riskTier.IsProhibited to determine deployment prohibition.

4. Smart Enum — DeploymentStatus + Transition Rules

Section titled “4. Smart Enum — DeploymentStatus + Transition Rules”

DeploymentStatus declares 6-state transition rules using a HashMap transition map.

public sealed class DeploymentStatus : SimpleValueObject<string>
{
public static readonly DeploymentStatus Draft = new("Draft");
public static readonly DeploymentStatus PendingReview = new("PendingReview");
public static readonly DeploymentStatus Active = new("Active");
public static readonly DeploymentStatus Quarantined = new("Quarantined");
public static readonly DeploymentStatus Decommissioned = new("Decommissioned");
public static readonly DeploymentStatus Rejected = new("Rejected");
private static readonly HashMap<string, Seq<string>> AllowedTransitions = HashMap(
("Draft", Seq("PendingReview")),
("PendingReview", Seq("Active", "Rejected")),
("Active", Seq("Quarantined", "Decommissioned")),
("Quarantined", Seq("Active", "Decommissioned")));
public bool CanTransitionTo(DeploymentStatus target) =>
AllowedTransitions.Find(Value)
.Map(allowed => allowed.Any(v => v == target.Value))
.IfNone(false);
}

Transition rule summary:

Current StatusAllowed Targets
DraftPendingReview
PendingReviewActive, Rejected
ActiveQuarantined, Decommissioned
QuarantinedActive, Decommissioned
Decommissioned(terminal state)
Rejected(terminal state)

Functorium Aggregate Roots have two factory methods:

  • Create() — Receives already-validated Value Objects from the Application Layer, creates a new Aggregate, and publishes DomainEvents
  • CreateFromValidated() — ORM/Repository restoration only. Passes through already-validated/normalized data directly without executing validation logic or publishing DomainEvents. Should only be called from the persistence layer

This contract clearly separates creation and restoration paths, preventing “the cost of redundantly validating already-valid data” and “the side effect of re-publishing events during restoration.”

AIModel.Create() — Receives VOs for creation + event publishing:

public static AIModel Create(
ModelName name, ModelVersion version,
ModelPurpose purpose, RiskTier riskTier)
{
var model = new AIModel(AIModelId.New(), name, version, purpose, riskTier);
model.AddDomainEvent(new RegisteredEvent(model.Id, name, version, purpose, riskTier));
return model;
}

AIModel.ClassifyRisk() — Soft Delete guard + event:

public Fin<AIModel> ClassifyRisk(RiskTier newRiskTier)
{
if (DeletedAt.IsSome)
return DomainError.For<AIModel>(
new AlreadyDeleted(), Id.ToString(),
"Cannot classify risk for a deleted model");
var oldRiskTier = RiskTier;
RiskTier = newRiskTier;
UpdatedAt = DateTime.UtcNow;
AddDomainEvent(new RiskClassifiedEvent(Id, oldRiskTier, newRiskTier));
return this;
}

ModelDeployment.TransitionTo() — State transition + unified transition method:

private Fin<Unit> TransitionTo(DeploymentStatus target, DomainEvent domainEvent)
{
if (!Status.CanTransitionTo(target))
return DomainError.For<ModelDeployment, string, string>(
new InvalidStatusTransition(),
value1: Status, value2: target,
message: $"Cannot transition from '{Status}' to '{target}'");
Status = target;
UpdatedAt = DateTime.UtcNow;
AddDomainEvent(domainEvent);
return unit;
}
public Fin<Unit> SubmitForReview() =>
TransitionTo(DeploymentStatus.PendingReview, new SubmittedForReviewEvent(Id));
public Fin<Unit> Activate() =>
TransitionTo(DeploymentStatus.Active, new ActivatedEvent(Id));
public Fin<Unit> Quarantine(string reason) =>
TransitionTo(DeploymentStatus.Quarantined, new QuarantinedEvent(Id, reason));

All state transition methods delegate to the private TransitionTo method. Transition validation logic is concentrated in one place, so adding a new transition only requires adding an entry to the AllowedTransitions map and a public method.

6. Entity Child Entity (AssessmentCriterion)

Section titled “6. Entity Child Entity (AssessmentCriterion)”

AssessmentCriterion inherits from Entity<AssessmentCriterionId> as a child entity that exists only within the ComplianceAssessment Aggregate boundary.

public sealed class AssessmentCriterion : Entity<AssessmentCriterionId>
{
public string Name { get; private set; }
public string Description { get; private set; }
public Option<CriterionResult> Result { get; private set; }
public Option<string> Notes { get; private set; }
public Option<DateTime> EvaluatedAt { get; private set; }
public static AssessmentCriterion Create(string name, string description) =>
new(AssessmentCriterionId.New(), name, description);
public AssessmentCriterion Evaluate(CriterionResult result, Option<string> notes)
{
Result = result;
Notes = notes;
EvaluatedAt = DateTime.UtcNow;
return this;
}
}

ComplianceAssessment.Create() — Automatic assessment criteria generation based on risk tier:

public static ComplianceAssessment Create(
AIModelId modelId, ModelDeploymentId deploymentId, RiskTier riskTier)
{
var assessment = new ComplianceAssessment(
ComplianceAssessmentId.New(), modelId, deploymentId);
var criteria = GenerateCriteria(riskTier);
assessment._criteria.AddRange(criteria);
assessment.AddDomainEvent(new CreatedEvent(
assessment.Id, modelId, deploymentId, criteria.Count));
return assessment;
}
private static List<AssessmentCriterion> GenerateCriteria(RiskTier riskTier)
{
var criteria = new List<AssessmentCriterion>
{
AssessmentCriterion.Create("Data Governance", "Verify data quality..."),
AssessmentCriterion.Create("Technical Documentation", "Review completeness..."),
AssessmentCriterion.Create("Security Review", "Assess security...")
};
if (riskTier.RequiresComplianceAssessment)
{
criteria.Add(AssessmentCriterion.Create("Human Oversight", "..."));
criteria.Add(AssessmentCriterion.Create("Bias Assessment", "..."));
criteria.Add(AssessmentCriterion.Create("Transparency", "..."));
}
if (riskTier.IsProhibited)
criteria.Add(AssessmentCriterion.Create("Prohibition Review", "..."));
return criteria;
}

The RequiresComplianceAssessment and IsProhibited domain properties of RiskTier determine the number of assessment criteria: Minimal/Limited gets 3, High gets 6, Unacceptable gets 7. Since these rules are embedded in the Smart Enum, the if branches align with the domain language.

7. IDomainService — DeploymentEligibilityService

Section titled “7. IDomainService — DeploymentEligibilityService”

DeploymentEligibilityService verifies deployment eligibility across Aggregates. It chains 3 verifications sequentially using FinT<IO, Unit> LINQ composition.

public sealed class DeploymentEligibilityService : IDomainService
{
public sealed record ProhibitedModel : DomainErrorType.Custom;
public sealed record ComplianceAssessmentRequired : DomainErrorType.Custom;
public sealed record OpenIncidentsExist : DomainErrorType.Custom;
public FinT<IO, Unit> ValidateEligibility(
AIModel model,
IAssessmentRepository assessmentRepository,
IIncidentRepository incidentRepository)
{
return
from _1 in CheckNotProhibited(model)
from _2 in CheckComplianceAssessment(model, assessmentRepository)
from _3 in CheckNoOpenIncidents(model, incidentRepository)
select unit;
}
}

The three verifications are composed sequentially using LINQ from...in. If the first verification fails, the rest are not executed (short-circuit). FinT<IO, Unit> expresses IO effects and failure possibility through types.

MethodReturn TypeClassificationReason
AIModel.Create()AIModelAlways succeedsOnly receives already-validated VOs
AIModel.ClassifyRisk()Fin<AIModel>FailableAlreadyDeleted when modifying archived model
AIModel.Archive()AIModelIdempotentRe-invocation allowed in already-archived state
AIModel.Restore()AIModelIdempotentRe-invocation allowed in already-restored state
ModelDeployment.Create()ModelDeploymentAlways succeedsOnly receives already-validated VOs
ModelDeployment.SubmitForReview()Fin<Unit>FailableInvalid state transition
ModelDeployment.Activate()Fin<Unit>FailableInvalid state transition
ModelDeployment.Quarantine()Fin<Unit>FailableInvalid state transition
ModelDeployment.RecordHealthCheck()ModelDeploymentAlways succeedsHealth check recording is always valid
ComplianceAssessment.Create()ComplianceAssessmentAlways succeedsOnly receives already-validated VOs
ComplianceAssessment.EvaluateCriterion()Fin<Unit>FailableWhen criterion cannot be found
ComplianceAssessment.Complete()Fin<Unit>FailableWhen unevaluated criteria exist
ModelIncident.Create()ModelIncidentAlways succeedsOnly receives already-validated VOs
ModelIncident.Investigate()Fin<Unit>FailableInvalid state transition
ModelIncident.Resolve()Fin<Unit>FailableInvalid state transition
ModelIncident.Escalate()Fin<Unit>FailableInvalid state transition

See Implementation Results to confirm how this type structure guarantees business scenarios.