Notes on Domain Modeling Made Functional
March 26, 2022
I’ll divide my notes from Domain Modeling Made Functional into
four main sections: fundamentals of
domain-driven design; functional architectures; domain modeling; and high-level takeaways.
I’ll include page numbers where applicable in case you’ve purchased the
book and want to use these notes as a reference.
Overall, I greatly enjoyed this book, and I’d likely advocate for using
its guidance if I become an architect of an enterprise software project
in the future. In practice, I plan to use its guidance on domain
modeling quite heavily: I want to understand the domain of freight
forwarding (and specifically, warehousing) deeply enough such that I can
model the core components effectively in software. In addition, I will
continue to look for ways to apply the principles of functional
programming (particularly around designing programs as pipleines)
despite my usage of object-oriented languages in my day-to-day (Ruby,
JavaScript, and Java).
Fundamentals of domain-driven design (DDD)
-
Fundamental components of DDD (p. 22)
-
Bounded contexts: subsystems with clear boundaries, each
of which own their data stores. These systems consist of
workflows, and each has its own contextual language.
-
Domain events: generated via techniques such as
event storming
(and related to
event sourcing)
- Commands
-
Ubiquitous: a language with nouns — data structures — and
verbs — business processes — that is shared between all
stakeholders in a project. These terms should be maintained in a
living document.
Functional architecture
-
What are the main components of a functional architecture? (pp. 43-55)
-
Bounded contexts as autonomus subsystems with well-defined [data]
boundaries. These contexts could be implemented in a
variety of ways, depending on the scale of the system.
-
Could be simple software modules/classes, separate
network-isolated services, or even microservices.
-
In general: start with a monolith and refactor into services
at sufficient scale.
-
Components stay decoupled when communicating (via events/message
queues), or simple function calls
-
Contracts between bounded contexts is a hard concept to get right,
but incredibly important (p. 48)
-
Code structure within a bounded context:
onion architecture
(p. 52)
Domain modeling
-
The idea of documentation as compilable code — i.e. types —
is the central to domain modeling. (p. 57)
-
Type patterns (p. 78)
-
Simple values (e.g. identifiers, messages). These values
are often constrained in the domain. For example, if there are
only 100 products in a catalog, an integer-based product code
could only go up to 100.
-
AND combinations. These types represent closely linked
data (i.e. documents), and are implemented with records.
-
Choices with OR. Represents a choice between this
or that.
-
Workflows. Business processes with well-defined inputs
and outputs. Often, these workflows cause side effects. (Workflows
correspond to functions.)
-
Value Objects vs. Entities
for classifying data(p. 88)
-
Value Objects: objects without a
persistent identity. For instance, if the fact that the fields of
two objects are the same means the “value” of those objects are
the same, you’re dealing with value objects.
-
Entity: objects with a persistent
identity. Often, these objects are documents of some kind. They
have a unique identity even if their components (fields) change.
-
Aggregate: collection of entities, where the top-level
entity is called the aggregate root. Aggregates enforce
consistency and invariants in the domain: if an entity in its
collection changes, it will reflect those changes across other
associated entites in the domain.
-
Core principle:
make illegal states unrepresentable
in the domain via integrity and consistency (p. 103)
-
In distributed systems, we should be able to trust that
“eventual consistency”
will get the system to the state we expect in spite of transient
failures.
-
Workflows implemented as
pipelines
(p. 119)
-
Data is passed from one function to another, in the form of a
pipeline. (And more concretely, using the pipeline operator in
FP.)
-
State machines in particular can be highly useful for modeling
workflows.
- Idea: documenting side effects in function type signatures.
Takeaways
-
F# is a great language for DDD: it has useful primitives such as
algebraic data types, modules, map/bind methods, Result/Async type
wrappers, and computation expressions.
-
I don’t believe that the third section of the book in particular,
since it was so implementation-heavy, is immediately useful to
most developers if there is already lots of code in a
different language. (Perhaps if the existing language is
part of the JVM, though, this assertion might not hold.)
-
I like to think of domain types as an instantiation of the
ubiquitious language.
-
However, I don’t think this book dove as deep into requirements
gathering as I would have liked. Without understanding the domain,
you can’t attempt to domain model.
-
With rigorous and intentional domain modeling, I can’t imagine the
design getting out-of-sync with the code nor the real world
requirements.
-
Pushing input/output to the boundaries of a workflow/application can
make business logic significantly easier to reason about. More
practically, I think this pattern can be implemented in simple method
calls as well: push logs, jobs, etc. to the very end of void
(unit-returning) methods to make it crystal-clear where the side
effects are occuring. Burying logs or other side effects in helper
functions can make a system’s behavior incrementally more difficult to
reason through (not to mention harder to test).