Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultModel legal transitions
Construction is only the first guard. A valid value can become invalid when application code updates it.
A transition is a named operation that changes one valid state into another, such as completing an order or moving a booking. It may return an error when the requested change is not allowed.
Keep updates in the aggregate module and name them after the business action. Callers use operations such
as changeEnd, cancel, or complete instead of modifying storage fields.
Make uncertain changes fallible
Changing one end of a booking can break its date relationship, so reuse the constructor.
open System
type BookingError = EndBeforeStart
type BookingDraft = { Start: DateOnly; End: DateOnly }
type Booking =
private
{ Start: DateOnly
End: DateOnly }
[<RequireQualifiedAccess>]
module Booking =
let start booking = booking.Start
let finish booking = booking.End
let create (draft: BookingDraft) =
if draft.Start <= draft.End then Ok { Start = draft.Start; End = draft.End }
else Error BookingError.EndBeforeStart
let changeEnd newEnd booking =
create
{ Start = start booking
End = newEnd }
SystemFsLiveDocsGeneratedPage35_76B7E1D94D35.BookingErrorEndBeforeStartFsLiveDocsGeneratedPage35_76B7E1D94D35.BookingDraftStart: DateOnlySystem.DateOnlyRepresents dates with values ranging from January 1, 0001 Anno Domini (Common Era) through December 31, 9999 A.D. (C.E.) in the Gregorian calendar.
End: DateOnlyFsLiveDocsGeneratedPage35_76B7E1D94D35.BookingMicrosoft.FSharp.Core.RequireQualifiedAccessAttributeThis attribute is used to indicate that references to the elements of a module, record or union type require explicit qualified access. Attributes
FsLiveDocsGeneratedPage35_76B7E1D94D35.BookingModulestart: Booking -> DateOnlybooking: Bookingfinish: Booking -> DateOnlycreate: BookingDraft -> Result<Booking,BookingError>draft: BookingDraft(<=): 'T -> 'T -> boolStructural less-than-or-equal comparison The first parameter. The second parameter. The result of the comparison. 5 <= 1 // Evaluates to false 5 <= 5 // Evaluates to true [1; 5] <= [1; 6] // Evaluates to true
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
changeEnd: DateOnly -> Booking -> Result<Booking,BookingError>newEnd: DateOnlyThe type tells the caller that the change can be refused:
match Booking.changeEnd proposedEnd booking with
| Ok changed -> save changed
| Error BookingError.EndBeforeStart -> showDateError ()Keep preserving changes total
Some operations preserve the invariant by their construction. Shifting both dates by the same number of days cannot
reverse their order, because adding the same days to both ends preserves Start <= End. The relation argument holds
for every days for which both DateOnly.AddDays calls succeed; AddDays throws ArgumentOutOfRangeException when
the shifted date leaves the supported range (years 0001–9999), so shift is total over the invariant but not over all
date inputs. Bound the dates or make the failure explicit if callers can reach the limits. The same Booking as
above, create and BookingDraft omitted since this section adds only shift:
open System
type Booking =
private
{ Start: DateOnly
End: DateOnly }
[<RequireQualifiedAccess>]
module Booking =
let start booking = booking.Start
let finish booking = booking.End
let shift days booking =
{
Start = (start booking).AddDays days
End = (finish booking).AddDays days
}
SystemFsLiveDocsGeneratedPage36_76B7E1D94D35.BookingStart: DateOnlySystem.DateOnlyRepresents dates with values ranging from January 1, 0001 Anno Domini (Common Era) through December 31, 9999 A.D. (C.E.) in the Gregorian calendar.
End: DateOnlyMicrosoft.FSharp.Core.RequireQualifiedAccessAttributeThis attribute is used to indicate that references to the elements of a module, record or union type require explicit qualified access. Attributes
FsLiveDocsGeneratedPage36_76B7E1D94D35.BookingModulestart: Booking -> DateOnlybooking: Bookingfinish: Booking -> DateOnlyshift: int -> Booking -> Bookingdays: intAddDays: int -> DateOnlyAdds the specified number of days to the value of this instance. The number of days to add. To subtract days, specify a negative number. An instance whose value is the sum of the date represented by this instance and the number of days represented by value.
This function can return Booking directly. Keep it inside the module that can see the private representation.
Use drafts for several user edits
An edit screen often changes several fields before submission. Convert to a draft, edit it, then call the constructor
once. Booking.toDraft is the same accessor Build A Private Aggregate
defines alongside create.
let edited =
booking
|> Booking.toDraft
|> fun draft ->
{ draft with
Start = proposedStart
End = proposedEnd }
match Booking.create edited with
| Ok booking -> save booking
| Error error -> redisplay edited errorDo not pass the draft into business functions that expect the invariant to hold.
Use types for important lifecycle states
When operations differ sharply by state, use separate types so unavailable transitions are absent from the interface.
type DraftOrder
type SubmittedOrder
type CancelledOrder
[<RequireQualifiedAccess>]
module Order =
val submit:
DraftOrder ->
Result<SubmittedOrder, SubmitError>
val cancel:
SubmittedOrder ->
Result<CancelledOrder, CancelError>Order.submit cancelledOrder cannot compile because the function requires DraftOrder.
Use this for lifecycle states that change what operations are legal. A single discriminated union field is simpler when all states share the same operations and callers already handle them by pattern matching.
Keep operation policies separate
A transition belongs to the aggregate when the rule is always true for those related values. Tenant permissions, current time, feature flags, and operation-specific approval rules belong outside it.
Apply those requirements with ordinary result-returning functions after intrinsic construction has succeeded.