Overview

6 Implementing the Domain Model

This chapter explains how to turn a carefully designed data model into Java code without losing the benefits of the model. Its central idea is determinism: separating the non-deterministic work of getting data from the deterministic work of transforming it. Pure functions take all required data as input, return data as output, and avoid side effects; impure functions interact with time, services, databases, mutable state, logging, or other parts of the environment. By pushing dependencies outward and making business logic deterministic wherever possible, code becomes easier to reason about, easier to reuse, and much simpler to test.

The chapter shows how this principle shapes both small implementation choices and larger architecture. Deterministic functions can be treated almost like lookup tables: the same inputs always produce the same outputs, so their behavior is bounded by their types. In Java, purity cannot be enforced directly, but conventions such as using static methods for deterministic functions can signal intent and reduce accidental coupling to instance state. The implementation is organized around a deterministic core surrounded by a non-deterministic shell: the shell loads data, calls services, saves results, and handles side effects, while the core performs the business transformations. This arrangement lets dependencies flow inward toward the core, never outward from it.

The Late Fees example demonstrates the approach in practice. The deterministic core implements steps such as collecting past-due invoices, building a draft late fee, computing totals and due dates, applying grace periods, and assessing whether a fee is billable, not billable, or needs approval. Along the way, the chapter emphasizes listening to type-system friction, introducing more types when they clarify domain meaning, and choosing code style based on communication rather than dogma. The outer shell then coordinates billing, approvals, persistence, and translation back into existing entity types. The result is a feature whose domain logic is explicit, testable, and flexible, with well-formed data acting as the interface between components and allowing future architectural changes with less disruption.

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.
  • 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
  • Functions model relationships between two pieces of data
  • 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 implementation principle introduced in Chapter 6?The chapter’s central principle is to organize implementation around determinism. The author argues that code becomes easier to reason about, reuse, and test when we separate how we get data from what we do with it. The “what” can often be implemented as pure, deterministic functions: values in, values out, with no side effects.
What is the difference between a pure function and an impure function?A pure function deterministically maps inputs to outputs and has no observable side effects. Given the same inputs, it always returns the same output and does nothing else. An impure function may read from the environment, call services, use mutable state, log, access the current time, generate random values, or otherwise interact with the outside world.
Why does the chapter recommend separating “how we get data” from “what we do with it”?Because “how we get data” often involves non-deterministic operations such as database calls, service calls, current time, or mutable state. By moving those operations outside the business logic and passing their results in as plain inputs, the business logic can become deterministic. This makes it simpler to test, since tests can use ordinary data instead of mocks, stubs, or complicated setup.
Why does the author use static methods to signal deterministic functions in Java?Java does not enforce purity, so the author uses static methods as a convention. A static method cannot directly access instance state, which adds friction against accidentally introducing non-deterministic dependencies. This is not a perfect guarantee, because static methods can still call things like LocalDate.now(), but it is a useful team convention and code-review signal.
How do deterministic functions improve testing?Deterministic functions reduce tests to a simple pattern: provide known inputs and assert expected outputs. There is usually no need for mocks, test doubles, service stubbing, database setup, or controlling global state. For example, instead of mocking LocalDate.now() and a contracts API, a due-date function can simply accept LocalDate and PaymentTerms as arguments.
What is the “deterministic core” and “non-deterministic shell” architecture?It is an architectural pattern where deterministic business logic lives in an isolated core, while side effects and environmental interactions live in an outer shell. The shell loads data, calls APIs, saves results, and handles I/O. The core receives data as inputs and returns data as outputs. Dependencies flow inward: the shell can depend on the core, but the deterministic core must not depend on the shell.
How is the Late Fees feature divided between deterministic and non-deterministic code?The deterministic core contains functions such as collectPastDue, buildDraft, assessDraft, dueDate, gracePeriod, and fee calculation helpers. These work from supplied data. The non-deterministic shell contains operations such as loadInvoicingData, submitBill, startApproval, and save, because these interact with time, repositories, billing APIs, approval APIs, or persistence.
Why does the chapter model Grace Period as a function instead of just a number of days?Grace Period is not always a fixed number of days. Customers in good standing receive 60 days, acceptable customers receive 30 days, but poor-standing customers must pay by the end of the month. That last rule depends on the date being adjusted. So the chapter models Grace Period as a relationship from one date to another, using Java’s TemporalAdjuster. This lets the code express the domain concept more precisely.
Why does the author prefer a switch expression over a Map for some business rules?A Map can elegantly represent a lookup table, but it can also be missing keys. That means the program must handle impossible or domain-invalid cases at runtime, such as a missing grace period. A switch over an enum can provide compile-time exhaustiveness checks, so if a new enum case is added, the compiler can force the developer to handle it.
Why does the chapter say “data is the ultimate interface”?Well-designed data types create seams in the application. Because each stage of the workflow communicates through explicit data, pieces such as billing, approvals, draft assessment, and persistence can be rearranged more easily. They can later be moved behind queues, run in parallel, split into services, batched, retried, or wrapped in transactions. Data acts as a stable interface between architectural components.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Data-Oriented Programming in Java ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Data-Oriented Programming in Java ebook for free