Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save natalie-o-perret/6d765dbd15a351c295bd8fe124dad188 to your computer and use it in GitHub Desktop.
Save natalie-o-perret/6d765dbd15a351c295bd8fe124dad188 to your computer and use it in GitHub Desktop.
Domain Modeling Made Functional Notes

Importance of a Shared Model

Focus of developer:

  • Not to code
  • But to solve problems

Good thing to share a mental model with the:

  • Domain Experts
  • Development Team
  • Other stakeholders
  • And... CODE! (still)

Why?

  • Faster TTM
  • More business value
  • Less waster (clearer requirements)
  • Easier maintenance
  • Increased "connection" among teams

How? General guidelines:

  • Focus on business events and workflows rahter than DS
  • Partition problem domain to subdomains
  • Ubiquitous Language

Example used through this book:

We're a tiny company that manufactures parts for other companies: widgets, gizmos and the like. We've been growing quite fast, and our current processes are not able to keep up.

Right now, everything we do is pape-based, we'd like to computerize all that so our staff can handle larger volumes of orders.

In particular, we'd like to have a self-service website so that customers can do some tasks themselves. Things like placing an order, checking order status, and so on.

Whenever some new elements come into play, you can consider that they either come from:

  • information from domain experts
  • arbitrary decisions for implementation purposes

Understanding The Domain Through Business Events!

Events

How to discover those events? => Event storming!

Event storming can only be achieved with everybody (business, devs, etc.), majors steps include:

  • question others' answers (ie. find gaps in requirements)
  • extend the chains of events as far as you can (ie. edges)
  • be explicit about the different ownerships (ie. x y team, customer, etc.)
  • aggregate everything in a consistent way (ie. big picture)

Example with an Order-Taking System:

  • Order-taking Team
    • Order Form Received
    • Order Placed
    • Order Shipped
    • Order Change Requested
    • Order Cancellation Requested
    • Acknowledgment Sent To Customer
  • Shipping Team
    • Return Requested
    • Quote Form Received
    • Quote Provided
    • New Customer Request Received
    • New Customer Registred
    • Dispatch Message Sent To Customer
  • Customer
    • Signed for delivery

Commands

Once we have events, we look at what are the events that trigger them.

If the command was "Make X happen" then if the workflow made X happen, the corresponding domain event should be "X happened".

Long story short:

  1. Event (triggers) =>
  2. Command (input data needed for workflow) =>
  3. (Business) Workflow (output list of events) => Non-Terminal Full Cycle JCVD!
  4. GOTO 1. ^^"""" ;-)

Translated to our example:

  1. Event: Order Form Received
  2. Command: Place Order
  3. Workflow: Place Order Workflow outputs: "Order Placed (for shipping)" + "Order Placed (for billing)"

Notes:

  • it goes without saying that not every single event needs to be associated with a command!
  • not every command suceeds, we'll discuss about that later (see chapter 10)

Partitioning the Domain into Subdomains

Example - Subdomains:

  • Problem Space (real world):
    • Order-taking Domain
    • Shipping Domain
    • Billing Domain

Note: the domains can overlap a bit with each other.

Creating a Solution Using Bounded Contexts

Problem Space != Solution Space (implementation): Only capture the information that is relevant to solving a particular problem.

Example - Bounded Contexts:

  • Solution Space (domain world):
    • Order-taking Context
    • Shipping Context
    • Billing Context

Example - Context Map:

  • Customer
    • => [Order/Quote Received]
      • => Order-taking Context
  • Order-taking Context
    • => [Quote Returned]
      • => Customer
    • => [Order Placed]:
      • => Shipping Context
      • => Billing Context

Notes: relationship between domains and bounded contexts is not always 1:1

Definitions

  • Domain: area of knowledge associated with the problem, that which a domain expert is expert in.

  • Domain Model: set of simplifications representing the relevant aspects of a domain regarding a particular problem.

  • Scenario: a User(pov)-Centric Goal Description (e.g placing an order), similar to a user story in agile. A use case is a more detailed version of a scenario.

  • Business Process: Business-Centric Goal Description.

  • Workflow: exact steps of a Business Process. We'll limit ourselves to a single person / team scope, so that it can be divided in smaller workflows when combined togetther.

  • Ubiquitous Language: sets of concepts and vocabulary that is associated with the domain and is shared by both the team members and the source code.

  • Bounded contexts (ie. think of boundaries) kinda subsystem in the implementation.

  • Context Map: high level diagram showing a the relationhsips between bounded contexts.

  • Domain Event: record of something that happened in the system, always described in the past tense.

  • Command: request for some process to happen and is triggered by a person or another event. If the process suceeds, the state of the system changes and one or more Domain Events are recorded.

Interview with a Domain Expert

  • Understand non-functional requirements
    • Discuss the context and scale of the workflow
    • Customer Expectations: system designed beginners or experts? => difference in how we think of the design
    • Discuss performances: latency, predictability etc.
  • Figure out the rest of the workflow

Think also about inputs and outputs (ie. events)

Example:

  • Input: Order Form
  • Workflow: Place Order
  • Output: Order Placed event => to notify Shipping and Billing contexts

But the workflow can also have another input like a Product Catalog and suffers from side effets like sending an "Order Acknowledgment" to the customer.

Fight Database-Driven Design

Don't do low level now => Persistence Ignorance: Database is not part of the ubiquitous language.

Fight Class-Driven Design

Don't do low level now, again! => Don't impose technical ideas on the domain during requirements gathering.

Documenting the Domain

Example - Workflow Documentation:

Bounded Context: Order-Taking

Workflow: "Place Order"
    triggered by:
        "Order form received" event
    primary input:
        Order form
    other input:
        Product Catalog
    output events:
        "Order Placed" event
    side-effects:
        Acknowledgment sent to the customer

Example - Data Structures Documentation:

bounded context: Order-Taking

data Order = 
    CustomerInfo
    AND ShippingAddress
    AND BillingAddress
    AND list of OrderLines
    AND AmountToBill

data OrderLine = 
    Product
    AND Quantity
    AND Price

data CustomerInfo = ???     // don't know yet
data BillingAddress = ???   // don't know yet

Representing Complexity in Our Domain Model

Representing Constraints

Most primitive values first:

context: Order-Taking

data WidgetCode = string starting with "W" then 4 digits
data GizmoCode = string starting with "G" then 3 digits
data ProductCode = WidgetCode OR GizmoCode

Requirement about quantities:

data OrderQuantity = UnitQuantity OR KilogramQuantity

data UnitQuantity = integer between 1 and 1000
data KilogramQuantity = decimal between 0.05 and 100.00

Representing the Life Cycle of an Order

Unvalidated Orders

data UnvalidatedOrder =
    UnvalidatedCustomerInfo
    AND UnvalidatedShippingAddress
    AND UnvalidatedBillingAddress
    AND list of UnvalidatedOrderLine

data UnvalidatedOrderLine = 
    UnvalidatedProductCode
    AND UnvalidatedOrderQuantity

Validated Orders

data ValidatedOrder = 
    ValidatedCustomerInfo
    AND ValidatedShippingAddress
    AND ValidatedBillingAddress
    AND list of ValidatedOrderLine

data ValidatedOrderLine =
    ValidatedProductCode
    AND ValidatedOrderQuantity

Price

data PricedOrder = 
    AND ValidatedShippingAddress
    AND ValidatedBillingAddress
    AND list of PricedOrderLine
    AND AmountToBill

data PricedOrderLine = 
    ValidatedOrderLine
    AND LinePrice

Acknowledgment

data PlacedOrderAcknowledgment = 
    PricedOrder
    AND AcknowledgmentLetter

Fleshing out the Steps in the Workflow

Overall Workflow

workflow "Place Order" = 
    input: OrderForm
    output:
        OrderPlaced event (put on a pile to send to other teams)
        OR InvalidOrder (put on appropriate pile)

        // step 1
        do ValidateOrder
        If order is invalid then:
            add InvalidOrder to pile
            stop
        
        // step 2
        do PriceOrder

        // step 3
        do SendAcknowledgmentToCustomer

        // step 4
        return OrderPlaced event (if no errors)

Validation SubStep

substep "ValidateOrder" = 
    input: UnvalidatedOrder
    output: ValidatedOrder OR ValidationError
    dependencies: CheckProductCodeExists, CheckAddressExists

    validate the customer name
    check that the shipping and billing address exist
    for each line:
        check product code syntax
        check that product exists in ProductCatalog

    if everything is OK, then:
        return ValidatedOrder
    else:
        return ValidationError

Price Calculation SubStep

substep "PriceOrder" = 
    input: ValidatedOrder
    output: PricedOrder
    dependencies: GetProductPrice

    for each line:
        get the price for the product
        set the price for the line
    set the amount to bill ( = sum of the line prices)

Send Acknowledgment SubStep

substep "SendAcknowledgmentToCustomer = 
    input: PricedOrder
    output: None

    create acknowledgment letter and send it and the priced order to the custmer

Bounded Contexts As Autonomous Software Components

  • Bounded contexts must be an autonomous subsystem
  • Depending on the deployment, it could be either:
    • separate module with a well-defined interface
    • more distinct component such as a .NET assembly
    • a contained service in the case of a micro-service architecture
    • more fine-grained with each workflow deployed as a container

Communicating Between Bounded Contexts

Example

  • The Place-Order workflow emits an OrderPlaced event
  • OrderPlaced event is put on a queue or otherwise published
  • The shipping context listens for OrderPlaced events
  • When an event is received, a ShipOrder command is created
  • The ShipOrder command initiates the Ship-Order workflow
  • When Ship-Order workflow finishes successfully, it emits an OrderShipped event

Note:

  • in that example above, the order taking component is considered upstream and the shipping one downstream
  • queues are generally are good candidates for queue-based

Transferring Data Between Bounded Contexts

Within the upstream domain boundaries, the chain of transformations from upstream to downstream looks like:

  • Upstream Context:
    • Domain Type
    • Domaine Type to DTO
    • DTO Type @ Domain Boundary
  • In between:
    • Serialize DTO to json/xml
    • Whathever medium of communication (even a function / method call)
    • Deserialize json/xml to DTO
  • Downstream Context:
    • DTO Type @ Domain Boundary
    • DTO Type to Domain Type
    • Domain Type

Trust Boundaries and Validation

Bounded context perimeter act as a "trust boundary":

  • Anything inside the bounded context will be trusted and valid.
  • Everything outside is untrusted and might invalid.

Therefore we need "gates" at the beginning (ie. input) and end(ie. output) of the workflow.

  • The input gate always validates the input to make it conforms to the constraints of the domain model. If the validation fails then rest of the workflow is bypassed and an error is generated.

  • The output gate is to make sure that private information does not leak out of the bounded context. Both to avoid accidental coupling and security reasons.

Contracts Between Bounded Contexts

Who gets to decide the contract? There are various relationships between contexts but here are the 3 most common ones:

  • Shared Kernel: two contexts share the same domain design, so the teams involved must collaborate.
  • Customer / Supplier (or Consumer Driven Contract): downstream context has the upper hand, so the upstream context should conform.
  • Conformist: the exact opposite of the consumer driven contract.

Anti-Corruption Layers

The bounded-context should not be adapted to the external systems with which it has to interact, sometimes, indirectly. That is the ACL is a proxy to deliver properly formatted data between two different contexts.

The ACL main purpose is not to perform validation or data corruption check but to reduce coupling between contexts to allow them to evolve independently.

A Context Map with Relationships

Example:

  • Adress Checking > ACL > Order-taking
  • Product Catalog > Conformist > Order-taking
  • Order-taking > Shared Kernel > Shipping
  • Order-taking > Consumer-driven > Billing

Workflows Within a Bounded Context

A workflow can be mapped to a single function, where the input is a command object and the output is a list of event objects.

Workflow Inputs and Outputs

The input is always associated with the data of a command. The output is always a set of events to communicate to other contexts.

Avoid Domain Events Within a Bounded Context

In a functional design the domain events are not raised internally (unlike the classic object-oriented design), if we need a "listener" for an event, we just append it to the end of the workflow.

Code Structure Within a Bounded Context

No traditional "layered" approach, this is due to the fact that layers might be too intermingled and can make the logic unecessarily complicated.

The Onion Architecture

  • 1st ring (inner layer): Domain
  • 2nd (intermediate layer): Services
  • 3rd (external layer): API/Interface + Infrastructure + Database

Keep I/O at the Edges

Remember persistence ignorance... I/O is a infrastructural concern.

Understanding Functions

Pure functions, only: its result is only determined by its input values, without observable side effects.

Type Signatures

let add x = x + 1   // Signature is: int -> int
let add x y = x + y // Signature is: int -> int -> int

Subfunctions

Example

// Signature is: int -> int
let squarePlusOne x = 
    let square = x * x
    square + 1

Note: last value is returned automagically.

Functions with Generic Types

// Signature is: `a -> `a -> bool
let areEqual x y = 
    (x = y)

Note: yup, implicit: as long as the two types are the same.

Types and Functions

Type in the functional world != Class in OO.

A type is a set of possible values that can be used as input or output for a function.

Composition of Types

Functional World = higly more compositional than OO.

F# use an algrebraic type system since, new types in F# are built from smaller types in 2 ways:

  • AND-ing
  • OR-ing

"AND" Types

Also called "product / record type".

Example: Fruit Salad = apple AND banana AND cherry

type FruidSalad = {
  Apple: AppleVariety
  Banana: BananaVariety
  Cherries: CherryVariety
}

"OR" Types

Also called a "choice type", "sum types" or "tagged / discriminated unions".

Example: Fruit Snack = apple OR banana OR cherry

type FruitSnack = 
  | Apple of AppleVariety
  | Banana of BananaVariety
  | Cherries of CherryVariety

Of course this can go "deeper":

type AppleVariety = 
  | GoldenDelicious
  | GrannySmith
  | Fuji

// etc.

Simple Types

Choice type with only one choice

type ProductCode =
  | ProductCode of string

Is always simplified to:

type ProductCode = ProductCode of string

Working with F# types

Example - Record Type:

// Product Type Definition
type Person = {First: string; Last: string}

// Constructing
let aPerson = {First="Alex"; Last="Adams"}

// Deconstructing
let {First=first; Last=last} = aPerson

// Equivalent to
// let first = aPerson.First
// let last = aPerson.Last

Example - Choice Type:

// Sum Type Definition
type OrderQuantity = 
  | UnitQuantity of int
  | KilogramQuantity of decimal

// Constructing...
// UnitQuantity and KilogramQuantity below 
// are just part of two differents OrderQuantity
// Cases are not subclasses!
let anOrderQtyInUnits = UnitQuantity 10
let anrderQtyInKg = KilogramQuantity 2.5

// Deconstructing...
// with pattern matching
let printQuantity aOrderQty =
  match aOrder with
  | UnitQuantity uQty ->
    printfn "%i units" uQty
  | KilogramQuantity kgQty
    printfn "%g kg" kgQty

Building a Domain Model by Composing Types

Example

type CheckNumber = CheckNumber of int
type CardNumber = CardNumber of string

type CardType = 
  Visa | MasterCard

type CreditCardInfo = {
  CardType : CardType
  CardNumber : CardNumber
}

type PaymentMethod = 
  | Cash
  | Check of CheckNumber
  | Card of CreditCardInfo

type PaymentAmount = PaymentAmount of decimal
type Currency = EUR | USD

type Payment = {
  Amount: PaymentAmount
  Currency: Currency
  Method: PaymentMethod
}

Modeling Optional Values, Errors and Collections

Optional Values

type Option<'a> = 
  | Some of 'a
  | None

Yup this is what a generic looks like in F#. No need to define this type above, it's already part of the standard library.

To indicate something optional this can be in two ways:

// like in C# or Java:
type PersonalName = {
  FirstName: string
  MiddleInitial: Option<string>
  LastName: string
}

// or putting the label option behind:
type PersonalName = {
  FirstName: string
  MiddleInitial: string option
  LastName: string
}

Errors

type Result<'Success, `Failure> = 
  | Ok of 'Success
  | Error of 'Failure

No Value

No void in F#, but the "unit type" which has a single value '()'.

Lists and collections

  • list: fixed-size immutable collection (implemented as a linked list)
  • array: fixed-size mutable collection
  • ResizeArray: variable size array, equivalent of List<T>
  • seq: lazy collection, equivalent of the C# IEnumerable<T>
  • Map and Set exist for C# HashSet<T> and Dictionary<T> but they are hardly used in the functional world

Note: list just like the option can be added as a suffix:

type Order = {
  OrderId : OrderId
  Lines : OrderLine list // a collection
}

Ways to create a list:

// List literal
let aList = [1; 2; 3]

// Prepending a value with the :: cons operator
let aNewList = 0 :: [1; 2; 3]

// Deconstructing...
let printList1 aList = 
  match aList with
  | [] -> 
    printfn "list is empty"
  | [x] ->
    printfn "list as one element: %A" x
  | [x; y] ->
    printf "list has two elements: %A and %A" x y
  | first::rest ->
    printfn "list is non-empty with the 1st element being: %A" first
  | longerList ->
    printfn "list has more than two elements"

Organizing Types in Files and Projects

F# has strict rules about the order of declarations:

  • A type higher in a file cannot reference another type further down in a file.
  • A file earlier in the compilation order cannot reference a file later in the compilation order.

It goes without saying that simpler types need to be put at the top of the file, before other complex types that leverage them.

Seeing Patterns in a Domain Model

  • Simple Values: building blocks represented by a primitive type such as strings and integers.
  • Combination of Values with AND: groups of closely linked data, typically documents of subcomponents of a document like names, orders, etc.
  • Choices with OR: a choice in our domain, Order or a Quote, UnitQuantity or a KilogramQuantity
  • Workflows: business processes that have inputs and ouputs

Modeling Simple Values

Examples:

// Definining the types
type WidgetCode = WidgetCode of string
type UnitQuantity = UnitQuantity of int
type KilogramQuantity = KilogramQuantity of decimal

// and their resp. values
let customerId = CustomerId 42
let orderId = OrderId 42

// can be deconstructed
let (CustomerId innerValue) = customerId

// This below does not compile, not the same types
// Feels safe =]
printfn "%b" (orderId = customerId)

// Same if a function is defined with a particular type as parameter and receive a different one
let processCustomerId (id:CustomerId)

Just like in C# you can have aliases, this can actually help to deal with performance issues in F#:

// But no more real type safety
type UnitQuantity = int

Another way to tackle memory issues can be using C#-like struct:

[<struct>]
type UnitQuantity = UnitQuantity of int

Finally for collection you can directly use a collection as single types:

type UnitQuantities = UnitQuantities of int[]

Modeling Complex Data

Records

Example:

type Order = {
	CustomerInfo : CustomerInfo
	ShipppingAddress : ShippingAddress
	BillingAddress : BillingAddress
	OrderLines : OrderLine list
	AmountToBill : ...
}

Choices

Example:

type ProdductCode =
	| Widget of WidgetCode
	| Gizmo of GizmoCode

type OrderQuantity =
	| Unit of UnitQuantity
	| Kilogram of KilogramQuantity

Unknown types

If you need to progress on your design while ignoring a few things you can still leverage an Undefined alias:

type Undefined = exn

Note: exn is actually an alias for the C# System.Exception

A Question of Identity

Value Objects

A type that has no persistent identity, one way to think of them is like the struct objects in C#: immutable and equality strictly based on the fact all their fields must be equal to one another. Like primitives, they are interchangeable, in the context of this book example, a WidgetCode, Name or an Address are typically value objects.

Examples:

let widgetCode1 = WidgetCode "W1234"
let widgetCode2 = WidgetCode "W1234"
// true
printfb "%b" (widgetCode1 = widgetCode2)

let name1 = {FirstName="Alex"; LastName="Adams"}
let name2 = {FirstName="Alex"; LastName="Adams"}
// true again
printfb "%b" (name1 = name2)

In F# no need for a specific equality implementation is required to achieve the equality described above since the default one is perfectly fine: two record values are equal if all their fields are equal, two choices are equal if they are... well, equal ;-)

Entities

Identifiers

Unlike value objects, entities have a persistent identity, even when their components change. They are therefore mutable in the sense of the "real-world" but retain a stable identity (ie. they have an identitfier that does not change).

Example:

type InvoiceId = InvoiceId of string

type UnpaidInvoice = {
	InvoiceId : InvoiceId
	// Other relevant fields...
}

Equality Implementation

Of course the first thing that may come to mind to a C# developer would be to override the Equals and GetHashCode method:

[<CustomEquality; NoComparison>]
type Contact = {
	ContactId : ContactId
	PhoneNumber : PhoneNumber
	EmailAddresss : EmailAddress
}
with
override this.Equals(obj) =
	match obj with
	| :? Contact as c -> this.ContactId = c.ContactId
	| _-> false
override this.GetHashCode() =
	hash this.ContactId

This approach is common in OO designs, but by changing the default equality behaviour silenty, it can trip you up on occasion.

Therefore, an (often preferable) alternative is to disallow equality testing on the object altogether by adding a NoEquality type annotation:

[<NoEquality; NoComparison>]
type Contact = {
	ContactId : ContactId
	PhoneNumber : PhoneNumber
	EmailAddresss : EmailAddress
}

Now the compiler will give you an error if you try to compare two contacts.

This approach has the the advantage of removing the ambiguity about what equality means.

Immutability

But you might say "hold on for a sec, is not true that F# data are immutable by default?". You're right but you can still return a new version of the data you are so that the immutability is no longer a problem (keep in mind that it can be however a source of concerns performance-wise).

Example:

let initialPerson = {PersonId=PersonId 42; Name="Jospeh"}

// can use the with keyword to make a copy with just changing some fields
let updatedPerson = {initialPerson with Name="Joe"}

// Another and better way is to just be explicit:
// Use a dedicated function returning a copy
// type UpdateName = Person -> Name -> Person

Note

The distinction between value objects and entities is context-dependent!!!:

Example:

  • when building a phone, each phone has its own identity
  • when they are being sold, the serial number isn't necessarily relevant

Aggregates

  • Collection of domain objects that can be treated as a single unit. The top-level entity acting as the "root". Others entities should only the aggregate by its identifier, which is the root identifier.

  • All changes to objects inside an aggregate must be applied via the top level to the root and the aggregate acts as a consistency boundary to ensure that all of the data inside the aggregate is updated correctly.

  • It's an atomic unit of persistence, database transactions and data transfer.

Consistency

The aggregate root enforces consistency, whenever you need changes the root, you're also going to check all the other dependencies.

References

Do not:

type Order = {
	OrderId : OrderId
	Customer : Customer
	OrderLines : Orderline list
	// etc.
}

But:

type Order = {
	OrderId : OrderId
	CustomerId : CustomerId
	OrderLines : OrderLine list
	// etc.
}

Ripple effect of immutability, do not store the whole customer but "an actual reference" to it.

Integrity of Simple Values

How to create bounded values? => Use of private constructor in a specific module containing a factory function.

// private constructor to the current module
type UnitQuantity = private UnitQuantity of int

module UnitQuantity =

  let create qty = 
  if qty < 1 then
    Error "UnitQuantity cannot be negative"
  else if qty > 100
    Error "UnitQuantity cannot be more than 1000"
  else
    Ok (UnitQuantity qty)

However, pattern matching can no longer apply (cannot extract the wrapped data)... => One workaround is to define a separate value function also in the UnitQuantity module.

let value (UnitQuantity qty) = qty

// The function above can be used like below:
let unitQtyResult = UnitQuantity.create 1

match unitQtrResult with
| Error msg ->
  printf "Failure, Message is %s" msg
| Ok uQty ->
  printfn "Success. Value is %A" uQty
  let innerValue = UnitQuantity.value uQty
  printfn "innerValue is %i" innerValue

Note that the constructors code can be moved to an helper module to reduce repetition.

Units of Measure

F# supports units of measure

[<Measure>]
type kg

[<Measure>]
type m

// Note that most SI units are already available in the:
// Microsoft.FSharp.Data.UnitSystems.Si namespace
let fiveKilos = 5.0<kg>
let fiveMeteres = 5.0<m>

// Therefore the definition below won't work
// and the compiler will cringe at you
fiveKilos = fiveMeters

// Same also below cause cause not all the same unit:
let listOfWeights = [
  fiveKilos
  fiveMeters
]

// Can define kg quantity though:
type KilogramQuantity = KilogramQuantity of decimal<kg>

Enforcing Invariants with the Type System

Want to make sure that a list is no empty, easy, just enforce it with the type system:

type NonEmptyList<`a> = {
  First: `a
  Rest: `a list
}

Of course, you need some additional functions for add remove and so on to make it fully functional but the gist is here.

Capturing Business Rules in the Type System

When domain experts talk about "verified" and "unverified" emails, you should always enforce them in the code as different types.

// This is wrong (albeit that thing below does compile)
type CustomerEmail = {
  EmailAddress : EmailAddress
  IsVerified : bool
}

// This is better but does not prevent creating the Verified case 
// by passing an unverified EmailAddress
// Therefore it's even better to have a check from the mail verification service
type CustomerEmail = 
  | Unverified of EmailAddress
  | Verified of EmailAddress

Making Illegal Sates Unrepresentable in Our Domain

Document that in addition to the verfied and unverfied cases detailed previously, validated and unvalidated cases do also exist, again with a private constructor and an address validation service down to to chain.

Consistency

Note:

  • First, consistency is a business term
  • Second, always context-dependent

Consistency should be ensured at the aggregate top-level type (eg. Order instead of OrderLine), example:

let changeOrderLinePrice order orderLineId newPrice =
  // Find orderLine in order.OrderLines using orderLineId
  let orderLine = order.OrderLines |> findOrderLine orderLineId
  
  // Make a new version of the OrderLine with new Price
  let newOrderLine = {orderLine with Price = new newPrice}

  // Create new list of lines, replacing old line with new line
  let newOrderLines = 
    order.OrderLines |> replaceOrderLine orderLineId newOrderLine
  
  // Make a new AmountToBill
  let newAmountToBill = newOrderLines |> List.sumBy (fun line -> line.Price)

  // Make a new version of the order with the new lines
  let newOrder = {
    order with
      OrderLines = newOrderLines
      AmountToBill = newAmountToBill
  }

  newOrder

The Workflow Input

Input of a workflow: always a domain object, eg. UnvalidatedOrder.

type UnvalidatedOrder = {
  OrderId : string
  CustomerInfo : UnvalidatedCustomerInfo
  ShippingAddress : UnvalidatedAddress
  // etc.
}

Command as Input

Actually not the domain object per say but rather the command carrying it, eg. PlaceOrder.

type PlaceOrder = {
  OrderForm : UnvalidatedOrder
  Timestamp: DateTime
  UserId: string
  // etc.
}

Sharing Common Structures Using Generics

Let's encourage a bit of code reuse.

type Command<`data> = {
  Data: `data
  Timestamp: DateTime
  UserId: string
  // etc.
}

// Example
type PlaceOrder = Command<UnvalidatedOrder>

Combining Multiple Commands in One Type

In some cases, all commands for a bounded context will be sent on the same input channel (eg. message queue), one solution is to create a choice type:

type OrderTakingCommand =
  | Place of PlaceOrder
  | Change of ChangeOrder
  | Cancel of CancelOrder

Modeling an Order as a Set of States

Example:

Unprocessed Order Form:

  • Unvalidated Order:
    • Validated Order:
      • Priced Order
      • Invalid Order
  • Unvalidated Quote:
    • ...

One way to model the domain is to create a type for each state of the order

type ValidatedOrder = {
  OrderId : OrderId
  CustomerInfo : CustomerInfo
  ShippingAddress : Address
  BillingAddress : Address
  OrderLines : ValidatedOrderLine list
}

type PricedOrder = {
  OrderId: //...
  CustomerInfo : CustomerInfo
  ShippingAddress : Address
  BillingAddress : Address
  // Below: different from ValidatedOrder
  OrderLines : PricedOrderLine list 
  AmountToBill : BillingAmount
}

type Order = 
  | Unvalidated of UnvalidatedOrder
  | Validated of ValidatedOrder
  | Priced of PricedOrder
  // etc.

State Machines

Why Use State Machines?

  • Each state can have different allowable behaviour
  • All the states are explicitly documented
  • It is a design tool that forces you to think about every possibility that could occur

How to Implement Simple State Machines in F#

Approach:

  • make each state having its own type, which stores the data that is relevant to that state (if any).
  • the entire set of states (ie. state machine) can then be represented by a choice type with a case for each state
  • the command handler is then represented by a function that accepts the state machine and returns a new version of it (updated choice type)

Example:

type Item = // etc. 
type ActiveCartData = { UnpaidItems: Item list }
type PaidCartData = { PaidItems: Item list; Payment: float }
type ShoppingCart = 
  | EmptyCart // no data
  | ActiveCart of ActiveCartData
  | PaidCart of PaidCartData

let addItem cart item = 
  match cart with
  | EmptyCart ->
    // Create a new active cart with one item
    ActiveCart { UnpaidItems = [item] }
  
  | ActiveCart { UnpaidItems = existingItems } ->
    // Create a new ActiveCart with the item added
    ActiveCart { UnpaidItems = item :: existingItems }
  
  | PaidCart _ ->
    // Ignore
    cart

Or say we want to pay for the itmes in the cart. The state transition function makePayment takes a ShoppingCart parameter and the payment information, like this:

let makePayment cart payment = 
  match cart with
  | EmpptyCart -> 
    // ignore
    cart

  | ActiveCart {UnpaidItems=existingItems} ->
    // Create a new PaidCart with the payment
    PaidCart {PaidItems = existing; Payment=payment}

  | PaidCart _ ->
    // ignore
    cart

The result is a new ShoppingCart that might be in the "Paid" state, or not (in the case that it was already in the "Empty" or "Paid" states).

You can see that from the caller's point of view, the set of states is treated as one thing for general manipulation (the ShoppingCart), but when processing the event internally, each state is treated separately.

Modeling Each Step in the Workflow with Types

The Validation Step

Example:

type ValidateOrder = 
  CheckProductCodeExists    // dependency
    -> CheckAddressExists   // dependency
    -> UnvalidatedOrder     // input
    -> Result<ValidatedOrder, ValidationError> // output

The Pricing Step

Example:

// The function always succeeds...
// so no need to return a Result
type PriceOrder = 
  GetProductPrice         // dependency
    -> ValidatedOrder     // input
    -> PricedOrder        // output

The Acknowledge Order Step

Example:

type AcknowledgeOrder = 
  CreateOrderAcknowledgmentLetter      // dependency
    -> SendOrderAcknowledgment         // dependency
    -> PricedORder                     // input
    -> OrderAcknowledgmentSent option  // output

Creating the Events to Return

Example:

type PlaceOrderEvent =
  | OrderPlaced of OrderPlaced
  | BillableOrderPlaced of BillableOrderPlaced
  | AcknowledgmentSent of OrderAcknowledgmentSent

type CreateEvents =
  PricedOrder -> PlaceOrderEvent list

Documenting Effects

Effects like: AsyncResult

Effects in the Validation Step

Example:

type ValidateOrder = 
  // dependency
  CheckProductCodeExists    
    // Now an AsyncResult dependency
    -> CheckAddressExists   
    // input
    -> UnvalidatedOrder     
    // output
    -> AsyncResult<ValidatedOrder, ValidationError list> 

Effects in the Pricing Step

Example:

type PricingError = PricingError of string
type PriceOrder = 
  GetProductPrice                         // dependency
    -> ValidatedOrder                     // input
    -> Result<PricedOrder, PricingError>  // output

Effect in the Acknowledge Step

Example:

type AcknowledgeOrder = 
  // dependency
  CreateOrderAcknowledgmentLetter      
    // Async dependency
    -> SendOrderAcknowledgment         
    // input
    -> PricedORder                     
    // output
    -> AsyncResult<OrderAcknowledgmentSent option>  

Composing the Workflow from the Steps

Definitions (examples above) for all the steps... but not so simple... dufferent types for input and output... We'll fix that later on.

Are Dependencies Part of the Design?

Hidden or shown?

Guidelines:

  • For functions exposed in a public API, hide dependency information from callers
  • For functions used internally, be explicit about their dependencies

Pipeline

Separate public API and internal steps.

Long-Running Workflows

Split them into smaller workflows...

Functions, Functions, everywhere

Guess what... F# is a functional language, ok let's move on...

Functions Are Things

Functions in F# are HOF... no kidding!

Treating Functions as Things in F#

Example:

let plus3 x = x + 3           // plus3 : x:int -> int
let times2 x = x * 2          // times2 : x:int -> int 
let square = (fun x -> x * x) // square : x:int -> int
let addThree = plus3          // addThree : (int -> int)

let listOfFunctions = 
  [addThree; times2; square]

for fn in listOfFunctions do
  let result = fn 100
  printfn "If 100 is the input, the output is %i" result

let is an important keyword...

let myString = "Hello"
let square x = x * x
// equivalent to let square = fun (x -> x * x)

Functions as Input

Example:

let evalWith5Then2 fn = 
  fn(5) + 2

let add1 x = x + 1
evalWith5Then2 add1
// hint: output is equal to 8...

let square = x * x
evalWith5Then2 square
// hint: output is equal to 27...

Functions as Output

Example:

let adderGenerator numberToAdd =
  // return a lambda
  fun x -> numberToAdd + x

// Alternative implementation below:
let adderGenerator numberToAdd = 
  // defined a nested ineer function
  let innerFn x = 
    numberToAdd + x
  
  return innerFn

Currying

Convert a multiparameter function into a single parameter function

Example:

let add x y = x + y
let adderGenerator x = fun y -> x + y

Actually, in F#, every function is a curried function! That is, any two-parameter function with signature a -> b -> c can also be interpreted as a one-parameter function that takes an a and return a function like (b -> c).

Partial Application

Application of currying...

Example:

let sayGreeting greeting name = 
  printfn "%s %s" greeting name

let sayHello = sayGreeting "Hello"
let sayGoodbye = sayGreeting "Goodbye"

Total Functions

Exceptions are not good...

Example:

let twelveDividedBy n =
  match n with
  | 6 -> 2
  | 5 -> 2
  | 4 -> 3
  | 3 -> 4
  | 2 -> 6
  | 1 -> 12
  | 0 -> ???

First Alternative: non-zero type

type NonZeroInteger = 
  // Defined to be constrained to non-zero ints
  // Add smart constructor, etc.
  private NonZeroInteger of int

// Usage
let twelveDividedBy n =
  match n with
  | 6 -> 2
  | 5 -> 2
  // etc. 0 can't be handled so won't cause any trouble

Second Alternative: option return

let twelveDividedBy n =
  match n with
  | 6 -> 2
  | 5 -> 2
  // etc.
  | 1 -> 12
  | 0 -> None

Voila ~~

Compositions

Composition in F#

Example:

// Dummy Example 1
let add1 x = x + 1
let square = x * x

let add1ThenSquare x = 
  x |> add1 |> square

// Dummy Example 2
let isEven x = 
  (x % 2) = 0

let printBool x =
  sprintf "value is %b" x

let isEvenThenPrint x = 
  x |> isEvent |> printBool

Building an Entire Application from Functions

Well, it's doable, just split big functions into smaller ones, composition and all that good stuff.

Challenges in Composing Functions

Mostly mismatching types... just add functions in between for conversion purposes...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment