Skip to content

Instantly share code, notes, and snippets.

@xpepper
Created January 26, 2025 15:17
Show Gist options
  • Save xpepper/2a138d045a7a99d05a831cf3a9468a6c to your computer and use it in GitHub Desktop.
Save xpepper/2a138d045a7a99d05a831cf3a9468a6c to your computer and use it in GitHub Desktop.

Revisions

  1. xpepper created this gist Jan 26, 2025.
    70 changes: 70 additions & 0 deletions dmmf_chapter6_create_types_aggressively.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,70 @@
    One thing I’m getting by reading chapter 6 is that we typically create very few types for describing and modeling a business domain, while one of the key learnings I got from this chapter that we should aggressively create types to capture differences and nuances, and to properly describe the different states of business workflows.
    (e.g. not an `EmailAddress` but a `UnverifiedEmailAddress` and a `VerifiedEmailAddress`)

    Chapter 6 is all about **using types as a way to explicitly model and describe the domain in a precise and meaningful way**. This means creating types to capture subtle differences and distinctions in your domain that are often overlooked when using a less type-focused approach.

    So, **yes, we should aggressively create types when modeling a business domain.** This isn’t about creating types for the sake of complexity but rather about creating types to represent the *real-world states and constraints* of your business workflows in a clear, unambiguous way. This helps make your code more expressive, safer, and easier to reason about.

    ### Why create many types?

    1. **Clarity in the model**
    A type like `UnverifiedEmailAddress` explicitly communicates that this email address hasn’t been verified yet, whereas a simple `EmailAddress` type might leave this ambiguous. The distinction helps convey intent and makes the model align with the business logic.

    2. **Avoid invalid states**
    By creating specific types for different states or roles, you prevent bugs where invalid states might accidentally occur. For example, if you only allow `VerifiedEmailAddress` in certain workflows (e.g., sending emails), you can enforce this at compile time, preventing runtime errors.

    3. **Alignment with the domain**
    Business workflows are full of nuanced states and transitions (e.g., unverified vs. verified emails, pending vs. completed orders). Modeling these explicitly makes the software closely mirror the real-world domain, which is a core goal of Domain-Driven Design.

    ---

    ### Examples from the book: Unverified vs. Verified Email

    Scott gives a great example of distinguishing between an unverified and a verified email address:

    ```fsharp
    type UnverifiedEmailAddress = UnverifiedEmailAddress of string
    type VerifiedEmailAddress = VerifiedEmailAddress of string
    ```

    This small change captures the lifecycle of an email address: it starts as unverified and must go through a verification process before it becomes usable. By defining these two separate types, you ensure:
    - The application can’t accidentally treat an unverified email as if it were verified.
    - Functions that rely on a `VerifiedEmailAddress` explicitly document their requirements, improving readability and safety.

    ### A Workflow Example: Order Status

    Imagine a workflow for an e-commerce order. It can be in one of several states:
    1. **Pending** (just created but not paid)
    2. **Paid** (payment received but not shipped)
    3. **Shipped** (on its way to the customer)
    4. **Delivered** (received by the customer)

    We can model these states using types:

    ```fsharp
    type PendingOrder = { OrderId: int; Items: string list }
    type PaidOrder = { OrderId: int; Items: string list; PaymentDate: System.DateTime }
    type ShippedOrder = { OrderId: int; Items: string list; ShippingDate: System.DateTime }
    type DeliveredOrder = { OrderId: int; Items: string list; DeliveryDate: System.DateTime }
    ```

    This approach creates a *state machine* where it’s impossible to mistakenly process an order in the wrong state. For example:
    - You can only ship a `PaidOrder`, not a `PendingOrder`.
    - Functions like `ShipOrder` or `DeliverOrder` explicitly state their inputs and outputs:
    ```fsharp
    let shipOrder (paidOrder: PaidOrder): ShippedOrder =
    { OrderId = paidOrder.OrderId
    Items = paidOrder.Items
    ShippingDate = System.DateTime.Now }
    ```

    ### Why this might feel unusual

    If you're used to object-oriented programming or less strict approaches to types, you might feel that creating lots of types is "too much work" or "overkill." But in functional programming, **types are lightweight tools for expressing intent**, and creating them is far less cumbersome than you might think. You’re trading off a small upfront cost for significant long-term benefits, like:
    - Fewer bugs.
    - Easier maintenance.
    - More understandable code for your team.

    ### Key takeaway

    By **aggressively creating types**, you’re not over-complicating your code—you’re creating a model that mirrors the real-world domain. This gives you a precise and safe way to represent business rules, ensures workflows are consistent, and reduces invalid states. So, yes, **embrace creating nuanced types**. It might feel unusual at first, but it’s one of the most powerful tools for building robust software that matches your domain.