Notes on the book Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#.

Understanding the domain

A developer’s job is to solve a problem through software.

define a shared model

  • focus on business events and workflows rather than data structures
  • partition the problem domain into smaller subdomains
  • create a model of each subdomain in the solution
  • develop a common language (known as the “Ubiquitous language”) that is shared between everyone involved

Business events

The value of the business is created in the process of transforming data.

Bounded Contexts

We therefore need to create a distinction between a “problem space” and a “solution space,” and they must be treated as two different things. To build the solution we will create a model of the problem domain, extracting only the aspects of the domain that are relevant and then re-creating them in our solution space

bounded contexts as autonomous software components

In general, an event used for communication between contexts will not be just a simple signal but will also contain all the data that the downstream components need to process the event.

trust boundaries and validation

The perimeter of a bounded context acts as a “trust boundary.” Anything inside the bounded context will be trusted and valid, while anything outside the bounded context will be untrusted and might be invalid.

The job of the output gate is different. Its job is to ensure that private information doesn’t leak out of the bounded context, both to avoid accidental coupling between contexts and for security reasons.

Contracts between bounded contexts

  • A Shared Kernel relationship is where two contexts share some common domain design, so the teams involved must collaborate.
  • A Customer/Supplier or Consumer Driven Contract relationship is where the downstream context defines the contract that they want the upstream context to provide.
  • A Conformist relationship is the opposite of consumer-driven. The downstream context accepts the contract provided by the upstream context and adapts its own domain model to match.
  • Anti-Corruption Layer in DDD terminology, often abbreviated as “ACL.” In the diagram above, the “input gate” often plays the role of the ACL—it prevents the internal, pure domain model from being “corrupted” by knowledge of the outside world.

build a context map with relationships

workflow within a bounded context

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

avoid domain events within a bounded context

code structure within a bounded context

use onion architecture ~= clean architecture

keep IO at the edges


Understanding Types

represent a domain with ADTs

  • AND types = product types = records
  • OR types = SUM types = enums

Domain modeling with types

Use type systems to capture the domain model accurately and can be read and understood by domain experts.

simple types + sum/product types + functions

functions = workflows

Integrity and Consistency in the Domain

parse, don’t validate https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/

lots of examples…

Capturing Business Rules in the Type System

ex.

type​ CustomerEmail = ​  | Unverified ​of​ EmailAddress ​  | Verified ​of​ VerifiedEmailAddress ​// different from normal EmailAddress​

type​ SendPasswordResetEmail = VerifiedEmailAddress -> ...

Making Illegal States Unrepresentable in Our Domain

Consistency

consistency = atomicity of persistence

Consistency between different context is hard, use eventual consistency:

  • do nothing
  • retry
  • compensating action

only update one aggregation per transaction

Modeling Workflow as Pipelines

We’ll create a “pipeline” to represent the business process, which in turn will be built from a series of smaller “pipes.” Each smaller pipe will do one transformation, and then we’ll glue the smaller pipes together to make a bigger pipeline. This style of programming is sometimes called “transformation-oriented programming.”

Following functional programming principles, we’ll ensure each step in the pipeline is designed to be stateless and without side effects, which means each step can be tested and understood independently. Once we have designed the pieces of the pipeline, we’ll just need to implement and assemble them.

  • Command as input
  • Sharing Common Structures Using Generics
  • Combining Multiple Commands in One Type
  • Modeling an Order as a Set of States https://blog.yoshuawuyts.com/state-machines/
  • Adding New State Types as Requirements Change
  • Modeling Each Step in the Workflow with Types
  • Creating the Events to Return
  • Documenting Effects with signature
  • Composing the workflow from the steps

Long-running workflow: Sagas

Implement the model

Understand functions

basic functional programming tutorial

Implementation: Composing a Pipelien

  • Some functions have extra parameters that aren’t part of the data pipeline but are needed for the implementation—we called these “dependencies”
  • We explicitly indicated “effects” such as error handling by using a wrapper type like Result in the function signatures. But that means that functions with effects in their output cannot be directly connected to functions that just have unwrapped plain data as their input.

Implementation: Working with Errors

Using the result type to make errors explicit. bind and map.

Serialization

persistence: state that outlives the process that created it Serialization: the process of converting from a domain-specific representation to a representation that can be persisted easily.

Persistence

  • Push persistence to the edge
  • CQRS
  • Bounded Contexts must own their data storage