6 Implementing the Domain Model
In this chapter, the invoice domain model from the previous chapter is turned into working Java code by using determinism as the main organizing principle. The author argues that methods should be treated as either pure, mathematical functions or impure operations, because pure functions are easier to reason about, test, and reuse. This leads to a deliberate separation between code that transforms data and code that touches the outside world, with the goal of protecting the domain logic from side effects and incidental complexity.
The core implementation is built by following the data model closely: first collect past-due invoices, then build a draft fee, then assess that draft against business rules. Each step is shaped by its types, and many functions become short, straightforward mappings from inputs to outputs. The chapter also shows how richer domain meaning can emerge from the type system itself, such as modeling grace periods as a function rather than a simple number, and how exhaustive pattern matching can preserve important business distinctions better than collapsing everything into a map or a vague conditional.
Once the deterministic core is established, the remaining impure shell handles loading data, calling billing and approvals services, and saving results. The chapter emphasizes that even when integrating with legacy entities, the domain model can stay isolated by converting to and from existing structures in a deterministic way. This creates clear seams in the architecture, improves flexibility, and makes future refactorings easier, while reinforcing the broader lesson that data shapes the interface of the program and can guide both implementation and system design.
Mathematical functions assign inputs to outputs
The state tracking that’s often required when reading Java code
Everything we need to know is in one spot
Comparing the patterns between testing methods and functions
Behind every function is a lookup table of its inputs and outputs
The relationship between immutable data and functions
Deterministic functions can be replaced with data without changing your program’s result
The flexibility that replacing computation with data brings
Returning a function as data
Dividing the world in two
Building around a deterministic core
The deterministic core
Is this where we should find out if the invariant holds?
All that’s left to implement
The sandwich
How do we get out of our world so we can save our work?
Treating the Invoice Entity as a DTO
Doing everything in one big for-loop makes us sensitive to errors and service latencies
Rearranging the pieces is easy when everything speaks well-formed data
Summary
- Determinism is a powerful tool for simplifying an implementation
- “Function” has a semantic meaning independent of Java. It describes something that deterministically maps inputs to outputs.
- Everything in Java is technically a method behind the scenes (even things like Function<A,B>). We separate the semantics from the syntax when talking about functions.
- Functions model relationships between two pieces of data
- Making methods static is a useful convention for signaling that they will act as functions
- Functions have a finite “size” that’s dictated by their type signature
- We can limit possible implementations in a function by controlling its inputs and outputs
- The implementation for well-scoped functions tends to write itself. The types make it so that there’s only one possible implementation that will compile.
- Functions are behaviorally no different from a lookup table. We can freely swap between them with no effect on our program
- Functions are just data. We can take them as input and return them as output
- Use caution when trading Functions for Maps. It trades compile time guarantees for runtime assumptions
- Don’t get caught up in purity. “Basically deterministic” is close enough to “actually deterministic” for most programming.
- Organize your implementation around deterministic and non-deterministic functionality
- Non-Determinism makes everything harder. Refactor towards deterministic code.
- Types capture important invariants we know about the domain. This will create friction with the parts of your code that work on assumptions.
- Let uncomfortable disagreements over types drive your codebase in a better direction
- Not every disagreement is an emergency. Be measured. Often annotating and moving on is the best strategy
- Don’t get wrapped up in the Optional holy war. Focus on what your code communicates
- How we interact with Optional can control where we draw our reader’s attention
- It’s OK to introduce new types! As many as you need! Anywhere you need them!
- Code clarity trumps all other concerns
- The exchange points between our feature and the rest of the world will always be the trickiest. Give them plenty of thought.
- Treating the Entities outside of our feature as DTOs lets us reuse all of the existing ORM machinery until we’re ready to promote our own
- Designing around data builds natural seams into our application. It makes it trivial to refactor and rearrange the individual pieces
FAQ
What is the main design principle used to organize the implementation in this chapter?
The chapter organizes the implementation around determinism: keep pure, deterministic functions in a core, and isolate non-deterministic work like I/O, clocks, and service calls in an outer shell.What is the difference between a pure function and an impure function?
A pure function deterministically maps inputs to outputs and has no side effects. An impure function may read external state, call services, log, mutate state, or otherwise behave differently depending on the environment.Why does the book recommend making deterministic methods static in Java?
static acts as a signal that the method should stay independent of instance state. It also adds friction against accidentally reaching for mutable fields or services, helping preserve determinism.Why are deterministic functions easier to test?
Because tests only need to provide inputs and assert outputs. There is usually no need for mocks, stubs, setup/teardown complexity, or controlling hidden external state.How does gracePeriod illustrate modeling with functions instead of raw data?
gracePeriod is really a relationship from customer rating to date adjustment, not just a number of days. Modeling it as a function captures that meaning more accurately than forcing it into a plain scalar value.Why does the chapter prefer a switch statement over a Map for some domain decisions?
A switch enforces exhaustiveness at compile time, while a Map relies on runtime assumptions and can silently miss keys. The chapter favors compile-time guarantees when the domain has a closed set of cases.
Data-Oriented Programming in Java ebook for free