Skip to main content

Command Palette

Search for a command to run...

What Is a Rich Domain Model?

Published
17 min read
What Is a Rich Domain Model?
L

I specialize in developing object-oriented java applications that aligns with business objectives, using Domain-Driven Design principles to ensure technical decisions drive tangible value. By focussing on a deep understanding of the business domain, I craft solutions that solve real problems while maximizing ROI. My approach evaluates the cost/profit ratio of every decision—only implementing technologies when the benefits outweigh the costs. I’ve been called in to revive stalled projects and address challenges where others have struggled. My focus is on creating software that not only meets but exceeds business expectations. Whether working with legacy systems or modern frameworks, I select the right technologies to maximize value—not just follow trends. I believe software should be a strategic asset, and this mindset guides every decision I make in development.

Most articles about rich domain models get lost in comparisons to anemic models, debates about OOP mechanics, or pattern catalogues. This is not one of those articles.

A rich domain model is not a technical pattern. It is a discipline — one that produces a living, explicit representation of the essential complexity of a business domain. Understanding what that means, and what it unlocks, requires stepping back from the code entirely.


Essential Complexity, Made Explicit

Start with what a rich domain model actually is.

It is a set of objects, each playing a defined role in the business domain, each owning the responsibility that role entails. Not what state they carry — but what they know, what they decide, and what belongs to them. Think of it less like a data structure and more like a cast of actors: each one has a role, and the role defines everything. What they are responsible for. What they know. What they act on. What they refuse.

This distinction matters more than it might seem. An actor on stage is not described by listing their costume and props. They are described by their role — what they do, what they own, what they are accountable for. The props are incidental. In the same way, a domain object is not defined by the fields it holds. It is defined by its responsibility. State may be part of how it fulfills that responsibility — but it is an implementation detail of the role, not the definition of it.

The contrast with an anemic model follows directly. An anemic model is a cast of actors who have been stripped of their roles. They stand on stage holding props while someone offstage calls out instructions. The data is visible. The knowledge of what to do with it is gone — moved into service classes, transaction scripts, and workflow configurations that grow without principle and conflict without resolution.

Fred Brooks gave us the vocabulary to understand why this matters. He distinguished between essential complexity — the complexity intrinsic to the problem itself, which cannot be removed — and accidental complexity, everything else: the frameworks, the indirections, the patterns applied without cause.

The actors and their roles are the essential complexity. They are not a representation of it or a metaphor for it — they are it, made visible and explicit. Every business rule that is genuinely hard, every lifecycle that has real consequences, every constraint that exists because the business demands it: these find their home in a role, owned by an actor, named and present in the model. You can see the essential complexity. You can point to it. You can reason about it directly.

Once the essential complexity is that explicit, accidental complexity loses its camouflage. It cannot pretend to belong. Every framework choice, every infrastructure decision, every pattern applied can be held up against a simple question: does a domain object — a named actor with a defined role — actually require this? If not, it is accidental complexity, and it has no business being there. The model makes that judgment possible because the essential complexity is no longer hiding.

This is not the same as reducing complexity. The business is as complex as it is. What changes is whether that complexity is visible, owned, and honest — or scattered, implicit, and discovered only when things break. The rich domain model ensures the essential complexity is always primary. Everything else is secondary, and known to be so.


A Tool for Learning the Domain

Most development approaches start from requirements. A user story describes motion through a system: a user does something, something happens. This teaches you the rivers — the flows, the happy paths, the scenarios that have been thought of so far.

A domain model teaches you the terrain. Once you understand the terrain, the rivers make sense. Without it, you are always following water, never knowing where you are.

This distinction matters enormously in practice. When a developer learns a business domain through user stories and debugging, they accumulate procedural knowledge. They learn symptoms. They build a mental model that is a patchwork of scenarios, edge cases, and tribal knowledge. That understanding does not transfer easily and does not survive personnel changes.

When a developer learns through the domain model — starting with the core concepts, understanding their responsibilities and relationships — they learn causes. The what and why of the business becomes clear before the how. Onboarding that previously took months can take hours, not because the business became simpler, but because its essential structure was made explicit and navigable.


Canonical Truth for the Business Domain

A codebase without a domain model has no authoritative reference for what the business believes. Logic accumulates in transaction scripts, in service classes, in stored procedures, in workflow configurations. It is never gathered in one place where you can ask: is this consistent? Does this conflict with that?

The rich domain model is that place. It is not documentation in the sense of comments or wikis — those go stale and lie. It is living documentation, expressed in code, that is wrong only when the code is wrong. When two features conflict, the domain model is the referee. When a new requirement arrives, the model is the context in which it is evaluated — is this already expressed somewhere? Does this contradict something that exists?

Without that context, conflicting logic does not just happen occasionally. It is inevitable. There is no shared reference, so there is no way to prevent divergence. The model prevents it not through process or discipline, but through the simple fact of existing.


What Belongs in the Domain Model

A common misconception is that domain objects are database rows dressed up with methods. This conflation produces models that are anemic by construction — the shape of the schema becomes the shape of the domain, and the domain becomes a mirror of the persistence layer rather than a representation of the business.

The domain object is defined by its responsibility, not by its persistence. Whether it holds state is irrelevant to whether it belongs in the model. What matters is whether it represents a genuine business concept with a defined responsibility.

Some domain objects have state that should be persisted. In that case, the ORM annotations live on the domain object itself — there is no separate entity class, no parallel representation. The domain object is the single source of truth, and persistence is simply a capability some objects happen to have. There is no ORM object that is not a domain object. If one exists, that is the smell — not a feature. Some will object that this violates persistence ignorance — that the domain should not know about its own storage. But a domain object declaring what it needs is not pollution. It is honesty. The alternative — a parallel entity class that mirrors the domain object field by field — is not cleaner architecture. It is the same information written twice, with an extra layer of indirection between them and nothing gained in return.

Other domain objects have no persistent state at all. A CurrencyConversion that owns the rules and cache for converting between currencies is a full citizen of the domain model. An Interaction that represents a session of intent against the domain — carrying the current user, the transaction boundary, the active roles — is a domain object. Neither has a table. Both have clear, defined responsibilities.

The question is never "does this have a table?" The question is always "does this represent something real in the business, with a responsibility that can be named?"


The Interaction: A Worked Example

Interaction deserves particular attention because it illustrates what correct modeling unlocks beyond the obvious.

Every non-trivial business application has the concept of an interaction: a moment of intent against the domain, initiated by a known user, within a defined transactional boundary, with a lifecycle that has a beginning and an end. This concept exists whether you model it or not. The question is whether it is explicit or scattered across framework configuration, security filters, transaction annotations, and audit log scrapers.

When modeled explicitly — made available within the execution context, whether via ThreadLocal, scoped storage, or whatever the runtime demands — Interaction becomes the natural owner of everything that belongs to that lifecycle. The storage mechanism is an implementation detail. The concept is not.

During an interaction, any part of the domain can ask Interaction.hasUserRole(CancelOrderRole.class) — not as a security check imposed from outside, but as a domain question answered where the action is performed. Authentication is resolved before the interaction begins; a valid Interaction means a valid user. Authorization is expressed where it is enforced.

At the end of an interaction, deferred actions execute within the same transactional boundary. Emails are sent, events are fired, downstream reactions trigger — and if anything fails, everything rolls back, including the email that had not yet been sent. This guarantee is structurally impossible to achieve with a message broker bolted onto the outside of an application without significant infrastructure overhead. Here it is a natural consequence of the model.

After the end of an interaction, post-transaction actions execute outside the boundary, intentionally and explicitly. On cleanup, state is torn down predictably — no leaked state between requests.

The audit trail — who did what, when, and did it succeed — emerges naturally because Interaction already knows all of it. It is not assembled from logs after the fact.

None of these capabilities were designed individually. They are all consequences of modeling the right concept. This is what essential complexity, made explicit, produces.


The Order: Lifecycle as Domain Responsibility

The same principle applies to any object with a meaningful lifecycle. Consider an Order.

Order is constructed from an OrderRequest. In its constructor — or through an assemble() method called immediately — it validates that all items have prices (failing fast if not), reserves inventory, creates the Invoice, determines from the request whether fulfillment is pickup or delivery, and if delivery, creates the Shipment internally. No external coordinator performs these steps. The Order knows what it means to be an order.

Once assembled, the Order's state gates what is possible. deliver() is only reachable because assemble() completed. Anything attached to an order — documents, notes, events — is evaluated against the current state. The object enforces its own rules.

The lifecycle of the order is expressed in OrderMilestone objects: created at LocalDateTime X, ItemsCompleted at X+1, Shipped at X+2. This is not logging in the developer sense. This is the Order remembering its own history. Audit trails, reporting, and debugging are free consequences of a model that is honest about time.

There is no OrderService that knows the steps. There is no OrderProcessor that coordinates the flow. What is often called orchestration is simply the Order's own behavior, waiting to be claimed.


There Is No Such Thing as Orchestration

"Orchestration" is a concept that appears when objects are not carrying enough responsibility. The argument is that some flows are too complex to live in any single object, that something external must coordinate. But this argument always rests on the same foundation: the objects being coordinated are anemic. They cannot coordinate themselves because they hold no behavior.

The stronger claim is this: orchestration is a business process, and every business process has an owner. The moment you ask "whose responsibility is this flow?" the answer is always a named thing in the business. Named things in the business belong in the domain model.

If the checkout flow belongs to Order, there is no orchestration — only an object doing its job. If a more complex cross-domain process exists, the business has a name for it. That name is your object.

The workflow engine question resolves the same way. A workflow engine is infrastructure for implementing an unmodelled requirement. It allows a business process to be encoded without ever being understood. The process runs, tickets close, and the pressure to model never arrives. Meanwhile the process becomes invisible — it lives in configuration, not in the domain, and the model no longer reflects reality.

By making the process explicit in the model, you force the understanding upfront. Traceability, accountability, and auditability are not bolted on afterward — they are natural consequences of a process that is owned and expressed. And the model becomes resistant to casual change. A workflow engine can be reconfigured quietly. A domain object that explicitly models a process requires intentional change. You must touch the model. That is not a constraint — it is a feature.


The Architecture That Emerges

When the domain model is honest and complete, the architecture that surrounds it becomes remarkably simple.

The domain is the center. Everything else is translation. An adapter takes an external signal — an HTTP request, a queue message, a UI event, a file drop — translates it into something the domain understands, and translates the response back. Whether that adapter is called a web service, a UI connector, or a queue client is an implementation detail. Its functional purpose is always the same: adapt an external request to the domain, and an answer from the domain to the outside world.

This framing eliminates the need for many patterns that exist only because the domain is not carrying its weight. There is no need for a dependency injection container to wire together a domain that is self-contained. There is no need for a repository pattern when persistence is an annotation on the domain object that requires it. There is no layered architecture to enforce when the boundary between domain and adapter is conceptual and obvious.

The complexity budget is spent entirely on essential complexity, because there is nowhere for accidental complexity to hide. Every technology choice can be evaluated against a single question: does a domain object require this? If not, it has no business being there. The domain model is not just a design tool — it is the justifier for every architectural decision, the brake on over-engineering, and the answer to YAGNI grounded not in gut feel but in domain reasoning.


The Principle Underneath

There is a principle that connects everything above:

The ease of implementing something without modeling it is proportional to the hidden cost of never having modeled it.

Every approach that starts from how rather than what — procedural scripts, transaction-script architectures, use-case driven development — shares this characteristic. The requirement is the input, the implementation is the output, and the domain never appears. Each new requirement starts from scratch, because there is no accumulated understanding to build on. The codebase grows. The knowledge does not.

A rich domain model inverts this entirely. The domain is the input. Requirements are queries against that understanding. New requirements find their place in something that already exists — or reveal, through the friction of not fitting, that the domain needs to grow. Either way, understanding accumulates. The model becomes more true over time, not less.

That is what a rich domain model is. Not a pattern. Not a layer. A discipline of making the essential complexity of a business explicit, owned, and honest — and letting everything else follow from that.



AI is genuinely useful in a domain-centric codebase — for implementing adapters, generating boilerplate, and accelerating everything that surrounds the model. It pattern-matches well against known structures, and once the domain is understood, there is plenty of that work to do.

Domain modeling is a different activity. It requires understanding what the business actually is — not just what a ticket describes. It requires recognizing when a concept is missing, resisting the obvious implementation in favor of the correct abstraction, and making judgment calls about responsibility that have no objectively correct answer. AI has no access to the lived understanding that produces those judgments.

The most useful role for AI in a modeling context is as a mirror — a Socratic partner for stress-testing a hypothesis about a concept's responsibility or boundary. It surfaces objections, identifies gaps, and forces precision. That is valuable. But the modeling itself remains a human activity, and the discipline of doing it remains more important in an AI-assisted world, not less. Without the model, AI produces procedural code at unprecedented speed — and accumulates the hidden cost of unmodeled requirements faster than any previous approach.


The common perception is that a rich domain model requires heavy upfront investment — that you must design everything before writing any code, and that this slows delivery. In practice the opposite is true, and the gap becomes visible quickly.

Early in a project, a team building a rich domain model is establishing core concepts and their responsibilities. This feels slower than a team wiring up framework configuration and generating boilerplate. But by the time the first meaningful features are being built, the domain team is adding behavior to objects that already understand the business. New requirements find their place. The model tells you where things belong. The other team is asking "where does this code go?" for every new feature — and the answers are becoming less consistent, not more.

The acceleration compounds. Maintenance is cheaper because the model is the documentation — it cannot go stale, because it is the code. Debugging is faster because the model expresses business intent, not just technical state. The difference between "the Order refused shipment because it was already delivered" and "some process node returned an unexpected status" is the difference between understanding and archaeology.

The people costs tell the same story. Onboarding a developer onto a well-modeled domain takes hours, not months. The knowledge is in the model, not in the heads of the people who built it. That is not just an efficiency gain — it is a risk reduction. The bus factor of an application with an explicit domain model is structurally higher than one without.

The cost of not modeling is real, large, and almost never measured — because there is no comparable version of the same application where it was modeled. You cannot see the cost of understanding you never accumulated. You only feel it, gradually, in every feature that takes longer than it should, every bug that touches more than it should, and every developer who leaves taking knowledge that was never made explicit.



Further Reading

This article is part of a series on domain-centric thinking.

If this raised the question of how to start modeling — how to discover the actors, assign the roles, and run a discovery session before a line of code is written — that is covered in [Rich Domain Models: Start with What Is, Not What Happens].

If you want to see a domain model grow through concrete examples — how a real model evolves as understanding deepens, and what it means to let a new requirement reshape the model rather than just add to it — that is covered in [Rich Domain Models: A Library Story].


The concepts in this article reflect practical experience building domain-centric applications. The Interaction pattern described has been in production use since 2009.

P

That’s a solid point. Software development really benefits when engineering principles come first, not just quick delivery. Planning, system design, and reliability still matter a lot, especially when building products that scale. It’s similar in service businesses too—Sawtransfer works better because the process is planned like an engineering system, not just random transport booking. Strong systems always create better user experiences.

L

Once you're used to rich domain modelling, delivery is faster than using template driven framework development. I've seen it happen many times.