Appearance
Domain Modeling
Understanding the real world first, then representing that understanding faithfully in code
Related Concepts: Clean Architecture | Use Cases | Domain Service | Repository
What is a Domain Model?
When developers work on a software system they learn about the business and industry that the software is for. If you're working in finance you're going to learn about how transactions and accounts work, and you'll learn the specific rules for calculating interest in different kinds of financial instruments. If you're working in shipping you'll learn about how cargo makes its way to ports, is loaded onto ships, and is moved around the globe. We learn all this domain knowledge and then encode it into the software systems that we build.
A domain model is the structured representation of that knowledge in code. It captures the concepts, relationships, rules, and behaviors of the real-world problem your software is solving. Martin Fowler defines it as "an object model of the domain that incorporates both behavior and data." Eric Evans calls it "a system of abstractions that describes selected aspects of a sphere of knowledge, influence or activity."
The key word is model. A model is a simplification — it doesn't try to represent everything about the real world, only the aspects relevant to solving the problem at hand.
Why Domain Modeling Matters
Well-designed systems model the business domain. This has two main benefits.
Your Code Becomes Intention-Revealing
A good domain model emanates domain knowledge. If a new developer is reading a section of code they learn about the domain by reading the software system. You don't even need to be a new developer on the project — it can just be you six months after you wrote something.
When a PurchaseOrder can only transition from Draft to PendingApproval by being submitted, and only a procurement employee can move it to Approved, the code is teaching you how the business works. The rules aren't buried in a service somewhere — they're expressed by the objects that own them.
Your System Becomes Easier to Change
Systems that model their domain effectively will be easier to understand, and therefore easier to change. If a system models the domain well then new feature requests will naturally fit into the system more smoothly with less work and awkwardness. A well-modeled domain gives you a place to put new behavior that makes sense — instead of bolting features onto the side of something that wasn't designed to accommodate them.
What a Domain Model Looks Like
A domain model is made up of a few fundamental building blocks.
Entities
Entities are objects defined by their identity rather than their attributes. A Customer is still the same customer even after they change their name or address. What makes them unique is their identity, which persists over time. Entities carry behavior — they enforce their own rules and protect their own consistency.
Value Objects
Value objects are defined entirely by their attributes. An Address or a Money amount has no identity of its own — two Money objects representing $10.00 are interchangeable. Value objects are typically immutable. Whether something is an entity or a value object depends on context: an Address is a value object in an e-commerce system but might be an entity in a shipping system where individual addresses are tracked over time.
Aggregates
An aggregate is a cluster of associated objects treated as a unit for data changes. It has a root entity that controls access to everything inside. When you save a PurchaseOrder, you save its line items with it — they're part of the same aggregate. Aggregates define consistency boundaries: the rules inside an aggregate are always enforced as a single atomic operation.
Domain Services
Some operations don't naturally belong to any single entity. Transferring funds between two accounts isn't something that belongs to either account — it's an activity in its own right. Domain services are stateless operations named for the activities they represent.
Domain Invariants
Invariants are business rules that must always be true. "Only submitted purchase orders can be approved." "You can't allocate more inventory than is available." These rules are expressed as assertions in domain code, enforced by the objects themselves — not checked after the fact by some external validator.
The Anemic Domain Model Anti-Pattern
The most common way domain modeling goes wrong is the anemic domain model. Martin Fowler described it in 2003 and it remains pervasive:
The basic symptom of an Anemic Domain Model is that at first blush it looks like the real thing. There are objects, many named after the nouns in the domain space, and these objects are connected with the rich relationships and structure that true domain models have. The catch comes when you look at the behavior, and you realize that there is hardly any behavior on these objects, making them little more than bags of getters and setters.
In an anemic model, all the domain logic lives in service objects. The "domain objects" are just data containers — they hold state but don't do anything with it. This is procedural code wearing an object-oriented costume.
The problem isn't just aesthetic. An anemic model incurs all the costs of a domain model — the complexity of mapping objects to a database, the overhead of maintaining entity relationships — without any of the benefits. You get the structure but not the intention-revealing behavior. You get the nouns but not the verbs.
A proper service layer is thin. It coordinates and delegates to behaviorally rich domain objects. The more behavior you find in the services, the more likely you are to be robbing yourself of the benefits of a domain model.
What Anemic Looks Like
typescript
// ❌ Anemic: the entity is just a data bag
class PurchaseOrder {
id: string;
status: string;
lineItems: LineItem[];
submittedBy: string;
approvedBy: string | null;
}
// All logic lives in a service
class PurchaseOrderService {
approve(po: PurchaseOrder, reviewerId: string): void {
if (po.status !== 'pending_approval') {
throw new Error('Cannot approve');
}
po.status = 'approved';
po.approvedBy = reviewerId;
}
}What Rich Looks Like
typescript
// ✅ Rich: the entity owns its behavior and enforces its rules
class PurchaseOrder {
private constructor(
private readonly id: PurchaseOrderId,
private status: PurchaseOrderStatus,
private readonly lineItems: LineItem[],
private readonly submittedBy: UserId,
private approvedBy: UserId | null
) {}
approve(reviewer: UserId): void {
if (this.status !== PurchaseOrderStatus.PendingApproval) {
throw new DomainInvariantViolation(
'Only submitted purchase orders can be approved'
);
}
this.status = PurchaseOrderStatus.Approved;
this.approvedBy = reviewer;
}
}The difference: in the rich model, you cannot put a PurchaseOrder into an invalid state from the outside. The entity enforces its own rules. The domain logic lives with the data it operates on.
Domain Modeling and Architecture
Effective domain modeling requires separating domain code from infrastructure code. If your entities are coupled to your database, your HTTP framework, or your ORM, they can't model the domain without compromise. This is why domain modeling and Clean Architecture go hand in hand — the architecture creates the space for the domain model to exist on its own terms.
In Clean Architecture, the domain model lives in the innermost layer. It has no dependencies on anything else. Use Cases orchestrate domain objects to achieve business goals. Repositories provide persistence without the domain knowing how or where data is stored. The domain just models the domain.
Domain Modeling and Domain-Driven Design
Domain modeling as a practice predates Domain-Driven Design by decades. It has roots in entity-relationship modeling (Peter Chen, 1976), object-oriented analysis throughout the 1980s and 1990s (Booch, Rumbaugh, Jacobson, Wirfs-Brock), and Martin Fowler's work on analysis patterns in the late 1980s.
Eric Evans's 2003 book Domain-Driven Design built on this foundation, adding strategic patterns like Bounded Contexts and Ubiquitous Language, and tactical patterns like Aggregates and Domain Events. DDD is a methodology for organizing entire systems around domain models. Domain modeling is the underlying practice — understanding a problem domain and representing it faithfully in code.
You don't need to adopt DDD wholesale to benefit from domain modeling. But if you're modeling your domain well, you're already doing the most important part of what DDD advocates.
Further Reading
- Anemic Domain Model - Martin Fowler's essential critique of behaviorless domain objects
- Domain-Driven Design - Fowler's overview of the broader DDD methodology
- Domain-Driven Design by Eric Evans - The foundational text on building systems around domain models
- Implementing Domain-Driven Design by Vaughn Vernon - Practical guidance, especially on aggregate design
- Domain Modeling Made Functional by Scott Wlaschin - Domain modeling in the functional paradigm
- Analysis Patterns by Martin Fowler - Reusable domain model patterns, predating DDD
- Domain Modeling Workshop - Synapse's hands-on workshop for practicing domain modeling skills
Domain modeling is the foundation that our Clean Architecture, Use Case, and Repository patterns build upon. The quality of your software depends, above all, on the quality of the model it embodies.