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.
-
Clarity in the model
A type likeUnverifiedEmailAddress
explicitly communicates that this email address hasn’t been verified yet, whereas a simpleEmailAddress
type might leave this ambiguous. The distinction helps convey intent and makes the model align with the business logic. -
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 allowVerifiedEmailAddress
in certain workflows (e.g., sending emails), you can enforce this at compile time, preventing runtime errors. -
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.
Scott gives a great example of distinguishing between an unverified and a verified email address:
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.
Imagine a workflow for an e-commerce order. It can be in one of several states:
- Pending (just created but not paid)
- Paid (payment received but not shipped)
- Shipped (on its way to the customer)
- Delivered (received by the customer)
We can model these states using types:
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 aPendingOrder
. - Functions like
ShipOrder
orDeliverOrder
explicitly state their inputs and outputs:let shipOrder (paidOrder: PaidOrder): ShippedOrder = { OrderId = paidOrder.OrderId Items = paidOrder.Items ShippingDate = System.DateTime.Now }
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.
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.