Overview

5 Modelling Domain Behaviors

In this chapter, the author moves from simple modeling exercises into the messy reality of building a real feature in an existing Java application. The example revolves around automatically charging late fees for overdue invoices, but the point is not the billing domain itself; it is the complexity of fitting new behavior into a legacy landscape of databases, external services, ORM-backed entities, and preexisting design decisions. The chapter argues that if we start from the requirements and focus on the data that flows through the feature, we can avoid burying the business meaning inside nested conditionals, defensive checks, and framework-driven code.

The core modeling technique is to treat domain behavior as a pipeline of data transformations. Instead of thinking in terms of one big method that “does everything,” the feature is broken into clear steps: collecting past-due invoices, building a draft late fee, deciding whether the fee is billable, and then either submitting it, sending it for approval, or rejecting it. Along the way, the author shows how to invent data types that make each step explicit, including wrapper types, decision types, lifecycle states, and parameterized records. Compile-time modeling with sealed types and generics helps make illegal states harder or impossible to represent, while also making the narrative of the feature readable directly from the types.

The chapter also emphasizes tradeoffs. Some representations are semantically strong but awkward to use; others are ergonomic but push important rules back into runtime checks. Rather than chasing purity, the author advocates for clarity and for placing constraints where they are most useful, even if that means mixing immutable data with mutable legacy entities or accepting some imperfect wrappers. The chapter closes by showing how to decouple the feature from existing systems through a boundary layer that gathers and reshapes external data into a richer internal model. That internal model then becomes the real design: a compact, expressive, and safer description of the behavior, with the awkwardness of the outside world confined to the edges.

The service-oriented landscape in which our application lives
The invoice! They're what our service exists to process!
How payment terms affect invoice due date
grace periods determine when we consider an invoice "past due"
Services for every little thing
Pulling Entities Objects / Tables from the requirements
turning the Invoice info into (ORM backed) Entities
the small concessions we make when our representation is tied to persistence
Visualizing behaviors as a factory. Data in; Data out.
The new data types that need design
the challenge of modeling closely related data types without duplication
refactoring approach: extracting common fields into their own type
Another design option: refactoring to extract what’s unique
another approach: refactor to express semantic information as a type variable
The different decisions for what we do with a draft
We have to figure out how to make the two worlds talk to each other
“what’s already there” shoving its sticky fingers into our world
Refactoring buildDraft. No more “how,” just “what.”
Limiting how many fingers can reach their way into our design
Good boundaries enable good design
designing a new customer data type

Summary

  • Design is about creating the world as you want it to be, not as you found it.
  • Reuse can be harmful. Services and Entities designed for other use cases will seldom fit ours.
  • Assembling behaviors out of ill-fitting representations creates illegal states and potential bugs
  • It’s OK to introduce new data types into your system!
  • Good data-oriented design uses data types to tell a story about the requirements
  • The design process is “done” when the code looks like it’s for humans rather than machines
  • Design is iterative. The first option is just the first option. Keep exploring.
  • The same data can be represented in an endless number of ways. Try multiple factorings
  • Good representations strike a balance between semantics, ergonomics, and safety
  • Behaviors are modeled as series of factory-like data transformations. Data in; data out.
  • Ignore implementation details and work “above” the code while designing. This enables rapid iteration.
  • Simple wrapper types are a good option for capturing “something we know” about a particular state
  • Avoid the pursuit of purity. The real-world is messy. Our modeling is in pursuit of clarity
  • We can interact with identity objects from a data-oriented context as long as we’re cautious. Wrapping in a record can serve as a light quarantine for mutability.
  • The amount of type safety we employ is a design decision
  • We have a responsibility to the people who use our software. The higher the stakes, the more we should weigh moving invariants into compile time checks.
  • Generics aren’t just for collections!
  • Type variables can be used to convert runtime invariants into compile time ones
  • Good modelling can make illegal behaviors impossible to express
  • If / else conditions at runtime can be expressed as algebraic sum types at compile time
  • Decoupling decision from action lets us decide when (if at all) that action gets performed
  • A lot of care is needed when fitting our design into the rest of the world
  • Without caution, our design can be infected with “what’s already there”
  • Separate how you get the data from what you do with it.
  • Decoupling “how” from “what” brings atomicity to your data loading and simplifies failure modes
  • Use a Data Access Layer to hide the details of existing systems and vend well-formed semantic data
  • The Façade pattern is a good option for creating “Smart” Repositories / “Data Access Objects”

FAQ

What is the main idea of modeling domain behaviors in this chapter?The chapter shows how to model behaviors as data transformations: inputs flow through a pipeline of well-named types, and each step describes a domain transition clearly. This makes the behavior easier to understand, reason about, and enforce.
Why does the chapter use pipelines like List<Invoice> -> List<PastDue> -> LateFeeDraft?Because pipelines make the story of the feature explicit. Each arrow represents a transformation from one meaningful domain state to another, which helps capture requirements without immediately committing to implementation details.
Why are illegal behaviors supposed to be impossible to represent?So the compiler helps prevent misuse. By designing types so that only valid states and valid transitions can be expressed, you reduce the need for defensive runtime checks and make incorrect usage harder or impossible.
Why is the original nested if/else implementation considered problematic?It hides the business rules inside control flow and defensive checks. The code becomes difficult to read because it mixes billing rules, approval logic, persistence concerns, and validation all in one place.
What does it mean to model decisions as algebraic data types?It means turning runtime branching outcomes into explicit data types such as Billable, NotBillable, or NeedsApproval. This separates deciding from acting, and lets different interpreters handle the decision later.
Why does the chapter introduce wrapper types like PastDue?Wrapper types give a name to a meaningful domain condition, such as an invoice being past due. Even a lightweight wrapper improves readability by making the intent of the data explicit in method signatures.
Why not always make every type semantically perfect and fully self-validating?Because in real systems, some meanings depend on external context. The chapter emphasizes that data-oriented design is about clarity, not purity, so sometimes a simple wrapper is useful even if it cannot enforce everything on its own.
Why does the chapter use generics like LateFee<Draft>?Generics lift lifecycle information to the type level. That allows methods to demand exactly the state they need, such as accepting only draft late fees for assessment, which increases compile-time safety and expressiveness.
Why introduce an EnrichedCustomer type?To decouple business logic from the messy details of multiple repositories and APIs. EnrichedCustomer bundles the relevant customer-related data into one semantic object, so downstream methods can work with plain data instead of service calls.
What is the final goal of the design process in this chapter?To end up with a coherent data model that tells the whole story of late-fee processing: collecting past-due invoices, building drafts, reviewing them, billing or rejecting them, and isolating external dependencies behind clear boundaries.

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