Skip to content

Application Type Design Decisions

This document analyzes the workflow rules defined in application business requirements to identify Use Cases (Command/Query) and ports. It determines which ports each Use Case uses and in what order it orchestrates them.

CommandInputCore FlowResult
RegisterModelCommandName, Version, PurposeVO composition -> risk classification -> model creation -> saveModelId
ClassifyModelRiskCommandModelId, RiskTierModel lookup -> reclassification -> update
CreateDeploymentCommandModelId, Url, Env, DriftVO composition -> model confirmation -> deployment creation -> saveDeploymentId
SubmitDeploymentForReviewCommandDeploymentIdDeployment lookup -> model lookup -> eligibility verification -> submit -> save
ActivateDeploymentCommandDeploymentId, AssessmentIdDeployment lookup -> assessment lookup -> passage confirmation -> activate -> save
QuarantineDeploymentCommandDeploymentId, ReasonDeployment lookup -> quarantine -> save
InitiateAssessmentCommandModelId, DeploymentIdModel lookup -> deployment lookup -> assessment creation -> saveAssessmentId
ReportIncidentCommandDeploymentId, Severity, DescVO composition -> deployment lookup -> incident creation -> saveIncidentId
QueryInputResultPort
GetModelByIdQueryModelIdModel detail (including deployments/assessments/incidents)IModelDetailQuery
SearchModelsQueryRiskTier?, Page, SizeModel listIAIModelQuery
GetDeploymentByIdQueryDeploymentIdDeployment detailIDeploymentDetailQuery
SearchDeploymentsQueryStatus?, Env?, Page, SizeDeployment listIDeploymentQuery
GetAssessmentByIdQueryAssessmentIdAssessment detail (including criteria)IAssessmentRepository
GetIncidentByIdQueryIncidentIdIncident detailIIncidentRepository
SearchIncidentsQuerySeverity?, Status?, Page, SizeIncident listIIncidentQuery
Event HandlerTrigger EventAction
QuarantineDeploymentOnCriticalIncidentHandlerModelIncident.ReportedEventAuto-quarantine deployment on Critical/High severity
InitiateAssessmentOnRiskUpgradeHandlerAIModel.RiskClassifiedEventCreate assessments for active deployments on High/Unacceptable upgrade

Ports are interfaces through which the Application Layer communicates with the outside world. Command ports (Repository) are write-only interfaces for state changes and lookups, and Query ports (Read Adapter) are read-only interfaces. This separation follows CQRS principles, allowing independent optimization paths for writes and reads.

External service ports express LanguageExt IO advanced features (Timeout, Retry, Fork, Bracket) through the return type FinT<IO, T>, enabling natural composition into the Application Layer’s FinT LINQ chain.

PortBase CRUDCustom Methods
IAIModelRepositoryIRepository baseExists(spec), GetByIdIncludingDeleted(id)
IDeploymentRepositoryIRepository baseExists(spec), Find(spec)
IAssessmentRepositoryIRepository baseExists(spec), Find(spec)
IIncidentRepositoryIRepository baseExists(spec), Find(spec)
PortRole
IAIModelQueryModel list search (filter, pagination)
IModelDetailQueryModel detail lookup (deployment/assessment/incident aggregation)
IDeploymentQueryDeployment list search
IDeploymentDetailQueryDeployment detail lookup
IIncidentQueryIncident list search
PortRoleIO Pattern
IModelHealthCheckServiceModel health checkTimeout + Catch
IModelMonitoringServiceModel drift monitoringRetry + Schedule
IParallelComplianceCheckServiceParallel compliance checkFork + awaitAll
IModelRegistryServiceExternal registry lookupBracket

Validates multiple Value Objects simultaneously and collects all errors at once. Does not stop at the first error; runs all validations.

// RegisterModelCommand.Usecase.Handle()
from vos in (
ModelName.Create(request.Name),
ModelVersion.Create(request.Version),
ModelPurpose.Create(request.Purpose)
).ApplyT((name, version, purpose) => (Name: name, Version: version, Purpose: purpose))

ApplyT composes each Fin<T>’s validation result applicatively. If 2 out of 3 fail, both errors are collected.

The core pattern of Command Handlers is FinT<IO, T> LINQ composition. It expresses IO effects (database lookups, saves) and failure possibilities (domain errors) in a single chain.

// SubmitDeploymentForReviewCommand.Usecase.Handle()
FinT<IO, Response> usecase =
from deployment in _deploymentRepository.GetById(deploymentId)
from model in _modelRepository.GetById(deployment.ModelId)
from _1 in _eligibilityService.ValidateEligibility(
model, _assessmentRepository, _incidentRepository)
from _2 in deployment.SubmitForReview()
from updated in _deploymentRepository.Update(deployment)
select new Response();

Each from clause executes only when the previous step succeeds. If ValidateEligibility returns a ProhibitedModel error, SubmitForReview is not executed.

All Use Cases encapsulate Request, Response, Validator, and Usecase within a single outer class using the Nested Class pattern.

public sealed class RegisterModelCommand
{
public sealed record Request(...) : ICommandRequest<Response>;
public sealed record Response(string ModelId);
public sealed class Validator : AbstractValidator<Request> { ... }
public sealed class Usecase(...) : ICommandUsecase<Request, Response>
{
public async ValueTask<FinResponse<Response>> Handle(...) { ... }
}
}

Advantages of this pattern:

  • All components of a Use Case are cohesive in one file
  • Request/Response types are clear at the Use Case namespace level
  • Validator is located in the same scope as Request, making rule tracing easy

Validators directly reuse domain VO Validate methods.

public sealed class Validator : AbstractValidator<Request>
{
public Validator()
{
RuleFor(x => x.Name).MustSatisfyValidation(ModelName.Validate);
RuleFor(x => x.Version).MustSatisfyValidation(ModelVersion.Validate);
RuleFor(x => x.Purpose).MustSatisfyValidation(ModelPurpose.Validate);
}
}

MustSatisfyValidation converts a VO’s Validate method into a FluentValidation rule. Domain validation rules are defined in one place and reused in the Application Layer.

Smart Enum type validation uses MustSatisfyValidationOf:

RuleFor(x => x.RiskTier)
.MustSatisfyValidationOf<Request, string, RiskTier>(RiskTier.Validate);

Entity ID validation uses MustBeEntityId:

RuleFor(x => x.ModelId).MustBeEntityId<Request, AIModelId>();

In the next step, we map this type design to C# code in Code Design.