If you have ever shipped a product that “worked” but felt strangely fragile, you have seen software complexity up close.
Not the fun kind of complexity, like solving a hard algorithm. The painful kind:
- every new feature adds unexpected side effects
- the team debates words more than solutions
- simple changes require weeks of coordination
- “requirements” sound clear in a doc, then turn fuzzy in implementation
People often introduce DDD as an engineering practice. But for product managers, it is more useful to treat DDD as a decision and collaboration framework that helps you reduce ambiguity in product thinking.
Table of Contents
- 1. Why Product Managers Should Use Domain-Driven Design (DDD)
- 2. Domain Model Explained for PMs (Definition, Examples, and Smell Tests)
- 3. DDD Layered Architecture: Presentation vs Application vs Domain vs Infrastructure
- 4. Domain-Driven Design (DDD) Building Blocks: Core Domain Model Concepts
- 5. Objects in DDD (Representing Domain Concepts and Change)
- 6. Domain Services in DDD (What Logic Doesn’t Belong to One Object)
- 7. Modules in DDD (How to Organize the Domain as Products Grow)
- 8. Aggregates in DDD (Designing Consistency Boundaries)
- 9. Factories in DDD (Creation Rules, Defaults, and “Allowed to Exist”)
- 10. Repositories in DDD (Working with Domain Objects Over Time)
- 11. Final Checklist: Domain Model Quality Checks (for PMs)
- 12. Conclusion: Why This Matters
1. Why Product Managers Should Use Domain-Driven Design (DDD)
1) What Actually Causes Software Complexity (Hint: Domain Ambiguity)
A lot of teams assume complexity comes from technology choices:
- microservices vs monolith → an architectural choice that often exposes unclear domain boundaries rather than fixing them
- which database → an optimization decision that cannot compensate for a weak domain model
- which framework → a productivity accelerator that does not define product behavior
- cloud infrastructure decisions → a scaling concern that sits far downstream from domain understanding
Those matter, but many of the hardest problems show up earlier, when the team does not share a solid understanding of the domain.
Domain here means:
The real-world activities, constraints, and mental models behind the product.
In other words, what users are actually trying to do and what “rules” exist in their environment. When domain understanding is weak, a predictable pattern happens:
- PMs and stakeholders describe needs using business language
- Engineers interpret that language into a technical design
- The technical design becomes hard to undo later
- The product starts accumulating “workarounds”
- Complexity keeps rising, even if the feature set is not that big
This is why complexity often appears even in simple-looking products.
2) When Strict Handoffs Increase Cost (Waterfall vs Agile Is Not the Point)
In a strict handoff flow (analyze → document → build), “analysis” and “design” tend to drift apart:
- During analysis, the discussion uses domain-specific terms that engineers may not fully internalize.
- During design, engineers make assumptions to fill gaps,
- Once code ships, those assumptions become expensive to reverse.
DDD pushes against that drift by keeping domain understanding and software design connected, so “what we mean” and “what we build” stay aligned. When a roadmap feels increasingly expensive even though features are incremental, the bottleneck is often domain ambiguity, not engineering speed.
3) DDD as a PM Framework: Alignment, Decisions, and Scalability
A practical way to think about DDD as a PM is that it creates a shared map of the product’s reality. That map helps in three areas PMs care about every day.
(1) Ubiquitous Language: How PMs and Engineers Avoid Vocabulary Debt
Many product disagreements are actually vocabulary disagreements.
Imagine you are building a subscription-based learning platform for companies. In meetings, people might say:
- “seat”
- “license”
- “member”
- “user”
- “learner”
- “account”
If these terms are used interchangeably, you get hidden confusion:
- Does “seat” mean a paid entitlement, or an invited person, or an active learner?
- Can one user have multiple seats across departments?
- Does deactivation remove access immediately or after the billing cycle?
DDD encourages you to define a ubiquitous language: a consistent vocabulary that both business and engineering use in conversation, docs, and code.
This is not about being overly precise for its own sake.
It is about lowering the cost of coordination.
(2) Writing Requirements as Business Rules (Not UI or Database Specs)
PMs often feel trapped between:
- writing requirements too vaguely, causing rework
- writing requirements too specifically, accidentally dictating implementation
DDD offers a third option: express requirements as domain operations and business rules, not UI details or database structure.
Instead of:
- “Add a toggle to disable seat sharing”
You can express:
- “A License can belong to only one Member at a time.”
- “A Member can belong to multiple Workspaces, but billing is per Workspace”
This gives engineers a stable foundation while leaving flexibility in how to implement.
(3) Conceptual Scalability: Bounded Context (Preview) and Product Boundaries
When products grow, the hardest scaling problems are not just performance or infra. They are conceptual:
- the same word starts meaning different things across teams
- one part of the product evolves faster and breaks assumptions elsewhere
- new customer segments introduce rules that conflict with existing ones
DDD introduces ideas like “bounded context” (we will get to it later), but even before that, a strong domain model helps you decide:
- what should change together
- what should remain isolated
- where teams should allow complexity to live
4) Key Takeaway: Turn Fuzzy Conversations into Explicit Domain Rules
DDD is not “an engineer thing you should tolerate.”
For PMs, DDD can be:
- a thinking framework for making product concepts crisp
- a collaboration tool that reduces misalignment
- a way to keep complexity from silently becoming strategy debt
You do not need to draw perfect diagrams or write production code to benefit from it.
What matters is building a habit:
Turn fuzzy product conversations into explicit domain concepts and rules that the whole team can use.
That is how DDD becomes a strategic PM skill.
2. Domain Model Explained for PMs (Definition, Examples, and Smell Tests)
When people first hear “domain model,” they often imagine something very concrete:
- a box-and-arrow diagram
- a UML chart
- a set of classes in code
Those can be useful, but they are not the domain model itself. They are just expressions of it.
1) What Is a Domain Model? (And What It Is Not)
A domain model is a structured way of expressing knowledge about user activity.
More specifically, it is:
- a shared understanding of what exists in the user’s world
- how those things relate to each other
- and which rules govern their behavior
It is an abstraction, not a specification.
Think about a product that helps teams plan offsite workshops.
Users talk about things like:
- sessions
- facilitators
- time slots
- agendas
- participants
- constraints like room capacity or session overlap
A domain model takes that messy reality and turns it into a coherent conceptual system:
- What is a Session, and what makes it valid?
- Can a Facilitator run two Sessions at the same time?
- Is an Agenda a fixed plan or a mutable draft?
- When does a Participant become “confirmed”?
None of this requires UI decisions or database tables yet. But without answering these questions, implementation choices become guesses.
A helpful rule of thumb:
If the team cannot explain a concept clearly in words, the code will not explain it either.
2) Domain Model vs Diagram vs Code: Where the Real Model Lives
- A diagram is a snapshot of how you currently understand the domain.
- Code is a concrete expression of that understanding.
- The domain model lives underneath both, and it evolves.
This is why teams can have beautifully drawn diagrams and still ship confusing products. The model itself never aligned with the intended goals.
Treat the domain model as “the product’s conceptual backbone,” not as a document artifact.
3) Domain Relationships: Direction, Qualifiers, and Removing Non-Essentials
| Principle | What it means | Why it matters |
|---|---|---|
| Impose direction | Prefer one-way relationships by default. One object knows about another, not both. | Reduces coupling and makes ownership and change impact clearer. |
| Use qualifiers | Add context (such as date, role, or state) to narrow large one-to-many relationships. | Turns vague relationships into precise domain rules and access patterns. |
| Eliminate non-essential associations | Remove direct links that are not required for behavior or decisions. | Keeps the model small, flexible, and easier to evolve. |
These three principles all serve the same goal: keeping the domain model understandable as it grows.
They limit how objects depend on each other, reduce hidden assumptions, and make the flow of decisions clearer.
Rather than adding more structure, they intentionally remove or constrain relationships so that only behavior-critical connections remain.
When applied together, these practices make the model easier to change and help truly important relationships stand out amid the noise.
4) What Makes a Good Domain Model? (Explainability, Collaboration, Iteration)
You should not judge a domain model by elegance or completeness. Instead, judge it by how well it supports shared understanding and decision-making. There are three characteristics worth calling out.
(1) Concepts are explainable, not just nameable
Naming things is easy. Explaining them is harder.
A weak model says:
- “We have a Session object.”
A stronger model can explain what Session actually means in the product.
For example, in a workshop scheduling product, a good domain model can clearly explain:
- when a Session starts and ends → a Session starts only when a facilitator joins, not when someone merely creates it on the calendar.
- which changes teams can make before the Session starts → teams can change the time and facilitator before the Session starts. Once the Session is in progress, teams cannot change them.
- which rules must always hold, even as the system grows → a facilitator can never run overlapping Sessions, regardless of future features
If different team members give different explanations of what a Session is, the model is not ready yet.
(2) Teams build it through collaboration, not extraction
Good domain models do not come from one person “capturing requirements.”
They emerge when:
- domain experts explain how they think and decide
- PMs ask clarifying “why” and “what if” questions
- engineers challenge edge cases and constraints
This back-and-forth forces everyone to refine their thinking. Domain experts often realize their own assumptions only when they try to explain them precisely.
(3) It evolves through iteration, not upfront perfection
A domain model that cannot survive iteration will quietly collapse under real usage.
Good domain models usually begin with a small but solid core, shaped through deep conversations with domain experts and users:
- a few critical concepts grounded in real user work
- clear ownership of responsibilities
- explicit rules that reflect how the business actually operates
Teams then evolve the model by testing it against real product decisions.
A typical cycle looks like this:
- define a small set of core concepts and rules through close collaboration with domain experts and users
- use them in actual feature and design discussions
- observe where explanations break down or feel forced
- refine language, boundaries, or responsibilities
- repeat, without losing the original core
Iteration here is not about constant rework.
It is about stress-testing early assumptions under real usage.
The goal is not to predict everything upfront. It is to build a model that can absorb change without losing clarity as the product evolves.
5) The PM’s Role in Domain Modeling (PRDs, Stakeholders, and Invariants)
(1) PRDs that reveal structure, not just scope
PMs can influence domain models in unique ways, even without writing code.
Here are three practical touchpoints.
You do not need a massive “domain model” section. Even small signals help:
- define key terms explicitly
- clarify which rules are strict vs flexible
- call out invariants (things that must always be true)
For example:
- “Once the session starts, teams keep confirmed participants.”
- “Before publication, teams may freely change the agenda.”
These sentences guide architecture decisions more than long feature lists.
(2) Stakeholder communication
When stakeholders disagree, the disagreement is often about mental models, not priorities.
Using domain language lets you ask:
- “Are we disagreeing about behavior, or about definitions?”
- “Does this rule apply to all sessions, or only to internal workshops?”
This shifts the conversation from opinion to structure.
PMs act as translators between user reality and system reality. Domain modeling is where that translation becomes explicit.
6) Example: Booking Domain Model (Room, Booking, TimeSlot)
Let’s ground this with a simple example that is not ecommerce. Imagine a product for internal team room bookings.
Core domain concepts might include:
- Room
- Booking
- TimeSlot
- Organizer
Some early questions a domain model helps answer:
- Does a Booking require both a Room and a TimeSlot, or can it exist before a team assigns a Room?
- Can two Bookings overlap if they belong to the same Organizer but different Rooms?
- Is a TimeSlot just start and end times, or does it include recurrence rules?
A simplified domain sketch in code (purely illustrative, not production-ready):
Core Concepts
─────────────
Room (Entity) Organizer (Entity)
│ │
│ referenced by │ referenced by
▼ ▼
┌───────────────────────────────────────────┐
│ Booking (Entity) │
│-------------------------------------------│
│ identity: bookingId │ (not shown in code, but implied)
│ │
│ fields: │
│ - roomId │ → references Room
│ - organizerId │ → references Organizer
│ - timeSlot (Value Object) │ → must be valid
└───────────────────┬───────────────────────┘
│ has
▼
┌──────────────────────────────┐
│ TimeSlot (Value Object) │
│------------------------------│
│ fields: │
│ - startTime │
│ - endTime │
│ invariant: │
│ startTime < endTime │
└──────────────────────────────┘
Code language: JavaScript (javascript)
Rule encoded in the model:
- A Booking is not valid without a valid TimeSlot.
- A TimeSlot is valid only if startTime < endTime.
- Booking references other concepts by IDs to avoid tight coupling.
When rules are clear in the domain model, implementation choices become constrained in a good way.
3. DDD Layered Architecture: Presentation vs Application vs Domain vs Infrastructure
Once a team starts taking the domain model seriously, a practical question follows quickly:
“Where does all this domain logic actually live in the system?”
This is where the four-layer architecture in DDD becomes useful. Not as a rigid rulebook, but as a way to separate responsibilities so the domain stays understandable over time.
When a product feels fragile, domain rules often spread across layers instead of living in one place. Layering exists to separate concerns. The core idea behind layering is separation of concerns.
IInstead of mixing everything together, teams divide the system into layers, each with a clear role and a different question to answer.
| Layer | Main Question It Answers |
|---|---|
| Presentation | How does the user interact with the system? |
| Application | What use case is being executed right now? |
| Domain | What are the business rules and concepts? |
| Infrastructure | How is this technically supported? |
When teams mix these concerns, two things happen:
- small UI changes accidentally break business rules
- technical shortcuts leak into product behavior
DDD uses layering to protect the domain layer, because that is where the long-term complexity lives.
1) Presentation Layer: UX Guardrails vs Business Rules (Source of Truth)
The presentation layer is the point where users or external systems first interact with the product. This includes screens, APIs, admin tools, and partner-facing interfaces.
What matters is not what appears here, but what role this layer plays.
The presentation layer intentionally limits its responsibilities:
- displaying current system state in a way users can understand
- collecting user input and signals
- translating those interactions into requests the application layer can process
From a PM perspective, this is where teams:
- design user flows
- make UX tradeoffs
- communicate language, affordances, and constraints to users
At the same time, teams should not enforce core business rules in the presentation layer.
For example, in a scheduling product:
- disabling a “Book” button guides user behavior through UX
- determining whether a booking violates overlap rules is a domain concern that must hold regardless of UI, API, or integration path
When teams let business logic live in the presentation layer, they reimplement the same rules in multiple places and create inconsistent behavior at the edges.
2) Application Layer: Use Cases and Orchestration (Keep It Thin)
The application layer sits between user interaction and domain logic.
The application layer sits between user interaction and domain logic. It does not define business rules, but coordinates how things work.
When a request comes in from the presentation layer, this layer decides:
- which domain operations to invoke,
- in what order,
- and within what transactional boundary.
In that sense, it answers questions like:
- “What actually happens when a user books a room?”
- “Which domain concepts does this flow touch?”
- “How do these steps fit together?”
A key DDD principle is to keep the application layer thin.
Thin does not mean unimportant. It means this layer avoids owning business rules or long-lived state, and instead focuses on orchestration.
PMs often define user flows visually. Application services are the technical counterpart of those flows.
Actions like create booking, cancel booking, or reschedule booking typically map to application-level use cases that coordinate domain behavior.
When business rules start creeping into this layer, problems tend to follow:
- the same logic gets duplicated across multiple flows,
- and similar actions begin to behave inconsistently.
3) Domain Layer: Business Rules, Invariants, and Product Guarantees
The domain layer is where the product’s core logic lives. This is where entities, value objects, domain services, and invariants come together to express how the business actually works.
If you want to understand:
- what the product truly allows or forbids,
- what must remain consistent over time,
- or why certain changes are disproportionately expensive,
you almost always end up in the domain layer.
What makes this layer especially important is that it encodes assumptions.
Rules like:
- “A booking cannot be modified after it starts,”
- “A facilitator cannot host overlapping sessions,”
- or “A license belongs to a workspace, not an individual,”
are not implementation details. They are statements about how the business operates.
Once such rules are embedded in the domain model, changing them has wide impact. That impact is not a problem, as long as the change is intentional and understood.
This is why PM involvement matters most here:
- early clarification reduces downstream rework,
- understanding invariants helps sequence the roadmap,
- and clear boundaries prevent accidental scope expansion.
The domain layer is the heart of the system.
4) Infrastructure Layer: Persistence and Integrations Without Leaking Meaning
The infrastructure layer supports the system without defining its meaning.
It includes databases, external APIs, messaging systems, file storage, and third-party services. These components make the product run, but they should not decide how the business works.
From a PM perspective, infrastructure discussions usually surface as:
- performance constraints,
- cost tradeoffs,
- reliability or scalability concerns.
DDD emphasizes an important inversion here:
the domain should not depend on infrastructure details. Ideally, it should not matter to the domain whether data is stored in one database or another, or whether notifications are sent via one service or another.
This separation makes it easier to:
- evolve the product without rewriting core logic,
- swap vendors or technologies as constraints change,
- and scale the system without distorting business rules.
5) Principles of Layer Interaction: How Layering Contains Change
┌──────────────────────────┐
│ Presentation Layer │◀─ UI, API, Controllers
└─────────────┬────────────┘
│ depends on
▼
┌──────────────────────────┐
│ Application Layer │◀─ Use Cases, Orchestration
└─────────────┬────────────┘
│ depends on
▼
┌──────────────────────────┐
│ Domain Layer │◀─ Business Rules, Invariants
└─────────────┬────────────┘
│ supported by
┌──────────────────────────┐
│ Infrastructure Layer │◀─ DB, Messaging, External APIs
└──────────────────────────┘
Code language: PHP (php)
The most important rule in a layered architecture is dependency direction.
Teams intentionally constrain dependencies so that outer layers depend on inner layers, not the other way around. This constraint limits how far the impact of a change can spread.
In practice, this rule is less about architectural purity and more about change containment. When dependencies flow in one direction, changes at the edges cannot quietly reshape the core of the product.
Concretely, this means:
- presentation depends on application → UI and API changes may affect how teams trigger actions, but not what the system fundamentally allows
- application depends on domain → user flows can change without rewriting business rules
- infrastructure supports all but should not leak upward → database or vendor changes should not redefine product behavior
When teams violate this direction, the first symptoms usually appear at the edges:
- UI-specific checks start acting like business rules
- database schemas or query patterns begin dictating what the product can or cannot do
Over time, the “real” rules become hard to locate, because they live in multiple layers at once.
A related principle follows naturally: communicate intent, not structure.
At the application boundary, the system should speak in terms of meaningful actions:
- schedule booking
- cancel booking
not in terms of how the system manipulates data internally.
- insert row
- update table
6) Example Flow: “Book a Room” Across Layers
Let’s connect everything with a simple flow. Here is how a “Book a Room” action typically travels across layers.
User clicks "Book"
│
▼
┌──────────────────────────────┐
│ Presentation Layer │
│ - collect form input │
│ - validate format (required) │
│ - express intent: │
│ "book this room" │
└───────────────┬──────────────┘
│
▼
┌──────────────────────────────┐
│ Application Layer │
│ - trigger CreateBooking │
│ - define execution order │
│ - open transaction │
└───────────────┬──────────────┘
│
▼
┌──────────────────────────────┐
│ Domain Layer │
│ - enforce booking invariants │
│ - apply TimeSlot rules │
│ - reject conflicts │
│ - decide if booking is valid │
└───────────────┬──────────────┘
│
┌──────────────────────────────┐
│ Infrastructure Layer │
│ - persist booking │
│ - send notifications │
│ - integrate external systems │
└──────────────────────────────┘
Code language: JavaScript (javascript)
What matters here is not the sequence itself, but where decisions are made.
- early layers express intent
- the domain layer decides truth
- infrastructure executes outcomes
The same flow would apply whether the booking came from a web UI, a mobile app, or an API integration. The entry point changes, but the core decision-making does not.
4. Domain-Driven Design (DDD) Building Blocks: Core Domain Model Concepts
Before diving into each concept in detail, it helps to see the full landscape.
Domain-Driven Design is not a single technique. It is a set of building blocks, each answering a different product question:
- What exists in the domain?
- What must stay consistent?
- Where do rules live?
- How does the model evolve safely over time?
The table below provides a PM-oriented map of the core DDD building blocks, what they are responsible for, and when they matter most in product work.
| Building Block | What it Represents | Core Responsibility | Key Question |
|---|---|---|---|
| Object | A meaningful concept in the domain | Represents domain meaning and behavior | “Is this a real concept users think about?” |
| Service | A business decision spanning multiple concepts | Enforces cross-object business rules | “This rule doesn’t belong to one object—where should it live?” |
| Module | A coherent part of the domain story | Groups related concepts | “What kind of product problem does this part exist to solve?” |
| Aggregate | A consistency boundary | Ensures invariants hold together | “What must never be partially updated, even briefly?” |
| Factory | Creation logic for domain objects | Enforces creation-time rules | “Under what conditions is this allowed to exist at all?” |
| Repository | Access to existing domain objects | Manages retrieval and persistence | “How do we work with this safely over time?” |
Each building block exists to answer a different kind of product question:
- Objects define what concepts exist in the product and carry domain meaning.
- Services define how teams make business decisions when rules span multiple concepts.
- Modules define how teams organize the domain so the model stays understandable as it grows.
- Aggregates define where teams enforce consistency immediately and where they allow it to relax.
- Factories define when something may exist at all.
- Repositories define how the system accesses and persists domain objects over time without leaking storage concerns.
They emerge as the product starts answering harder questions about identity, change, consistency, and scale.
When a feature feels simple but unexpectedly expensive, this table often points to which responsibility is missing, overloaded, or misplaced in the current model.
5. Objects in DDD (Representing Domain Concepts and Change)
Before diving into Entities and Value Objects, it helps to clarify what “Object” means in the context of DDD.
In Domain-Driven Design, an Object is not a technical construct or a programming artifact. It is a way to represent a concept in the domain that the product needs to reason about.
An object exists to:
- carry meaning in the domain
- encapsulate rules or constraints
- participate in business decisions
What matters is not how the object is implemented, but what role it plays in expressing the product’s reality.
From this perspective, DDD makes a further distinction:
- some objects persist over time→ Entities
- some objects only describe a situation or state → Value Objects
The difference is not technical. It is about how the product treats change.
1) Entity vs Value Object (How PMs Decide Identity and Change)
If the four-layer architecture explains where logic lives, the building blocks explain what that logic consists of. Most domain models start to drift when teams are unclear about one foundational distinction:
What should be modeled as an Entity, and what should be modeled as a Value Object?
This distinction looks technical at first, but it shapes how PMs think about identity, change, and consistency in a product.
2) What Is an Entity? (Identity and Continuity Over Time)
An Entity represents something the product treats as the same thing over time, even as its details change. What defines an entity is not how it looks at any given moment, but the fact that the system cares about its continuity.
In practice, entities almost always have an ID. But the important idea is identity, not the ID itself.
Consider a B2B hiring platform used by recruiting teams to manage applications and interviews internally.
In most hiring domains, two entities appear almost immediately:
- Candidate: a real person the product interacts with
- their profile may be updated
- their contact details may change
- past interactions still belong to them
- Application: a specific attempt by that candidate to apply for a role
- it has a clear start and end
- it moves through stages (screening, interview, offer)
- its history and current status matter independently
Both are entities because the product needs to refer back to them as the same thing later, even after changes occur.
We can visualize Application entity like this:
Core Concepts (Hiring Domain)
────────────────────────────
┌──────────────────────────┐
│ Candidate (Entity) │
└─────────────┬────────────┘
│ participates in
▼
┌────────────────────────────────────────────┐
│ Application (Entity) │
│--------------------------------------------│
│ identity: applicationId │
│ │
│ fields: │
│ - candidateId: CandidateId │
│ - status: ApplicationStatus │
│ - interviewSlot: InterviewSlot | null │
│ │
│ invariants: │
│ - status transitions must be valid │
│ - interviewSlot required when INTERVIEW │
│ - interviewSlot must be null otherwise │
└────────────────────────────────────────────┘
Code language: JavaScript (javascript)
The important point is not the stages themselves, but the fact that the identity stays constant while state evolves.
If users expect something to remain the same object tomorrow, even after multiple changes, you are likely dealing with an Entity. When continuity matters more than current state, you are modeling an Entity.
3) What Is a Value Object? (Replaceable Descriptions and Immutability)
A Value Object is something the product uses to describe a situation, not to track a thing.
The easiest way to tell is to ask a simple question:
If this changes, do we still think of it as the same thing?
If the answer is no, you are probably looking at a Value Object.
Consider the same internal ATS platform.
While Candidate and Application are entities, an InterviewSlot plays a very different role. It does not represent a thing the product tracks. It simply describes when an interview is scheduled.
This distinction becomes clear when we look at how InterviewSlot relates to other concepts in the domain.
We can visualize that behavior like this:
Core Concepts (Hiring Domain)
────────────────────────────
┌──────────────────────────┐
│ Candidate (Entity) │
└─────────────┬────────────┘
│ participates in
▼
┌────────────────────────────────────────────┐
│ Application (Entity) │
│--------------------------------------------│
│ identity: applicationId │
│ │
│ fields: │
│ - candidateId │
│ - status │
│ - interviewSlot │
│ │
│ rules / invariants: │
│ - status follows allowed transitions │
│ - interviewSlot allowed only in INTERVIEW │
└─────────────┬──────────────────────────────┘
│ has
▼
┌──────────────────────────────────────────┐
│ InterviewSlot (Value Object) │
│------------------------------------------│
│ fields: │
│ - startTime │
│ - endTime │
│ │
│ invariant: │
│ - startTime < endTime │
│ │
│ characteristics: │
│ - no identity │
│ - no lifecycle │
│ - replaced, not updated │
└──────────────────────────────────────────┘
Code language: JavaScript (javascript)
What this diagram shows is that InterviewSlot exists only to describe a condition of an Application.
- teams do not reference it independently
- teams do not track it across time
- it has no meaning outside its values
If the interview time changes, the product does not need to remember the old slot. It simply uses a new one. There is no concept of “this specific InterviewSlot” surviving changes.
From a product perspective, value objects work well when:
- history does not matter on its own
- past values do not affect future decisions
- attributes alone determine equality
If, later on, the product starts tracking interview slots as things teams book, reschedule, or audit independently, that change signals a boundary shift. At that point, the concept needs identity and continuity, and teams should reconsider it as an entity.
4) Example: “IntervieSlot” as Value Object
At first glance, it might seem simpler to store start and end times directly as fields on Application:
┌────────────────────────────────────────────┐
│ Application (Entity) │
│--------------------------------------------│
│ identity: applicationId │
│ │
│ fields: │
│ - candidateId │
│ - status │
│ - <strong>interviewStartTime</strong> │
│ - <strong>interviewEndTime </strong> │
│ │
│ rules / invariants: │
│ - status follows allowed transitions │
│ - interviewSlot allowed only in INTERVIEW │
└────────────────────────────────────────────┘
Code language: HTML, XML (xml)
This works technically. But it blurs an important question:
Are we just storing timestamps, or are we modeling an interview slot?
When the product starts to care about what makes a valid slot, not just the raw values, a separate Value Object becomes useful. This would work technically. But modeling InterviewSlot as a distinct Value Object serves several purposes:
┌──────────────────────────────────────────┐
│ InterviewSlot (Value Object) │
│------------------------------------------│
│ fields: │
│ - startTime │
│ - endTime │
│ │
│ invariant: │
│ - startTime < endTime │
│ │
│ characteristics: │
│ - no identity │
│ - no lifecycle │
│ - replaced, not updated │
└──────────────────────────────────────────┘
Code language: JavaScript (javascript)
(1) It captures a domain concept explicitly
- Domain experts and users do not talk about “start times and end times.” They talk about “interview slots.”
- When a term shows up consistently in product discussions, it deserves to exist in the model. This is the basis of Ubiquitous Language.
(2) It groups related constraints
- Rules like “start time must be before end time” are not really rules about
Application. They are rules about time ranges. - Putting them in
InterviewSlotprevents these constraints from being scattered across services and entities.
(3) It enables reuse
- Once InterviewSlot exists, other parts of the domain can use it like checking interviewer availability and detecting scheduling conflicts
- Without it, each feature tends to reimplement its own version of “valid time range,” often with slightly different assumptions.
(4) It makes change cheaper
- When time-related rules evolve, having one place to change them matters.
- Unifying interview time logic into a Value Object limits how far those changes spread.
A good heuristic is to ask:
- Do domain experts use a specific term for this combination of fields?
- Are there invariants that relate these fields to each other?
- Will other parts of the domain need the same concept?
If the answer is yes to any of these, a Value Object may be appropriate even if it is only used in one place at first.
5) When a Value Object Becomes an Entity (Two-Sided Platform Example)
Now imagine the product evolves into a two-sided hiring platform, where candidates and interviewers actively coordinate schedules.
In this new model, interview slots are no longer just chosen and stored. They are managed.
Interview slots can now be:
- offered by interviewers
- booked by candidates
- rescheduled multiple times
- canceled and audited later
As soon as the platform works this way, new questions start to matter:
- “Is this the same interview slot that was scheduled last week?”
- “Who changed it, and when?”
- “Do we need to notify both sides when this interview changes?”
These questions introduce something new: continuity.
Once the product needs to refer back to the same interview slot across time, the concept has crossed a boundary. It is no longer just a description of time. It has become something the product tracks as the same thing over time.
At that point, the model changes.
Later Stage (Two-Sided Hiring Platform)
──────────────────────────────────────
┌──────────────────────────────────────────┐
│ InterviewSlot (Entity) │
│------------------------------------------│
│ identity: interviewSlotId │
│ │
│ fields: │
│ - startTime │
│ - endTime │
│ │
│ rules / invariants: │
│ - rescheduling follows allowed rules │
│ - cancellations are recorded │
│ │
│ characteristics: │
│ - identity preserved │
│ - history matters │
│ - changes are tracked │
└──────────────────────────────────────────┘
What changed was the product’s expectations.
In an internal ATS, interview time answers “when is this application’s interview?”
In a two-sided platform, interview slots answer “what is the current state of this specific scheduling agreement?”
The moment the product needs to say “this specific interview” and care about its past, the concept stops being a Value Object and becomes an Entity.
6) Common Modeling Mistakes: Everything-Entity, Mutable VO, Anemic Entities
These mistakes are common, especially when teams are still learning the domain.
They usually come from reasonable assumptions that stop being reasonable over time.
- Making Everything an Entity
- A frequent early decision is to model every concept as an entity, “just in case.” → This introduces identity and lifecycle where the product does not actually need them.
- What this leads to:
- unnecessary IDs and persistence logic
- more state to keep consistent
- higher coordination cost for simple changes
- Signals PMs often notice: small feature changes take longer than expected, discussions focus on “which one exactly?” even when users do not care
- Mutable Value Objects
- Value objects are meant to describe something, not to evolve over time.
- When a value object starts mutating internally, it behaves like a weak entity:
- identity is implied but never explicit
- history exists but is not modeled
- boundaries become unclear
- This often signals one of two things: the concept actually needs identity, or another missing concept should own the change
- Anemic Entities
- An anemic entity stores data but owns no behavior.
- In these models:
- entities hold fields
- services enforce all rules
- behavior lives far from the concepts it describes
- This separation makes the model harder to reason about:
- rules are scattered
- intent gets diluted across layers
- understanding the product requires reading multiple services
6. Domain Services in DDD (What Logic Doesn’t Belong to One Object)
Once entities and value objects are in place, teams often hit a practical question:
“Where does this logic go if it doesn’t clearly belong to one object?”
This is where Services come in.
Services are frequently misunderstood, especially by PMs, because the word is overloaded. In DDD, a service is not “just another class” and not the same as a technical microservice.
1) Why Domain Services Exist (Cross-Concept Business Decisions)
Services exist because some business behavior does not belong to a single object.
Entities and value objects are good at modeling things, but products also have behavior that is about decisions and coordination, not ownership.
A service is usually appropriate when:
- an operation spans multiple domain concepts
- logic feels unnatural inside one entity
- the action is central to the business narrative
- the operation is pure logic, not state ownership
If the logic only manipulates one entity’s internal state, start there first.
Typical examples include:
- approving a loan after multiple checks
- assigning an interviewer based on availability
- transferring credits between accounts
- determining eligibility across multiple rules
These actions share a pattern:
- they involve more than one domain concept
- they represent a business decision
- they do not naturally belong to one entity
When this kind of behavior is forced into an entity, the model starts to bend:
- entities reach into others
- responsibilities blur
- domain concepts lose clarity
Services exist to hold this kind of logic without distorting the entities. They give important domain actions a clear place, so the model can stay readable and intentional.
2) What Makes a Good Domain Service? (Stateless, Intent-Driven, Layer-Appropriate)
Not every helper function is a domain service. Good domain services share a few traits.
| Characteristic | What it means in practice | Why it matters |
|---|---|---|
| 1. Stateless | The service does not own long-lived data. It operates only on entities, value objects, and inputs passed in at execution time. | Keeps responsibilities clear and avoids turning services into hidden state holders. |
| 2. Expresses domain intent | The service name reflects a business action, not a technical step. – Good: scheduleInterview, approveApplication– Weak: processData, handleLogic | Makes domain behavior readable to both PMs and engineers. The name carries meaning. |
| 3. Layer-appropriate | The service’s responsibility matches the layer it lives in. | Prevents business rules from leaking into orchestration or infrastructure code. |
A good domain service is defined less by its structure and more by the role it plays in the model.
First, domain services are intentionally stateless. They do not hold long-lived data or represent business objects themselves. Instead, they operate on existing entities and value objects.
Second, a domain service should clearly express domain intent. Its name and purpose need to reflect a business action that makes sense to product stakeholders, not an internal technical step.
Finally, domain services must live in the right layer. While services exist across layers, domain services are specifically responsible for enforcing business rules. Keeping orchestration and integration logic out of the domain layer preserves clear boundaries and reduces model erosion.
Together, these characteristics help services reinforce the domain model, rather than becoming a convenient dumping ground for misplaced logic.
3) Application Service vs Domain Service vs Infrastructure Service
| Layer | What the service is responsible for | What it deliberately avoids |
|---|---|---|
| Application Service | Coordinates the use case end to end. It receives the request, invokes domain logic, manages the transaction, and returns the result. | Making business decisions or embedding domain rules. |
| Domain Service | Enforces business rules that span multiple domain concepts, such as availability, eligibility, and conflicts. | Managing flow, transactions, or external side effects. |
| Infrastructure Service | Handles interactions with external systems, such as calendars, notifications, or third-party APIs. | Knowing or enforcing business rules. |
The table below shows how different services across layers support the same user action: interview scheduling.
Each row shows where teams make decisions and what responsibilities each layer owns. For this reason, this comparison excludes presentation logic by design. The presentation layer does not make domain decisions or host domain services; it only translates user intent into application requests and renders the result.
Seen this way, the table highlights how teams decompose a single product action into coordination, decision-making, and external effects without mixing concerns across layers.
4) Example: Credit Transfer Domain Service (Rules Across Two Projects)
Consider a platform where teams receive monthly usage credits and can transfer them between projects.
┌────────────────────┐ ┌────────────────────┐
│ Project A (Entity) │ │ Project B (Entity) │
│--------------------│ │--------------------│
│ credits: 100 │ │ credits: 40 │
└─────────┬──────────┘ └─────────┬──────────┘
│ transfer 30 credits │
└──────────────▶──────────────┘
┌────────────────────┐ ┌────────────────────┐
│ credits: 70 │ │ credits: 70 │
└────────────────────┘ └────────────────────┘
A credit transfer is a business action, not a property of a single project. It comes with a few essential rules:
- credits cannot be negative
- the transfer must succeed or fail as a whole
- both projects must exist
No single project can enforce these rules on its own. The operation only makes sense when both sides are considered together.
Credit Transfer (one user action)
─────────────────────────────────
┌──────────────────────────┐
│ Presentation Layer │ UI / API
│ "Transfer 30 credits" │
└─────────────┬────────────┘
│ request
▼
┌──────────────────────────┐
│ Application Layer │ Use case coordinator
│ TransferCreditsUseCase │
│ - begin transaction │
│ - call domain service │
└─────────────┬────────────┘
│ domain operation
▼
┌──────────────────────────┐
│ Domain Layer │ Business rules live here
│ CreditTransferService │
│ - validate amount > 0 │
│ - ensure no negative │
│ - apply changes to A,B │
└─────────────┬────────────┘
│ persistence + side effects
▼
┌──────────────────────────┐
│ Infrastructure Layer │ Technical implementation
│ ProjectRepository │
│ - load A,B │
│ - save updated balances │
│ EventPublisher/Notifier │
└──────────────────────────┘
Code language: JavaScript (javascript)
This makes credit transfer a natural fit for a domain service.
- the operation involves multiple entities
- the rules apply across those entities
- the action does not belong to either project alone
7. Modules in DDD (How to Organize the Domain as Products Grow)
As a product grows, even a well-designed set of entities and services can become hard to navigate.
At that point, the question is no longer:
“Is this logic correct?”
It becomes:
“Where does this logic belong?”
This is the role of Modules in DDD.
1) What Is a Module? (Concept-First Grouping, Not Folder Structure)
A module is a way to organize the domain so that it remains understandable as it grows.
It is tempting to think of modules as folders or packages, because that is how they show up in code. But those are just containers. A module is defined by meaning, not by syntax.
A useful way to think about a module is as a chapter in the product’s domain story.
Each chapter:
- focuses on a specific theme or responsibility
- can be understood mostly on its own
- interacts with other chapters in limited, intentional ways
For example, in a hiring product, concepts related to applications and interview scheduling might naturally form one module, while candidate evaluation and feedback form another module.
Each module tells a coherent part of the domain story, without forcing the reader to constantly jump between unrelated concerns.
This is what a good module does. It gives the team a shared answer to the question:
“What kind of problem does this part of the system exist to solve?”
2) Good Module Design: High Cohesion and Low Coupling
The table below summarizes three principles that matter most when modules are used as a coordination tool, not just a code organization technique.
| Principle | What it means | Why PMs should care |
|---|---|---|
| Concept-first grouping | Modules are organized around domain concepts, not technical layers. | Helps teams reason in product language instead of code structure. |
| High cohesion | Concepts inside a module are closely related and tend to change together. | Makes it easier to predict the impact of a feature or change. |
| Low coupling | Modules interact through limited, intentional boundaries. | Reduces cross-team coordination and unintended ripple effects. |
(1) Concept-first grouping
Grouping code by ui/, services/, or database/ may be convenient technically, but it hides the product’s real structure. When modules are named after concepts like scheduling or reporting, discussions stay grounded in domain language instead of implementation detail.
(2) High cohesion
Inside a good module, concepts naturally belong to the same conversation. When a product change consistently affects the same set of concepts, that is a sign the boundary is right. When small changes scatter across unrelated areas, the module boundary is likely wrong.
(3) Low coupling
Even well-designed modules become hard to work with if they depend heavily on each other’s internals. Clear, minimal connections between modules keep changes local and reduce the need for constant alignment across teams.
Together, these principles ensure that modules scale not just the codebase, but the team’s ability to reason about the product.
3) Example: Learning Platform Modules (Catalog, Enrollment, Progress, Billing)
Imagine a corporate learning platform used by large organizations to train employees.
Learning Platform Modules
─────────────────────────
┌────────────────────────┐
│ Catalog (Module) │
│────────────────────────│
│ courses │
│ learningPaths │
│ prerequisites │
└────────────────────────┘
┌────────────────────────┐
│ Enrollment (Module) │
│────────────────────────│
│ enrollments │
│ eligibilityRules │
│ capacityLimits │
└────────────────────────┘
┌────────────────────────┐
│ Progress (Module) │
│────────────────────────│
│ completionTracking │
│ assessments │
│ certifications │
└────────────────────────┘
┌────────────────────────┐
│ Billing (Module) │
│────────────────────────│
│ plans │
│ invoices │
│ usageLimits │
└────────────────────────┘
Each module tells a coherent part of the product story.
- Catalog answers what can be learned.
- Enrollment decides who can access what, and under which conditions.
- Progress tracks what actually happened over time.
- Billing defines what the organization pays for and what limits apply.
4) Module Relationships: Upstream/Downstream, Shared Kernel, Anti-Corruption Layer
Modules do not exist independently. They form a system, and the direction of their relationships matters.
DDD gives names to these patterns:
- Upstream / downstream: one module defines rules, others follow.
- Shared kernel: a small set of shared concepts (used sparingly).
- Anti-corruption layer: translation when models should not leak across boundaries.
In the learning platform, those relationships might look like this:
Module Relationships (Learning Platform)
────────────────────────────────────────
┌────────────────────────┐
│ Catalog (Module) │
└──────────┬─────────────┘
│ defines available learning content
▼
┌────────────────────────┐
│ Enrollment (Module) │
└──────────┬─────────────┘
│ creates learning records
▼
┌────────────────────────┐
│ Progress (Module) │
└────────────────────────┘
┌────────────────────────┐
│ Billing (Module) │
└──────────┬─────────────┘
│ constrains access & limits
▼
┌────────────────────────┐
│ Enrollment (Module) │
└────────────────────────┘
This diagram makes an important idea visible: some modules set rules, others adapt to them.
- Catalog defines what exists; enrollment does not change that definition.
- Billing imposes constraints; enrollment must respect them.
- Progress observes outcomes; it does not redefine access rules.
For PMs, the practical question is simple but powerful:
Which module owns the rule, and which modules should adapt?
When that answer is unclear, rules start drifting silently across the product.
5) When to Refactor Modules (Signals PMs Notice First)
Module structures are not meant to be permanent.
As a product grows, the way features cluster often changes. What once felt like a clear module can slowly lose its focus. Responsibilities pile up, ownership becomes fuzzy, and changes that should be small start touching multiple areas.
PMs usually notice this before anyone else. Roadmap items become harder to scope. Teams collide on the same areas more often. Estimation feels off, not because the work is harder, but because boundaries no longer match the product.
Refactoring a module is how the model catches up with reality.
- Sometimes that means splitting a module that now contains two different ideas.
- Sometimes it means merging modules that always change together.
- Sometimes it means redrawing boundaries so decisions become local again.
This is not a correction of a past mistake. It is a response to new understanding.
8. Aggregates in DDD (Designing Consistency Boundaries)
1) What Is an Aggregate? (What Must Stay True Together)
In most products, a single user action rarely changes just one thing.
Now consider a concrete example in a hiring platform.
When an application moves to the INTERVIEW stage, two things usually happen together:
- the application status changes
- an interview time is assigned
These two changes only make sense together.
If the status is INTERVIEW but no interview time exists, the product feels broken. If an interview time exists but the status is still SCREENING, that is just as confusing.
Updating them independently creates states the product should never allow.
This is the problem that Aggregates exist to solve.
An Aggregate is simply a way to say:
“These things must always be checked and changed together.”
An Aggregate is a way to decide what must stay consistent together.
Inside an aggregate, teams treat changes as a single unit. Either all related rules pass and the change succeeds, or the system applies no change at all.
2) Aggregate Root, Boundary, and Internal Objects (Who Can Change What)
| Component | What it is | Why it exists |
|---|---|---|
| Aggregate Root | The main entity exposed to the outside | Ensures all rules are enforced in one place |
| Boundary | The conceptual line around the aggregate | Defines what must stay consistent together |
| Internal Objects | Entities or value objects inside the boundary | Prevents external code from bypassing rules |
The aggregate root is the only object the rest of the system may touch.
If another part of the product wants to make a change, it cannot directly update internal pieces. It must ask the root to do it. This gives the product a single place to say “yes” or “no” to a change.
The boundary defines how much must be correct at the same time.
Everything inside the boundary is assumed to change together. If one part becomes invalid, the whole change is rejected. This is what prevents states that look fine individually but make no sense when combined.
Internal objects exist to support the aggregate’s behavior, not to be managed independently.
They can have their own structure and rules, but they are never updated on their own. If external code could modify them directly, it would be easy to skip important checks and leave the product in a broken state.
Seen this way, an aggregate is not a technical container. It is a decision boundary.
3) Aggregate Design Rules (Invariants, Access, References, Transactions as a Guideline)
| Rule | What it means | Why it matters |
|---|---|---|
| Protect invariants | All rules that must always hold are enforced inside the aggregate | Prevents invalid states from entering the system |
| Restrict external access | Only the aggregate root is accessible from outside | Stops rules from being bypassed |
| Manage identity scope | The root has global identity; internal objects do not | Keeps boundaries clear and reduces coupling |
| Delete as a unit | Deleting the root removes the entire aggregate | Avoids orphaned or meaningless data |
| Transaction boundary | One aggregate is modified per transaction | Makes consistency costs explicit |
| Reference roots only | Aggregates refer to other aggregates by root only | Prevents hidden cross-boundary dependencies |
4) Example: Itinerary as an Aggregate (Travel Planner)
Imagine a personal travel itinerary planner used to organize multi-day trips.
In this product:
- an itinerary is created and owned by a single user or team
- all planning happens within one itinerary
- trips are not shared, reused, or managed across itineraries
- travelers, segments, and dates only matter in the context of that itinerary
- there is no need to track “which traveler across all trips booked the most hotels”
- segment history or changes are not tracked independently, only the current itinerary state matters
- travelers are not shared or referenced across multiple itineraries
- two segments with identical dates and locations are treated as interchangeable
In other words, the product does not treat travelers, segments, or schedules as independent objects. Everything is scoped to “this trip.”
At the center of this domain is the Itinerary. An itinerary is not just a list of dates. It represents a plan that must always make sense as a whole.
That is why it works well as an Aggregate.
Itinerary Aggregate
───────────────────
┌──────────────────────────────────────────┐
│ Itinerary (Aggregate Root, Entity) │
│------------------------------------------│
│ identity: itineraryId │
│ │
│ rules / invariants: │
│ - segments fit within trip dates │
│ - total duration always calculable │
│ - traveler changes keep plan valid │
└───────────────┬──────────────────────────┘
│ owns
┌───────┼───────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ TripSegment │ │ TravelerDetails │
│ (Value Object) │ │ (Value Object) │
│------------------│ │------------------│
│ dateRange │ │ traveler info │
│ location │ │ preferences │
└──────────┬───────┘ └──────────────────┘
│ uses
▼
┌──────────────────┐
│ DateRange │
│ (Value Object) │
│------------------│
│ startDate │
│ endDate │
└──────────────────┘
Code language: JavaScript (javascript)
The Itinerary itself is the aggregate root.
From a product perspective, this is the thing users recognize as “my trip”.
- users create an itinerary
- users add or remove segments
- users invite or remove travelers
- users view the itinerary as a single coherent plan
Every meaningful change goes through the itinerary. There is no concept of editing a segment or traveler in isolation without considering the itinerary it belongs to.
5) How to Size Aggregates (Smallest “Must Be Consistent” Set)
A common rule of thumb is to keep aggregates small.
Not because small is elegant, but because large aggregates carry real trade-offs.
| Question | What it reveals |
|---|---|
| Do these always change together? | If not, they likely do not belong together |
| What breaks if they are briefly out of sync? | Immediate breakage suggests one aggregate |
| Do users notice instantly? | User-visible inconsistency matters |
| Is coordination becoming expensive? | The aggregate may be too large |
The “right” size of an aggregate is:
the smallest set of objects that must change together to keep the domain rules valid
Anything larger increases coupling, contention, and cost.
6) Aggregate vs Module (Consistency vs Organization)
At first glance, aggregates often look like modules.
- Both group related concepts.
- Both draw boundaries.
- Both help reduce complexity.
But they exist for very different reasons.
| Aspect | Module | Aggregate |
|---|---|---|
| Primary goal | Organize code and responsibilities | Protect consistency rules |
| Boundary is about | Ownership and structure | What must stay correct together |
| Transaction scope | Can span multiple modules | Limited to one aggregate |
| Failure impact | Usually technical | User-visible product inconsistency |
| Change motivation | Maintainability | Product guarantees |
A module answers:
“Where should this logic live?”
An aggregate answers:
“Which changes must succeed or fail together?”
From a product perspective, this distinction matters because aggregates turn consistency into an explicit product decision. They force teams to choose where immediate correctness is required and where it is not.
7) Why Multiple Aggregates Appear (Different Product Promises)
Multiple aggregates emerge when a product starts making different kinds of promises at the same time.
- Some promises are about planning.
- Some are about ownership.
- Some are about money.
- Some are about long-lived records.
Trying to enforce all of them inside a single aggregate creates tension.
The moment you hear questions like:
- “What if this part succeeds but that part fails?”
- “Can this be retried independently?”
- “Does deleting this really delete everything?”
you are already dealing with multiple aggregates.
Each aggregate exists to protect one core set of invariants, and no single boundary can realistically hold all of them without becoming brittle.
In practice, multiple aggregates allow:
- different lifecycles to evolve independently
- failures to be isolated
- product rules to stay understandable over time
8) Example: Itinerary vs Booking vs Payment (Events and Asynchronous Reality)
Now imagine the same travel planner evolves.
Initially, users only planned trips.
Later, the product adds real bookings and payments. At this point, the domain changes fundamentally.
With bookings and payments:
- bookings may outlive itineraries
- payments may fail, retry, or be refunded
- cancellations may happen days later
- financial records must remain correct even if plans change
Planning and execution are no longer the same thing. This is where multiple aggregates become necessary.
| Aggregate | What it represents | What must always be true |
|---|---|---|
| Itinerary | A user’s travel plan | The plan makes sense as a whole |
| Booking | A real-world reservation | Status reflects an external commitment |
| Payment | Movement of money | No double charge, no lost refund |
A typical flow looks like this:
- User edits the itinerary → Itinerary aggregate is updated (planning rules are validated and saved atomically)
- User confirms a segment → Booking aggregate is created (initial state:
PENDING_PAYMENT) - User proceeds to payment → Payment aggregate attempts to charge asynchronously
- Payment succeeds → Payment success event is emitted
- Booking reacts to the payment result → Booking aggregate updates its own status to
CONFIRMED - User views the itinerary again → Itinerary reflects the updated booking status (by reference, without owning or controlling the booking)
At no point does one aggregate directly modify another’s internals. Each aggregate changes in its own transaction, coordinated through references and events, not shared state.
Each aggregate protects a different invariant, with a different cost of failure.
Multiple Aggregates
───────────────────
┌──────────────────────────────────────────┐
│ Itinerary (Aggregate Root) │
│------------------------------------------│
│ identity: itineraryId │
│ │
│ invariants: │
│ - segments fit trip dates │
│ - plan always coherent │
└───────────────┬──────────────────────────┘
│ references
▼
┌──────────────────────────────────────────┐
│ Booking (Aggregate Root) │
│------------------------------------------│
│ identity: bookingId │
│ │
│ invariants: │
│ - status reflects provider state │
│ - cancellation rules enforced │
└───────────────┬──────────────────────────┘
│ depends on
▼
┌──────────────────────────────────────────┐
│ Payment (Aggregate Root) │
│------------------------------------------│
│ identity: paymentId │
│ │
│ invariants: │
│ - charge once │
│ - refund correctly │
└──────────────────────────────────────────┘
9. Factories in DDD (Creation Rules, Defaults, and “Allowed to Exist”)
So far, we have focused on objects after they exist.
But every object has a starting point.
Before an aggregate can protect invariants, before services can coordinate behavior, something must be created correctly in the first place.
That moment of creation is often where product rules quietly leak. This is the problem that Factories exist to solve.
1) What Is a Factory? (Creation as a Product Decision)
A Factory is a domain concept that controls how objects come into existence.
It answers a simple but critical question:
“Under what conditions is this object allowed to exist at all?”
Creation is not just allocating memory or calling a constructor.
In most products, creating an object means:
- validating inputs
- enforcing invariants
- applying defaults
- connecting related objects in a valid way
A factory centralizes this responsibility.
If an object exists, the system can assume:
“It passed all creation rules.”
2) Why Creation Needs a Boundary (Multiple Creation Paths Problem)
Creation usually looks simple.
- a user clicks a button
- something appears
- the flow moves forward
The problem is that the same thing is often created from many different flows.
For example, a booking might be created:
- from the main booking flow
- from a promotion page
- by customer support
- from a rebooking feature
All of these seem to be just “create a booking.”, but they are different creation paths to the system.
Without a clear boundary, each path slowly makes its own assumptions:
- one applies default cancellation rules
- another forgets them
- one sets the initial status correctly
- another leaves it undefined
Over time, the product ends up with bookings that behave differently depending on how they were created.
This is why creation needs a boundary.
A Factory defines one rule:
“No matter where this is created, these conditions must be true.”
Factories do for creation what aggregates do for updates:
they prevent product rules from quietly drifting apart.
3) Factory Principles: Atomic Creation and Invariant Enforcement
Well-designed factories are not complicated, but they are deliberate.
They exist to make creation predictable, not flexible.
The principles below describe what a factory must guarantee, not how it is implemented.
| Principle | Meaning | Why it matters |
|---|---|---|
| Atomic creation | Creation fully succeeds or fully fails | Prevents objects from existing in a half-valid state |
| Invariant enforcement | All required rules are checked at creation | Stops invalid states before they enter the system |
| Explicit interface | Callers express intent, not construction steps | Allows creation rules to change without breaking flows |
The key point is that creation is not gradual.
A factory should never return an object that is “almost valid” or “valid for now.”
If creation fails, nothing should exist.
That clarity is what makes later reasoning about the product possible.
4) Factories and Aggregates (Why Aggregate Roots Need Controlled Creation)
Factories matter most where mistakes are hardest to undo: aggregate roots.
Aggregate roots:
- define consistency boundaries
- protect critical invariants
- represent concepts the product treats as authoritative
Because of this, they should never be created informally or indirectly.
In practice, this means:
- aggregate roots are created through factories
- internal objects are created as part of that process
- creation rules live in one place, not scattered across flows
This mirrors the role aggregates already play after creation.
Aggregates protect correctness over time.
Factories protect correctness at the moment of creation.
Together, they ensure that once something exists, the system never has to ask:
“Was this created the right way?”
5) Example: Subscription Factory (Trial vs Enterprise Rules)
In this SaaS product, a Subscription is treated as an aggregate.
A subscription is not considered valid unless certain conditions are already true at creation time.
For this domain:
- usage limits must be defined
- trial subscriptions must have an expiration
- enterprise subscriptions must not bypass approval rules
These are not behaviors applied later. They define whether a subscription is allowed to exist at all.
Subscription Factory
────────────────────
┌──────────────────────────────────────────┐
│ Subscription Factory │
│------------------------------------------│
│ decides at creation time: │
│ - which plan type is requested │
│ - which default limits apply │
│ - whether expiration is required │
│ - whether creation should be rejected │
└───────────────┬──────────────────────────┘
│ creates
▼
┌──────────────────────────────────────────┐
│ Subscription (Aggregate Root) │
│------------------------------------------│
│ identity: subscriptionId │
│ │
│ guaranteed at existence: │
│ - limits are defined │
│ - expiration rules satisfied │
│ - plan-specific constraints respected │
└──────────────────────────────────────────┘Code language: JavaScript (javascript)
The important point is when these rules are applied.
By the time a Subscription aggregate exists:
- defaults are already fixed
- invalid combinations have already been rejected
- no follow-up steps are required to “complete” it
The Subscription aggregate never exists in a “raw” or incomplete form. If it exists, it already satisfies all creation rules.
6) Factory vs Constructor (When Each Is Appropriate)
Every object is created somehow. A constructor is the most basic way to create an object.
It takes given values and assembles them into an instance.
In product terms, using a constructor means:
“If the required fields are provided, the object can exist.”
A constructor and a factory both create objects, but they answer different questions.
- A constructor creates an object.
- A factory creates a valid product concept.
| Aspect | Constructor | Factory |
|---|---|---|
| What it does | Assembles an object from given values | Decides whether an object may exist |
| Creation rule | “If values are provided, create it” | “Create only if rules are satisfied” |
| Business rules | Usually minimal or none | Explicitly enforced at creation |
| Variants | Not suited for multiple variants | Handles multiple creation variants |
| Change tolerance | Creation logic is hard to evolve safely | Creation rules can evolve in one place |
| Product meaning | Creation is a technical step | Creation is a product decision |
| Typical signal | “We just need an object here” | “This must never exist incorrectly” |
If creating the object incorrectly can cause user confusion, revenue loss, or policy violations,
creation should go through a factory.
Rule of thumb
- If an object can safely exist with any reasonable data → a constructor is enough.
- If an object should never exist unless certain conditions are met → use a factory.
10. Repositories in DDD (Working with Domain Objects Over Time)
Factories answer one question:
“Under what conditions is this object allowed to exist?”
Repositories answer a different one:
“How do we work with this object over time without breaking the domain rules?”
Once objects exist, products rarely interact with them just once. They are loaded, updated, saved, and revisited across many user actions.
Repositories define how that ongoing interaction happens safely.
1) What Is a Repository? (Domain Interface Over Storage Details)
A Repository is a domain-layer component that manages domain objects after they are created and until they are no longer needed.
It provides an interface that allows the rest of the system to request and work with domain objects using domain language, while hiding the complexity of the underlying storage mechanism.
In practice, a repository is responsible for handling:
- storing existing objects
- retrieving them
- removing them when necessary
all from the perspective of the domain model, not the database.
The domain does not know or care whether objects are stored in SQL, NoSQL, caches, external APIs, or any combination of them.
By hiding persistence details and exposing intent-driven operations, repositories allow clients to focus on domain concepts instead of storage concerns.
This is what keeps domain logic stable even as storage strategies and infrastructure evolve.
2) Why Repositories Matter (Product Queries, Performance, and Rules)
Repositories sit at an important intersection:
- product requirements (what must be queried)
- performance constraints (what must be fast)
- domain rules (what must stay consistent)
Without repositories, these concerns tend to mix:
- domain logic starts depending on query details
- performance optimizations leak into product rules
- changes in storage ripple through the system
A repository absorbs that complexity and presents a clean, stable surface to the domain. This is why repositories are one of the most practical DDD concepts for product management.
3) Repository vs Factory (Birth vs Lifecycle)
Factories and repositories are often confused because both “deal with objects.”
They do very different things.
| Factory | Repository |
|---|---|
| Decides if a new object may exist | Manages objects that already exist |
| Enforces creation-time rules | Preserves correctness over time |
| Focused on birth | Focused on retrieval and persistence |
Creation and retrieval answer different product questions. Keeping them separate makes behavior easier to reason about.
4) Repository Design Principles (One per Aggregate Root, Domain-Centric Methods)
Well-designed repositories are simple, but strict.
| Principle | What it means | Why it matters |
|---|---|---|
| One repository per aggregate root | Each repository manages exactly one aggregate root. Internal objects are never loaded or queried directly. | Preserves the aggregate’s consistency boundary and prevents bypassing invariants. |
| Domain-centric interfaces | Repository methods express business intent, not query mechanics. Questions should be recognizable at the product level. | Keeps domain language clean and prevents database concerns from leaking upward. |
| No transaction control | Repositories do not decide when to commit or how multiple aggregates are coordinated. | Keeps repositories focused on access, not orchestration or flow control. |
5) Example: Subscription Repository (Conceptual)
A Subscription Repository exists to work with subscriptions that already exist.
It answers questions like:
- “Which subscription is this?”
- “Does this account already have one?”
- “This subscription changed — can you store it?”
It does not decide whether a subscription should exist or whether a change is allowed.
Those decisions belong to the factory and the aggregate.
Subscription Repository
───────────────────────
┌──────────────────────────────────────────┐
│ Subscription Repository │
│------------------------------------------│
│ domain questions it answers: │
│ - load subscription by ID │
│ - find subscriptions for an account │
│ - save updated subscription │
└───────────────┬──────────────────────────┘
│ returns / stores
▼
┌──────────────────────────────────────────┐
│ Subscription (Aggregate Root) │
└──────────────────────────────────────────┘
The repository intentionally avoids talking about how subscriptions are stored or retrieved.
Details such as storage technology, query language, or performance optimizations are real concerns, but they sit outside the domain contract the repository provides.
11. Final Checklist: Domain Model Quality Checks (for PMs)
Use this checklist at PRD time, during design reviews, and when a feature feels “simple but oddly expensive.”
1) Language and Concepts
- Every core term can be explained in one sentence (not just named)
- Key terms are used consistently across PRDs, tickets, and engineering discussions
- When disagreements happen, it is clear whether they are about behavior or definitions
2) Entities vs Value Objects
- For each concept: if it changes, it is still “the same thing over time” (Entity)
- Concepts that do not require continuity are modeled as replaceable descriptions (Value Objects)
- No Value Object is quietly treated like an Entity (tracked, edited, audited)
3) Relationships
- Most relationships are one-directional by default
- Large one-to-many relationships include qualifiers (date, role, state) when needed
- Direct associations that do not affect behavior are removed
- Bidirectional relationships exist only when justified by real domain rules
4) Services
- Cross-concept business decisions live in domain services, not inside single entities
- Service names express business intent, not technical steps
- Domain services remain stateless and do not own data
5) Modules
- Modules are organized around product concepts, not technical layers
- Concepts inside a module tend to change together
- Module dependencies are limited, intentional, and directional
6) Aggregates
- Each aggregate boundary represents the smallest set that must change together to stay valid
- Invariants are enforced inside the aggregate boundary
- External access is restricted to the aggregate root
- Most transactions modify only one aggregate
- Cross-aggregate coordination happens via references and events, not shared state
- The cost of immediate consistency is an explicit product decision
7) Multiple Aggregates
- Different product promises (planning, ownership, money, records) are separated
- Aggregates with different failure modes have independent lifecycles
- Long-lived or high-risk concepts (payments, bookings) are not mixed with planning concepts
8) Factories
- There is exactly one correct way for critical objects to come into existence
- Creation-time rules (defaults, validity, forbidden combinations) are centralized
- Creation fully succeeds or fully fails
- Multiple creation paths do not produce inconsistent objects
9) Repositories
- Each aggregate root has its own repository
- Internal objects are never loaded or queried directly
- Repository methods reflect business questions, not database mechanics
- Repositories focus on access and persistence, not transactions or flow control
10) Sanity Signals (Quick Smell Tests)
- “How is this state even possible?” → missing invariant or creation boundary
- “Same thing behaves differently depending on where it was created” → factory issue
- “A small change touches many unrelated areas” → boundary or relationship problem
- “We need to query deep internals everywhere” → aggregate or repository leak
- “Everything must be immediately consistent” → multiple aggregates not acknowledged
If the team cannot clearly state what must be true, when it must be true, and who enforces it, the domain model will leak—no matter how clean the code looks.
12. Conclusion: Why This Matters
A domain model breaks down when the team cannot clearly explain three things.
First, what must always be true in the product, regardless of feature or flow.
Second, when those rules apply, for example only at creation time, during updates, or across the entire lifecycle.
Third, which part of the model is responsible for enforcing them, such as an aggregate, a factory, or a service.
When these answers are unclear, rules slowly spread across UI logic, services, and storage code. The system may still work, but it becomes fragile and expensive to change.
A well-designed domain model keeps these responsibilities explicit, so product decisions remain understandable as the system evolves.

