Domain Model

This post explores creating a domain model in F#. It continues the series functional domain project which targets creating a basic asset management API.

Domain Requirements

These are the requirements (I’ve made up) for our asset management system.

  • An asset is a thing that the organization owns that has some capital value
  • An asset may contain another asset (e.g. a park contains a playground that contains a slide)
  • An asset is based on a template to ensure similar assets store the same information
  • A template contains custom fields which have a type (text, numeric, date)
  • An asset stores the values for the fields in its template
  • An asset may have a maintenance program. This specifies what work is carried out at certain intervals. For these requirements, an asset may only have a single maintenance program.
  • Any maintenance carried out must be recorded.

First Model

The following is a first pass at a model expressing these requirements.

module DomainTypes

/// MaintenanceProgram must be immutable in data store so that maintenance records referencing it have accurate information.
type MaintenanceProgram = {
    Id: System.Guid
    Summary: string
    Period: System.TimeSpan
    Details: string
}

type MaintenanceRecord = {
    Id: System.Guid
    // One of MaintenanceProgramId and Summary must be set
    MaintenanceProgramId: System.Guid option
    Summary: string option
    DateComplete: System.DateTime
    Details: string
    Cost: decimal
}

type FieldValue = 
    | StringField of string
    | DateField of System.DateTime
    | NumericField of float

type FieldDefinition = {
    Id: System.Guid
    Name: string
    Field: FieldValue
}

type Field = {
    Definition: FieldDefinition
    Value: FieldValue
}

type Template = {
    Id: System.Guid
    Name: string
    Fields: FieldDefinition list
    // I considered making MaintenanceProgram compositional, but that
    // might be limiting for organizations that want a simple set
    // of scheduled maintenance periods without specifics on the type of maintenance
    MaintenanceProgramId: System.Guid
}

type Asset = {
    Id: System.Guid
    Name: string
    Commissioned: System.DateTime
    Cost: decimal
    Fields: Field list
    TemplateId: System.Guid 
    Subassets: Asset list
}

F# records and discriminated unions provide a very natural way to express a domain model. In this model the majority of information is stored as records, with discriminated unions used to indicate and control the type of value stored in a field.

This model also makes the difference between composition and reference relationships clear. Composition implies the target instance will die with the source instance, whereas in a reference relationship the lifetime of the target is independent of the source. The Asset type illustrates this: the field values make no sense if there is no asset so they are compositional, and therefore we include the Field type within the Fields property of the Asset; whereas the template will be used by many assets so it is referenced by id.

Interestingly (at least to me), one could argue a sub-asset, i.e. a component of a system, could be reused in another asset when its current parent asset is decommissioned. This model is forbidding this by including subassets in the Asset rather than referencing them.

Refining Intent

At this point I wanted to enforce my constraint on the MaintenanceRecord, and via google, ran into the excellent series on F# for fun and profit, Designing with types

Firstly, several fields represent the same concept, but there is no way of identifying that. If the field type is replaced by a single case union type then that relationship becomes clearer. For instance:

type MaintenanceSummary = MaintenanceSummary of string

type MaintenanceProgram = {
    Summary: MaintenanceSummary
    ...
}

type MaintenanceRecord = {
    Summary = MaintenanceSummary option
    ...
}

Next, as noted in comments, we want to ensure one of MaintenanceProgramId and Summary is set in MaintenanceRecord. We can use a discriminated union to enforce this by replacing

type MaintenanceRecord = {
    MaintenanceProgramId: System.Guid option
    Summary: MaintenanceSummary option
    ...
}

with

type MaintenanceRecordSummary = 
    | MaintenanceProgramId of System.Guid
    | Summary of MaintenanceSummary

type MaintenanceRecord = {
    Summary: MaintenanceRecordSummary
    ....
}

This is as far as I’m going to take it for now as my overall knowledge of F# is still pretty limited, but I expect that as I implement the surrounding layers I’ll better understand the purpose of more of the advice within designing with types likely implement them.