This series explores the scalability benefits of pure domains. This post introduces important terms and ideas with some examples.
The the foundational idea of this series is a system where the business rules (or domain rule) don’t change the system state. Instead they return data and let consumers enact the corresponding change. Such a system is maximally event-driven and simplifies some difficult problems in high-scale systems.
This approach is sometimes called Functional Core Imperative Shell since the pure domain takes many ideas from functional programming while state changes are enacted imperatively at the application boundaries.
There are a few terms critical to understanding this series.
- Domain: Domain refers to our core problem or business. Domain can also refer to the “domain layer” that reflects domain rules in the software. So if were writing accounting software, our domain is the code that embodies the rules of accounting. Systems will commonly have many sub-domains. Non-domain code is incidental to the business. For example, sending emails or storing data in a database is common non-domain code. How we notify users or store data could change without changing how the business works.
- Pure: Pure refers to pure functions, which are functions that always return the same output given the same input and have no side-effects. Essentially, they don’t rely on or change the system state. They simply map inputs to outputs. Pure functions are deterministic. They always return the same output given the same input.
- Event: Events represent some change in program state. For example, the user clicked a UI element, or an item was sold.
A Pure Domain through Events
A pure domain layer is then core business rules decoupled from direct system state.
Pure domain rules can only effect system state through the data they return. The returned data is effectively an event.
A pure domain also only know about data you pass. It can’t depend on any implicit system state. The only context the pure domain can reference are the arguments to the called domain function. This data acts like a command. Commands can be thought of as client triggered events (e.g. user requests).
Commands decouple our pure domain from read state while returning events decouples the domain from writing state. Together the domain can be totally pure, thus deterministic and decoupled from hidden state.
A pure domain may be hard to picture at first. Here are some resources if you want to explore.
Domain Modeling Made Functional’s is an accessible and fantastic overview to pure domain modeling.
Mark Seemann covers this approach as Functional Architecture
Here’s a simple sample endpoint.
BlockWebsite wouldn’t persist a site block rule. Instead it returns data respresenting the change in state. In this case, the data to create a block rule. The application boundary (e.g. REST API, console application, or other clients) then passes this data to some simple function that maps the event to a persistence command (e.g. create a SQL insert statement).
This series focuses on scalability. However, there are some non-scalability benefits worth mentioning.
- Clarity: Pure functions have no side-effects. What you see is all you need to think about. This makes them easy to reason about and compose into new applications.
- Simplified Testing: Pure domains don’t require mocking. The domain no longer relies on injected dependencies, all information is contained in the input and output.
- Purity also promotes fast and reliable tests. There is no risk of slow out-of-process infrastructure.
- Dry-run domain rules: Separating decisions from enactment of state also means we can preview or dry-run possible actions safely
This post introduced key ideas like purity, events, and domains. We sampled what a pure domain might look like and highlighted some additional resources. In particular, command and event data structures isolate a domain from reading or writing state resulting in pure (thus deterministic) domain layers.
Next up we’ll dig into some more complex benefits of a pure domain.