Clean Architecture, Domain Driven Design
What is a Domain?
A domain is simply a part of the real world that we want to model in software. It cannot be directly “poured over the keyboard” and turned into code. Instead, we must create an abstraction of the domain—an organized way of representing real-world knowledge in terms that software can understand.
We begin learning about the domain by talking with domain experts and gathering raw knowledge. However, this knowledge by itself is not easily transformed into software constructs. To make it useful, we build a mental blueprint, an abstraction of the domain that guides our design.
At first, this blueprint is always incomplete. But as we iterate—by refining our understanding, testing assumptions, and collaborating with experts—the model becomes more accurate, richer, and clearer.
Example:
In an e-commerce system, the domain includes concepts such as orders, customers, payments, and shipping. You cannot directly code “shopping behavior,” but by abstracting it, you can model it with entities (Customer, Order), value objects (Address, Money), and aggregates (OrderAggregate controlling line items, payments, and status).
The Strategic Phase of Domain-Driven Design (DDD)
Domain-Driven Design (DDD) emphasizes that large systems should not rely on one unified model for everything. Instead, it introduces the concept of Bounded Contexts—separate models tailored for specific areas of the system. Each context reflects the unique language and rules of its subdomain.
Purpose of the Strategic Phase
The strategic phase of DDD focuses on high-level domain understanding. It helps us:
-
Identify the core domain — the most critical part of the business where differentiation and innovation matter most.
-
Define bounded contexts — each with its own domain model, ensuring clarity and preventing conflicts.
-
Map relationships between contexts — such as partnerships, dependencies, or integrations.
-
Improve communication — by aligning technical and business teams through a shared Ubiquitous Language.
Example:
Imagine building a project management platform. Instead of one massive model, you create bounded contexts:
-
Task Management Context (tasks, deadlines, assignments)
-
Collaboration Context (forums, discussions, messages)
-
Billing Context (subscriptions, invoices, payments)
Each context has its own model and language, but together they form a cohesive system. The strategic phase defines these boundaries and ensures smooth interaction between them.
✅ This refined version:
-
Explains domain in simple, relatable terms.
-
Adds a practical example (e-commerce & project management).
-
Clearly distinguishes between raw knowledge, abstraction, and blueprinting.
-
Makes the strategic phase of DDD less abstract and more actionable.
Key Concepts
1. Core Domain
The most important part of the business that provides a competitive advantage.
Is a part of the business Domain that is of primary importance to the success of the organization.
Example: In an e-commerce system, the pricing and discount engine might be the core domain.
2. Bounded Contexts
defines the limits within which a specific domain model is consistently understood (specific business domain terms), ensuring clarity (each term has a specific meaning in this BC), consistency, and separation from other models in different contexts.
Example:
In an e-commerce system, the term “Order” might mean something different to the Shipping team than it does to the Inventory team. Each of these would have its own bounded context with its own model of what an “Order” is.
3. Context Mapping
Defines how different bounded contexts interact with each other.
In Domain-Driven Design (DDD), context mapping is a way to visualize how different parts (bounded contexts) of your system interact with each other and with external systems.
A Bounded Context is a clearly defined area of your application where a specific domain model applies, with its own rules, language, and data structures.
A context map helps you answer questions like:
-
Where are the boundaries? (Which teams or services own which parts of the system?)
-
How do they communicate? (Direct calls, APIs, messaging, events?)
-
What are the relationships? (Do they depend on each other? Who leads and who follows?)
-
What translations are needed? (Do you have to convert data formats, rename fields, or change units?)
When drawing your context map:
-
Identify each bounded context in your project (core domains, supporting domains, external systems).
-
Draw them as separate boxes with clear boundaries.
-
Show relationships with arrows or lines indicating the flow of communication or dependencies.
-
Label integration types — for example:
-
REST API
-
Message Queue / Event Bus
-
Database Replication
-
-
Mark translation requirements (e.g., “Convert USD to SAR” or “Map internal user ID to external system ID”).
-
Include teams — note which team owns or maintains each bounded context.
💡 Remember: this diagram is not just for documentation. Your code, integrations, and workflows should reflect what’s in the diagram.
Legend:
-
Boxes = bounded contexts
-
Lines/arrows = integration direction
-
Labels = type of integration (API, Event, Import, etc.)
-
Notes = translation/formatting rules
4. Ubiquitous Language
A shared, well-defined language between developers, domain experts, and stakeholders.
Example: Using "Order Confirmation" instead of vague terms like "Processed Order."
Example Dialogue: Aligning on a Ubiquitous Language
BA: Let’s start by going through the process of handling orders. Could you walk me through it?
C: Sure. A User browses the menu, puts food into their Basket, and then confirms the Purchase.
BA: Okay — you said “User,” “Basket,” and “Purchase.” Earlier you mentioned “Customer” and “Cart” in our email. Should we agree to consistently call the person placing an order a Customer and the temporary food collection a Cart?
C: Yes, “Customer” and “Cart” are fine.
BA: Great. Once the order is placed, what happens next?
C: The request goes to the Vendor, and they can either Approve or Decline it.
BA: Just to clarify — when you say “Vendor,” is that the same as the Restaurant?
C: Yes, I mean the restaurant.
BA: Let’s stick with “Restaurant” everywhere so we avoid confusion. And instead of mixing “Approve” and “Accept,” which one feels more natural to you?
C: Let’s go with “Accept.”
BA: Perfect. What happens after the restaurant accepts?
C: We charge the Client’s credit card.
BA: The “Client” here is still the Customer, correct?
C: Yes, the same person.
BA: Then we’ll use “Customer” consistently instead of “Client.”
BA: After payment, what’s next?
C: The kitchen starts Cooking the order, then the Courier picks it up.
BA: Is “Cooking” the same as “Preparing,” and is “Courier” the same as “Delivery Driver”?
C: Yes to both.
BA: Okay — let’s use “Preparing” for the kitchen stage and “Delivery Driver” for the person delivering.
BA: How does delivery tracking work?
C: The driver marks it as On Route and then Completed.
BA: To keep it consistent with earlier terms, can we use “On the Way” instead of “On Route” and “Delivered” instead of “Completed”?
C: Yes, that works.
Final Agreed Ubiquitous Language
Term | Definition |
---|---|
Customer | Person placing an order |
Cart | Temporary collection of items before placing an order |
Order | A confirmed request for food items |
Restaurant | The food provider receiving the order |
Accept / Reject | Restaurant’s decision on whether to fulfill the order |
Payment | Process of transferring money from customer to restaurant |
Credit Card / Digital Wallet | Supported payment methods |
Paid | Status after successful payment |
Preparing | Restaurant is making the food |
Delivery Driver | Person delivering the order |
On the Way | Driver is transporting the order to the customer |
Delivered | Order has reached the customer |
Building a language like that has a clear outcome: the model and the language are strongly interconnected with one another. A change in the language should become a change to the model.
5. Subdomains
Subdomain is one specific area of the whole business domain(i.e.: Product Catalog, Orders, Invoicing, and Shipping).
The domain is broken into three subdomains:
Core Domain – The most valuable part (e.g., recommendation engine for Netflix).
Supporting Subdomain – Necessary but not the main focus (e.g., user management).
Generic Subdomain – Standard functionalities that can be outsourced (e.g., payment processing)
Why is the Strategic Phase Important?
Ensures clear separation of concerns between different parts of the system.
Prevents miscommunication by enforcing a shared ubiquitous language. Helps design a scalable and maintainable architecture.
Enables better team collaboration by defining ownership of bounded contexts.
The tactical phase of DDD
Tactical DDD is when you define your domain models with more
precision. The tactical patterns are applied within a single bounded context.
In a microservices architecture, where each bounded context is a microservice
candidate, we are particularly interested in the entity and aggregate patterns.
What is abstraction In manufacturing or engineering, abstraction refers to the process of simplifying a complex system by focusing on its essential features while hiding or ignoring irrelevant details. It helps engineers and designers manage complexity, improve communication, and build systems more efficiently.
Practical Examples:
CAD Modeling: A 3D model may abstract real-world objects by showing only the necessary parts or dimensions for a specific task (e.g., omitting internal components in a simplified assembly view).
System Design: Engineers abstract different layers of a system (like mechanical, electrical, software) to work on them separately before integrating them.
Simulation Models: A manufacturing process may be represented in a simplified simulation to analyze performance or test scenarios without modeling every physical detail.
Why It Matters:
Makes complex systems manageable
Improves problem-solving and design clarity
Enables modular thinking (e.g., interchangeable parts or reusable designs)
What is this abstraction in software architecture? abstraction refers to structuring the system by focusing on high-level components and their interactions, while hiding the internal details of those components. It helps architects manage complexity by organizing the system into logical layers, modules, or services — each with a clear purpose and interface.
It is a model, a model of the domain. According to Eric Evans, a domain model is not a particular diagram; it is the idea that the diagram is intended to convey. It is not just the knowledge in a domain expert’s head; it is a rigorously organized and selective structuring of that knowledge. A diagram can represent and communicate a model, as can carefully written code, as can an English sentence.
We need to communicate this model with domain experts, with fellow designers, and with developers. The model is the essence of the software, but we need to create ways to express it, to communicate it with others. We are not alone in this process, so we need to share knowledge and information, and we need to do it well, precisely, completely, and without ambiguity. There are different ways to do that.
1. Graphical: diagrams, use cases, drawings,
pictures, etc.
2. Writing: We write down our vision about the domain.
3. Language: We can and we should create a language to communicate
specific issues about the domain. We will detail all these later, but the main
point is that we need to communicate the model.
When we have a model expressed, we can start doing code design. This is different from software design. It’s one thing to move a painting more to the left, and a completely different thing to tear down one side of the house in order to do it differently. Nonetheless, the final product won’t be good without good code design. Here code design patterns come in handy, and they should be applied when necessary. Good coding techniques help to create clean, maintainable code.
Software design is like creating the architecture of a house, it’s about the big picture. On the other hand, code design is working on the details, like the location of a painting on a certain wall. Code design is also very important, but not as fundamental as software design. A code design mistake is usually more easily corrected, while software design errors are a lot more costly to repair. |
Building a language like that has a clear outcome: the model
and the language are strongly interconnected with one another. A change
in the language should become a change to the model.
However, in real life, such a dialog is much more verbose, and people very often talk about things indirectly, or enter into too much detail, or choose the wrong concepts; this can make coming up with the language very difficult.
To begin to address this, all team members should be aware
of the need to create a common language and should be reminded to stay focused
on essentials and use the language whenever necessary. We should use our own
jargon during such sessions as little as possible, and we should use the
Ubiquitous Language because this helps us communicate clearly and precisely.
Some may say that UML is good enough to build a model upon.
Indeed, it is a great tool to write down key concepts as classes and to
express relationships between them. You can draw four or five classes on a
sketchpad, write down their names, and show the relationships between them.
It’s very easy for everyone to follow what you are thinking, and a graphical
expression of an idea is easy to understand. Everyone instantly shares the same
vision about a certain topic, and it becomes simpler to communicate based on
that. When new ideas come up, the diagram is modified to reflect the
conceptual change.
UML diagrams are very helpful when the number of elements involved is small. But UML can grow like mushrooms after a nice summer rain. What do you do when you have hundreds of classes filling up a sheet of paper as long as Mississippi? It’s hard to read even by software specialists, not to mention domain experts. They won’t understand much of it when it gets big, and it does so even for medium size projects. Also, UML is good at expressing classes, their attributes, and the relationships between them. However, the classes’ behavior and the constraints are not so easily expressed. For that UML resorts to text placed as notes into the diagram.
So UML cannot convey two important aspects of a model:
1. the meaning of the concepts it represents.
2. what the objects are supposed to
do.
But that is OK since we can add other communication tools to do it. We can use documents. One advisable way of communicating the model is to make some small diagrams each containing a subset of the model. These diagrams would contain several classes and the relationship between them. That already includes a good portion of the concepts involved.
Then we can add text to the diagram. The text will explain behavior and constraints which the diagram cannot.
Each such subsection attempts to explain one important aspect of the domain, it points a “spotlight” to enlighten one part of the domain. Those documents can be even hand-drawn because that transmits the feeling that they are temporary, and might be changed in the near future, which is true because the model is changed many times in the beginning before it reaches a more stable status.
It might be tempting to try to create one large diagram over the entire model. However, most of the time such diagrams are almost impossible to put together. And furthermore, even if you do succeed in making that unified diagram, it will be so cluttered that it will not convey the understanding better than the collection of small diagrams.
Any domain can be expressed with many models, and any model can be expressed in various ways in code. For each particular problem, there can be more than one solution. Which one do we choose? Having one analytically correct model does not mean the model can be directly expressed in code. Or maybe its implementation will break some software design principles, which is not advisable. It is important to choose a model which can be easily and accurately put into code. The basic question here is: how do we approach the transition from model to code? One of the recommended design techniques is the so-called analysis model, which is seen as separate from code design and is usually done by different people.
Analysis Model Design Technique
The Analysis Model Design Technique is a structured approach used in software engineering to define, analyze, and document system requirements before moving to the design and implementation phases. It ensures that the system meets business needs by breaking down complex requirements into well-defined components.
If the analysts work independently, they will
eventually create a model. When this model is passed to the designers, some of
the analysts’ knowledge about the domain and the model is lost. While the model
might be expressed in diagrams and writing, chances are the designers won’t
grasp the entire meaning of the model, the relationships between some
objects, or their behavior. There are details in a model which are not easily
expressed in a diagram and may not be fully presented even in writing. The
developers will have a hard time figuring them out. In some cases, they will
make some assumptions about the intended behavior, and it is possible for them
to make the wrong ones, resulting in the incorrect functioning of the program.
A better approach is to closely relate domain modeling and
design. The model should be constructed with an eye open to the software and
design considerations. Developers should be included in the modeling process.
|
The main idea is to choose a model that can be appropriately expressed in software so that the design process is straightforward and based on the model. Tightly relating the code to an underlying model gives the code meaning and makes the model relevant.
Getting the developers involved provides feedback. It makes sure that the model can be implemented in software. If something is wrong, it is identified at an early stage, and the problem can be easily corrected. Those who write the code should know the model very well, and should feel responsible for its integrity.
They should realize that a change to the code implies a
change to the model; otherwise they will refactor the code to the point where
it no longer expresses the original model. If the analyst is separated from the
implementation process, he will soon lose his concern about the limitations
introduced by development. The result is a model which is not practical.
Those who contribute in different ways must consciously engage those who touch
the code in a dynamic exchange of model
ideas through the Ubiquitous Language.
If the design, or some central part of it, does not map to
the domain model, that model is of little value, and the correctness of the
software is suspect. At the same time, complex mappings between models and
design functions are difficult to understand and, in practice, impossible to
maintain as the design changes.
OOP
To tightly tie the implementation to a model usually
requires software development tools and languages that support a modeling
paradigm, such as object-oriented programming. Object-oriented programming is
suitable for model implementation because they are both based on the same
paradigm. Object-oriented programming provides classes of objects and
associations of classes, object instances, and messaging between them. OOP
languages make it possible to create direct mappings between model objects with
their relationships, and their programming counterparts.
PP
Some areas, like mathematics, work well with procedural programming because they’re mostly about calculations. But most real-world domains are more complex and can’t be expressed as just algorithms, so procedural programming isn’t ideal for model-driven design.
The DDD four conceptual layers
In many applications, most code isn’t about the core business logic—it’s about databases, files, networks, or the user interface. If this “infrastructure” code gets mixed with the business logic, the system becomes hard to understand, design, and automatically test. Even small UI changes can accidentally change business rules, and changing a rule might require editing code in multiple unrelated places.
The solution is to separate the program into layers. Keep domain logic in its own layer, apart from UI, application services, and infrastructure. This separation keeps the domain model focused, easier to improve, and able to clearly express business knowledge. A typical domain-driven design uses four conceptual layers:
User Interface (UI) Layer – Handles user interaction and displays information.
Application Layer – Coordinates tasks and delegates work to the domain layer.
Domain Layer – Contains the business logic and domain model.
Infrastructure Layer – Provides technical capabilities like database access, file systems, and messaging.
Example: Interaction Between Application, Domain, and Infrastructure Layers
Imagine a user wants to book a flight route. The process might work like this:
-
User Request (UI Layer → Application Layer)
-
The user enters the desired route, dates, and passenger details in the booking interface.
-
The UI sends this information to an application service in the application layer, such as
FlightBookingService
.
-
-
Application Layer Orchestration
-
The
FlightBookingService
receives the request and coordinates the booking process. -
It retrieves the necessary domain objects (e.g.,
Flight
,Route
,Passenger
) from the infrastructure layer via repositories likeFlightRepository
orRouteRepository
.
-
-
Domain Logic Execution (Domain Layer)
-
The application service invokes methods on these domain objects.
-
For example, the
Flight
object might runCheckSecurityMargins()
to ensure there is enough separation from other already booked flights. -
The domain layer enforces business rules such as seat availability, flight scheduling constraints, and passenger eligibility.
-
-
Decision and State Update (Domain Layer)
-
If all checks pass, the domain objects update their internal state to reflect that the booking is “decided” or “confirmed”.
-
These changes happen entirely within the domain layer, keeping business logic isolated from infrastructure concerns.
-
-
Persistence (Application Layer → Infrastructure Layer)
-
After the domain objects are updated, the application service calls repository methods to save the new booking state back to the infrastructure layer.
-
The infrastructure layer handles the actual persistence, such as storing the updated objects in a database.
-
This structured approach shows clearly how each layer has its own responsibility:
-
The UI layer handles input/output with the user.
-
The application layer coordinates actions and workflow.
-
The domain layer enforces business rules and state changes.
-
The infrastructure layer manages technical concerns like data storage.
Entities
Entities are objects that have a unique identity. For example, a person’s identity might be a combination of name, birth date, parents’ names, and address, or a Social Security number in the US. A bank account’s identity is usually the account number. The key point is that entities must be distinguishable by their identity, so the system can tell different entities apart and recognize when two references mean the same entity. If this fails, the system’s integrity breaks down.
Value Objects
Value Objects represent descriptive aspects of a domain without identity. They focus on attributes rather than being unique objects. For example, an address (street, city, state) can be a Value Object linked to a customer. Value Objects should be:
-
Simple and small
-
Immutable if shared
-
Easy to copy or discard without side effects
They can contain other Value Objects and sometimes references to Entities. Attributes that logically belong together form a Value Object, rather than being scattered.
Services
Sometimes, domain behavior (actions) doesn’t naturally belong to any single Entity or Value Object. For example, transferring money between accounts doesn’t fit well inside either account object. In such cases, Services are used.
Characteristics of Services:
-
Represent domain operations that don’t fit naturally into Entities or Value Objects.
-
Work with multiple domain objects.
-
Are stateless (no internal state).
Services group related behaviors clearly and avoid cluttering Entities or Value Objects with unrelated responsibilities.
Example:
Name: OrderPaymentService
Purpose:
Handles the process of charging a Customer for an Order once the Restaurant accepts it.
This logic doesn’t belong to:
-
Customer (because a customer doesn’t charge themselves)
-
Restaurant (because the restaurant doesn’t directly process payments)
-
Order (because the payment process involves an external payment provider and other domain rules)
Since the payment process:
-
Involves multiple entities (
Order
,Customer
,Restaurant
) -
Has no internal state of its own
-
Is a standalone domain concept
…it qualifies as a Domain Service.
Service Interface (in Ubiquitous Language terms)
How This Fits the Domain
-
Inputs:
Order
(Entity),PaymentMethod
(Value Object) -
Output:
PaymentReceipt
(Value Object) -
Rules: Only process payment for accepted orders, use external payment gateway, update order status.
-
Stateless: The service itself doesn’t store state — it operates on entities and value objects.
Clean architecture layers
DM: Data Model, DTO: Data Transfer
Objects
As
presented above, we have 4 layers, in our use case we are going to focus on 3
·
A
presentation layer (UI) : that interacts with external requests (e.g. API/ UI)
·
A business
layer (application layer): that implements use cases or business logic. This is
where your handlers are (if you use CQRS/MediatR), validations and mapping
takes place
·
An
infrastructure layer (Data access): This is where you interact with the
Database with tools such as DbContext
Why use DTOs?
We just discussed the 3 layers that we will be using,
and you might already notice that the data access layer will not be referenced
by the presentation layer.
So
to reflect the entities in your presentation layer, you will use DTOs.
What are DTOs? Just the same classes as your
entities with perhaps more/fewer fields
A Data Transfer Object (DTO) is a simple, serializable object used to carry data between different layers or services in an application. Its primary purposes are: Layer Separation: DTOs decouple the internal domain models from external layers (such as the API or UI). This ensures that if an internal entity changes, the external contract (API or UI data format) can remain stable. Controlled Data Exposure: DTOs allow selective sharing of information. For example, you may omit sensitive fields such as a user’s account balance when sending data to an external consumer. Adaptation for Clients: DTOs can reshape or combine data to better fit the needs of the receiving layer without changing the underlying domain model. Trade-offs: Using DTOs introduces the need for data mapping between the domain model and the DTO. This adds complexity, but it can be managed efficiently with tools like AutoMapper that automate the mapping process.
|
Modules
In Domain-Driven Design (DDD), a Module is a way of grouping related concepts, classes, and tasks into a cohesive unit to reduce complexity in large, evolving systems.
As a domain model grows, it becomes harder to understand and communicate about the whole model. Modules help by:
-
Organizing related elements into meaningful groups.
-
Clarifying relationships between concepts.
-
Reducing mental overload when navigating the system.
Naming Conventions
Modules often follow a hierarchical naming structure, which:
-
Makes logical organization visible in the name.
-
Helps avoid namespace collisions with third-party or external modules.
-
Usually starts with the organization’s name or domain name (often reversed for uniqueness).
Example:
In C#, the structure may simply start with the organization’s name:
Typical Module Hierarchy
-
Domain Layer Modules:
-
domain.model
– Contains entities, value objects, aggregates, reusable interfaces, and abstract classes. -
domain.service
– Contains domain services (if placed outside themodel
module).
-
Example:
-
Application Layer Modules:
-
Organized by service type if needed (e.g.,
application.team
,application.product
). -
If there are few services, they may remain in the main application module.
-
Example:
com.saasovation.agilepm.application.team
-
Presentation Layer Modules:
-
resources
– Contains RESTful resource providers. -
resources.view
(orresources.presentation
) – Contains pure presentation components.
-
Example:
Example: Agile Project Management Context
These modules contain the core domain concepts of Scrum for Agile project management.
Key Principles for Using Modules
-
Group by Concept, Not by Technical Layer – Each module should have a meaningful domain-related name.
-
Use Hierarchical Naming – Reflect the logical organization in your namespaces or packages.
-
Modularize Only When Needed – Avoid over-segmentation unless it improves clarity or maintainability.
-
Separate Concerns Clearly – Domain model, domain services, application services, and presentation resources should be organized distinctly.
Value Objects
When you are trying to decide whether a concept is a Value, you should
determine whether it possesses most of these characteristics:
• It measures, quantifies, or describes a thing in the domain.
• It can be maintained as immutable.
• It models a conceptual whole by composing related attributes as an integral unit.
• It is completely replaceable when the measurement or description changes.
• It can be compared with others using Value equality.
• It supplies its collaborators with Side-Effect-Free Behavior [Evans].
Aggregates
An Aggregate is a cluster of domain objects (Entities and Value Objects) that are treated as a single unit for data changes and consistency. Each Aggregate has a boundary that defines what is inside and outside. Within the boundary:
-
One Aggregate Root (Entity) exists, which is the only entry point from outside.
-
Other Entities inside have local identities meaningful only within the Aggregate.
-
Value Objects and Entities inside can reference each other, but external objects may only reference the Aggregate Root.
The Aggregate Root is responsible for enforcing invariants (business rules that must always remain consistent).
Key Rules for Aggregates
-
Root-Only Access:
External objects access the Aggregate only through the root. -
Consistency Boundaries:
Aggregates define transactional consistency boundaries.-
Within the boundary → transactional (immediate) consistency.
-
Across boundaries → eventual consistency.
-
-
One Aggregate per Transaction:
Only one Aggregate instance should be modified in a single transaction. If multiple need updates, prefer eventual consistency via domain events. -
Reference by Identity:
Aggregates reference other Aggregates only by ID, not by direct object references. -
Model True Invariants:
Aggregates should be shaped around the business rules that must remain consistent. -
Design Small Aggregates:
Favor small Aggregates (often just a Root with Value Objects). Larger Aggregates reduce scalability and performance.
Design Guidelines
-
Aggregate Root Responsibilities:
-
Enforce all business rules (invariants).
-
Control creation, update, and deletion of internal members.
-
Ensure no external object can change internal state directly.
-
-
Domain Services vs. Aggregates:
-
Aggregates encapsulate rules that fit naturally within one consistency boundary.
-
Use Domain Services for behaviors that span multiple Aggregates.
-
-
Application Services Role:
-
Orchestrate use cases.
-
Retrieve Aggregates via Repositories.
-
Delegate business logic to Aggregates or Domain Services.
-
Trade-Offs and Special Cases with Aggregates
1. Large Aggregates → Harder to Scale and Maintain Performance
Explanation:
Large Aggregates contain too many entities and value objects under one root. This makes every transaction heavier, increases lock contention, and reduces scalability.Example:
Imagine an E-commerce “Order” Aggregate that includes not only theOrder
root but also everyOrderItem
, thePaymentTransaction
, theShipment
, and even theInvoice
as part of the same Aggregate.
Any update (like changing the shipping address) would lock the entire Aggregate, creating performance bottlenecks.
Better Approach: Split it into smaller Aggregates — e.g.,Order
,Payment
, andShipment
— each with its own root.
2. Eventual Consistency → Recommended When Multiple Aggregates Need Coordination
Explanation:
Instead of forcing multiple Aggregates to update in a single transaction (which is costly and violates the one-aggregate-per-transaction rule), use domain events to achieve eventual consistency.Example:
In the food delivery domain:The Order Aggregate is updated when a customer places an order.
It then publishes an
OrderPlaced
event.The Restaurant Aggregate subscribes to that event and updates its preparation queue.
Both Aggregates remain consistent eventually, without being locked in the same transaction.
3. Breaking the Rules May Be Justified in Special Cases
a. Batch Creation in the UI (When Invariants Are Not at Risk)
Explanation:
Sometimes the UI allows users to create multiple objects at once (batching). If each object is a valid Aggregate by itself and doesn’t depend on others for invariants, batching is acceptable.Example:
A Project Management tool allows a user to create 10 Backlog Items at once. EachBacklogItem
is its own Aggregate, so whether created individually or in batch, invariants remain intact.
b. Lack of Infrastructure (No Messaging/Event Mechanisms)
Explanation:
Eventual consistency usually depends on messaging systems or async processing. If the project lacks this infrastructure, developers may need to temporarily update multiple Aggregates in one transaction.Example:
In a legacy system without message queues, updating bothCustomer
andInvoice
in the same transaction may be unavoidable to ensure payment records stay in sync.
c. Enterprise Constraints (Global Transactions Enforced)
Explanation:
Some enterprises mandate strict two-phase commit transactions across services for auditing or policy reasons. This forces multiple Aggregates into the same transaction even though it hurts scalability.Example:
A Banking system requiresAccount
andLedger
entries to be updated atomically in the same global transaction for compliance.
d. Performance (Using Direct References for Queries)
Explanation:
Normally, Aggregates should reference others only by identity. But for performance, sometimes direct references are added to avoid expensive joins.Example:
In a CRM system, ifCustomer
needs to frequently display theirPreferredAddress
, storing a direct reference instead of just an ID may improve query performance. This must be balanced carefully with design principles.
Example
Imagine a Customer Aggregate:
-
Customer (Root) – global identity, enforces invariants.
-
Address (Value Object) – belongs inside the Aggregate.
-
PaymentMethod (Entity with local identity) – accessible only through Customer.
External systems can only reference the Customer
root. If they need the Address, a copy is returned, not a direct reference.
✅ Perfected Definition:
Aggregates are consistency boundaries in a domain model. They group related entities and value objects under a single root entity that controls access and enforces business invariants. Aggregates ensure transactional consistency internally, reference other Aggregates only by identity, and should be kept small to maximize scalability. Eventual consistency should be used across Aggregate boundaries, with Domain Events coordinating changes.
Factories
A Factory in Domain-Driven Design (DDD) is responsible for creating complex domain objects or Aggregates in a controlled and expressive way. Unlike simple object instantiation, Factories ensure that creation logic respects business rules, invariants, and the Ubiquitous Language of the domain.
Factories can appear in two main forms:
-
Aggregate Factories (Factory Methods): When an Aggregate Root provides methods to create new instances of entities or Aggregates.
- Service-Based Factories: When a separate Domain Service is responsible for translating o constructing objects, often across Bounded Contexts.
Factory Methods in Aggregates
Sometimes, the creation of an object is not just a technical detail but a business concept. In those cases, the Aggregate Root exposes a Factory Method.
Example – Calendar Aggregate:
Here, the Calendar
Aggregate Root is more than just a data container. It actively controls creation of CalendarEntry
, ensures correct initialization, and raises a domain event (CalendarEntryScheduled
).
Example – Forum Aggregate:
Here, Forum.startDiscussion()
enforces the rule that no discussion can be started if the forum is closed, while still creating the Discussion
object.
Notice how the method name directly expresses the Ubiquitous Language: “Authors start discussions on forums.”
Service-Based Factories
Sometimes, creating a domain object requires translation from another Bounded Context. In such cases, a Domain Service can act as a Factory.
Example – UserRoleToCollaboratorService:
This service translates user identities from the Identity & Access Context into corresponding Collaboration Context roles (Author
, Creator
, Moderator
, etc.).
And the domain interface:
This design clearly separates life cycles and concepts between contexts while preserving clarity in the domain language.
Wrap-Up
-
Factories make models more expressive and aligned with the Ubiquitous Language.
-
Factory Methods in Aggregates enforce invariants while creating new objects.
-
Service-Based Factories handle object creation that requires cross-context translation.
-
This separation ensures correctness, clarity, and maintainability in the domain model.
Repositories
In Domain-Driven Design (DDD), a Repository is a mechanism for accessing, retrieving, and persisting Aggregates.
-
Each persistent Aggregate type typically has its own Repository (one-to-one relationship).
Example:OrderRepository
forOrder
Aggregate,CustomerRepository
forCustomer
Aggregate.This ensures clear separation and aligns with the idea of “one Repository per Aggregate.”
-
In some cases, when two or more Aggregates share the same hierarchy, they may share a single Repository.
The Aggregates are closely related and stored in the same persistence structure.This happens when:
They share a natural lifecycle (e.g., always created, loaded, or queried together).
The boundary between them is more technical than conceptual, so separating Repositories would add unnecessary complexity.
Example: Financial Accounts
BankAccount
(Aggregate Root) andTransactionHistory
(could be another Aggregate Root).
Transactions are always tied to an account and stored together.
-
So instead of two repositories, a single
AccountRepository
manages both.
Reduce redundancy: Prevents duplicate persistence logic.Why Do This?
Consistency: Ensures Aggregates that always live together are persisted together.
Performance: Avoids multiple repository calls when they always belong to the same hierarchy.
Repositories abstract away persistence concerns, allowing the domain layer to work with pure domain objects without depending on the database or ORM technology.
Without the Repository Pattern
When using an ORM directly (e.g., DbContext
in Entity Framework Core):
-
The application layer depends directly on persistence APIs.
-
This creates several issues:
-
Testability – Difficult to test
DbContext
without database dependencies. -
Repetitive Code – Each
DbSet
leads to repetitive CRUD logic. -
Tight Coupling – Application layer depends on the ORM (violating Dependency Inversion).
-
ORM Lock-in – Switching from EF Core to another ORM (like Dapper) requires major code changes.
Why Use the Repository Pattern?
The Repository Pattern introduces an abstraction (interface) between domain logic and persistence.
Benefits:
-
Testability: Repository interfaces allow mocking in unit tests.
-
DRY Principle: Reusable repository abstractions reduce boilerplate code.
-
Separation of Concerns: Keeps business logic independent from persistence.
-
ORM Flexibility: Changing ORM implementations affects only the Repository layer.
Disadvantages
As with any abstraction, Repositories come with trade-offs:
-
Added Complexity: Introduces an additional layer.
-
Orchestration: Interfaces must be correctly implemented and registered in DI.
-
Mapping Overhead: Requires DTO–Entity mapping; tools like AutoMapper may not always work with generic expressions.
-
Transaction Management: Must coordinate persistence across multiple Repositories (often using a Unit of Work).
Repository Design Approaches
1. Collection-Oriented Repositories
-
Behave like an in-memory
Set
orCollection
. -
Hide all persistence details (no explicit
save()
calls). -
Adding an object means it is automatically tracked.
-
Modification is performed by retrieving the object, applying commands, and committing the transaction.
Example (Calendar Repository):
Usage:
Persistence backends (e.g., Hibernate, EF Core) must track changes automatically via:
-
Copy-on-Read: Compare loaded vs. modified copies at commit.
-
Copy-on-Write: Use proxies that track changes and flush them at commit.
2. Persistence-Oriented Repositories
-
Explicitly expose persistence operations like
save()
,update()
,delete()
. -
More practical when underlying ORMs require explicit save commands.
Implementation Steps
-
Define a Repository Interface in the same module as the Aggregate type.
-
Provide an Implementation in the infrastructure layer (preferred).
Summary
-
Repositories decouple domain logic from persistence and make testing, maintenance, and ORM replacement easier.
-
Choose between collection-oriented (abstracting persistence completely) and persistence-oriented (exposing save operations) based on project needs.
-
Use a Unit of Work to coordinate transactions across multiple Repositories.
Domain Services
What a Domain Service Is Not
-
A Domain Service is not the same as a service in Service-Oriented Architecture (SOA).
-
SOA services are usually coarse-grained, remote, system-level APIs, often built with RPC or messaging technologies for integration between distributed systems.
-
Domain Services, on the other hand, live inside the domain model and are focused on business logic, not on system integration.
-
-
A Domain Service is also not an Application Service.
-
Application Services orchestrate use cases, coordinate transactions, and act as clients to the domain model.
-
Domain Services encapsulate business logic that doesn’t naturally belong to a single Entity or Value Object.
-
In practice, Application Services often call Domain Services.
-
-
The word service in “Domain Service” does not imply that it must be a heavyweight, distributed, or remote operation. It is simply a stateless business operation defined in the domain.
What a Domain Service Is
A Domain Service is a stateless operation that belongs to the domain model, expressed in the Ubiquitous Language, and represents a business concept that:
-
Does not naturally fit into a single Entity or Value Object.
-
Often involves multiple Aggregates.
-
Encapsulates domain rules, processes, or transformations.
Typical Uses of Domain Services:
-
Perform a significant business process
-
Example:
TransferMoneyService
handling funds movement between twoAccount
Aggregates.
-
-
Transform domain objects
-
Example:
SchedulingService
converting a draftMeetingRequest
into a confirmedCalendarEntry
.
-
-
Calculate complex values that require multiple inputs
-
Example:
BurndownChartCalculator
computing metrics across multipleSprint
Aggregates in Agile project management.
-
Guidelines for Using Domain Services
-
Use Domain Services sparingly. Most business logic should remain within Entities and Value Objects.
-
Overusing them risks creating an Anemic Domain Model (all logic in services, entities reduced to data containers).
-
Always name them using the Ubiquitous Language so they reflect actual business terminology.
-
Keep them stateless. They should not manage internal state but operate on passed-in domain objects.
Example: Agile Project Management Context
In an Agile project management system, a Domain Service might calculate progress across several BacklogItem
Aggregates:
This service represents a business concept (“calculate a burndown chart”) that doesn’t belong inside a single BacklogItem
or Sprint
but requires combining their data.
Transformation Services (Integration-Oriented)
Some Domain Services are technical in nature and serve as translators between Bounded Contexts.
-
Example: Converting a
User
in the Identity and Access Context into aModerator
Value Object in the Collaboration Context. -
These typically reside in the infrastructure layer, using adapters or translators to map external representations into local domain concepts.
Summary
-
Domain Services are domain-model-level stateless operations that encapsulate business logic not belonging to a single Entity or Value Object.
-
They are not SOA services and not Application Services.
-
Use them only when logic spans multiple Aggregates or represents a distinct business process.
Overuse leads to Anemic Models, but correct use results in cleaner, expressive, and more maintainable domain models.
Application Layer
Delegation from Presentation Model
The Presentation Model should not take on the responsibility of orchestrating complex business operations. It must not act as an Application Service itself. Instead, its role should be limited to delegating tasks to an Application Service, such as BacklogItemApplicationService
, which serves as a Facade to handle more complex use case flows.
Domain Payload Objects (DPO)
An alternative to DTOs is the Domain Payload Object (DPO) [Vernon].
-
What it does: A DPO groups whole Aggregate instances into a single container, allowing multiple Aggregates to be passed between layers for view rendering.
-
How it works:
-
The Application Service retrieves Aggregates via Repositories.
-
It instantiates the DPO and places Aggregate references inside.
-
The presentation layer retrieves Aggregates from the DPO and queries their attributes for display.
-
Drawbacks:
-
Aggregates must expose state for reading, which risks coupling the UI to the model. This can be mitigated using Mediators, Double Dispatch, or an Aggregate Root query interface.
-
Since DPOs hold Aggregate references, unresolved lazy-loaded collections can cause runtime errors once the Application Service transaction is closed.
-
Solutions: Use eager loading or apply a Domain Dependency Resolver (DDR) strategy per use case.
-
1. State Representations in REST
Clear Definition
When building REST APIs, we should not expose domain Aggregates directly.
-
Aggregates represent the internal business model.
-
If we return them as-is (1:1 mapping), clients will need to understand internal rules, structures, and relationships — which couples them too tightly to the domain model.
Instead, REST responses should be designed as use-case-specific representations, also known as Presentation Models. These models show only the data that is necessary for the client’s interaction (nothing more, nothing less).
Example – Bad Approach (Direct Aggregate Mapping)
Imagine an Order Aggregate:
Problems:
-
It exposes internal attributes (
internalStockCode
, loyaltyPoints). -
It forces the client to understand the Order Aggregate structure.
Example – Good Approach (Use-Case-Specific Representation)
If the client only needs to display a summary of orders in a mobile app, the REST resource might look like:
Benefits:
-
Only includes what’s necessary for this use case.
-
Hides internal details.
-
Makes the API easier to evolve without breaking clients.
2. Use Case Optimal Repository Queries
Clear Definition
Normally, Application Services fetch Aggregates through Repositories and then assemble DTOs or DPOs (Domain Payload Objects). This means extra mapping and orchestration code.
A Use Case Optimal Repository Query avoids this by:
-
Designing query methods directly in the Repository that return Value Objects shaped specifically for the use case.
-
These Value Objects are domain-specific, not DTOs.
-
This is similar to CQRS but still works against the same domain persistence store (no separate read database required).
Example – Normal Repository Query (Extra Work in Service Layer)
Here, the Application Service has to manually assemble the WeeklySchedule
.
Example – Use Case Optimal Query (Better)
Define the query inside the Repository:
Repository implementation directly queries persistence and returns the Value Object:
Usage:
Benefits:
-
The Repository handles orchestration.
-
The service layer just delegates, making it thinner.
-
The
WeeklySchedule
Value Object is shaped perfectly for the use case (reporting weekly schedules).
✅ In summary
-
REST State Representations: Always return use-case-focused representations, not raw Aggregates.
-
Use Case Optimal Queries: Move query logic into Repositories so they can return domain Value Objects directly shaped for the use case, reducing boilerplate in Application Services.
Supporting Multiple, Disparate Clients
Applications often serve multiple client types (e.g., RIA, thick clients, REST APIs, messaging, automated tests).
-
One solution is to use Data Transformers.
-
Application Services accept a Data Transformer parameter and double-dispatch based on it to produce the required format.
Example:
Possible implementations of CalendarWeekDataTransformer
:
-
CalendarWeekCSVDataTransformer
-
CalendarWeekDPODataTransformer
-
CalendarWeekDTODataTransformer
-
CalendarWeekJSONDataTransformer
-
CalendarWeekTextDataTransformer
-
CalendarWeekXMLDataTransformer
This allows one Application Service to serve multiple client needs consistently.
Role of Application Services
-
Application Services are the direct clients of the domain model.
-
Responsibilities:
-
Orchestrating use case flows (one method per flow).
-
Managing transactions (ACID guarantees).
-
Enforcing security.
-
Delegating business operations to the domain model (Aggregates, Value Objects, Domain Services).
-
Important Distinction:
-
Application Services ≠ Domain Services.
-
Application Services handle orchestration.
-
Domain Services contain domain business logic.
-
-
Keep Application Services thin; the bulk of business logic belongs inside the domain layer.
Command Objects
When Application Service methods take many parameters, the method signature becomes noisy and hard to maintain.
-
A better approach is to use a Command Object [Gamma et al.].
-
A Command encapsulates all parameters into a single object that represents a business request.
-
This makes methods cleaner, supports queuing, logging, or even undo operations.
Composing Multiple Bounded Contexts
User interfaces often combine multiple models (e.g., Products
, Discussions
, Reviews
) into a unified view.
Two approaches:
-
Multiple Application Layers (portal/portlet style):
-
Each UI component interacts with its own Application Layer tied to a specific Bounded Context.
-
Downside: harder to harmonize across use case flows.
-
-
Single Application Layer (preferred):
-
Provides a unified source for composition.
-
Aggregates and services from different contexts are combined here into cohesive results for the UI.
-
This layer contains no business logic—it only aggregates and adapts.
-
✅ Summary
-
Presentation Model delegates to Application Services (no heavy lifting in the UI).
-
DPOs allow passing Aggregates together, but require careful handling of lazy loads.
-
REST endpoints should expose use-case-specific representations, not raw Aggregates.
-
Use case optimal queries and Value Objects simplify views and reduce orchestration complexity.
-
Data Transformers support multiple clients consistently.
-
Application Services orchestrate use cases, transactions, and security while staying thin and delegating business rules to the domain model.
-
Command objects prevent bloated method signatures.
-
For cross-context composition, a single Application Layer often works best.
Aggregates and Event Sourcing
What is Event Sourcing?
Event Sourcing is a design pattern where the state of an Aggregate (10) is not stored directly as a snapshot, but instead captured as a series of domain Events (8). Each Event represents something that has happened in the past (e.g., “TaskCreated”, “TaskAssigned”, “TaskCompleted”).
When you need the current state of an Aggregate, you don’t fetch a stored object. Instead, you replay its Events in the order they occurred, rebuilding the state step by step.
Event Store (8)
An Event Store is a specialized persistence mechanism used in Event Sourcing. Unlike a traditional database table that stores only the latest state of each entity, the Event Store records every Event that has ever occurred for an Aggregate.
-
Each Aggregate has its own Event Stream, identified by the Aggregate Root’s identity.
-
The Event Stream is an ordered log of domain Events.
-
The Event Store acts as the single source of truth because it contains the complete history of all changes.
Clearer Explanation of Source of Truth
Instead of relying on a single snapshot of an object, the Event Store ensures you always have the full history. From this history, you can always derive the current state by replaying Events.
✅ Example:
Imagine a BankAccount
Aggregate. Instead of storing only the current balance, the Event Store saves Events like:
-
AccountOpened($0)
-
DepositMade($100)
-
WithdrawalMade($30)
If you replay these Events, you derive the current balance: $70
. Even if you never stored “$70” explicitly, you can compute it at any time from the Event Store.
Why Use Event Sourcing?
-
Auditability: You can always track what happened, when it happened, and who performed the action.
-
Debugging & Replay: You can replay historical Events to simulate or debug system behavior.
-
Flexibility: You can project different views of the same data without altering the core model.
1. Auditability and Traceability
Every change in the system is explicitly recorded as an event.
-
You know when something happened, what happened, and why.
-
Perfect for financial systems, healthcare records, or compliance-heavy industries.
Example: If a patient’s medical record changes, you can trace exactly when a diagnosis was added, modified, or removed.
2. Flexibility
From the same stream of events, you can project different views depending on the use case — without altering the domain model.
Example: In an e-commerce system, an Order
Aggregate may generate this event stream:
From this:
-
The Customer Order History view shows placed and shipped dates.
-
The Financial Summary view shows total revenue from the order.
-
The Inventory view shows which items should be deducted from stock.
All three views come from the same data source — the event stream — without modifying the core model. Each view subscribes to only the specific events it cares about, ensuring that it stays relevant to its own use case while ignoring unnecessary events.
Example:
-
A Customer View might subscribe to events such as
CustomerRegistered
andCustomerAddressChanged
. -
An Order View might subscribe only to
OrderPlaced
,OrderShipped
, andOrderCancelled
. -
A Billing View could subscribe to
InvoiceGenerated
andPaymentReceived
.
This way, every view is powered by the same underlying event stream but only reacts to the subset of events that matter to it.
3. Scalability and Projections
Since events are stored as an append-only log, you can scale read models independently. Different consumers (dashboards, analytics engines, audit services) can subscribe to the event stream and build their own projections.
Example: A shipping service could subscribe only to OrderShipped
events, while the accounting service subscribes to financial transaction events.
REST and DDD – Why Not Expose Aggregates Directly?
It might be tempting to expose your domain model directly as REST resources (e.g., returning Aggregates as JSON). However, this approach is brittle because every change in the domain model leaks into the API.
❌ Bad Approach: Exposing the Aggregate Directly
Response (returns the full Task
Aggregate):
-
This response exposes every internal detail of the
Task
Aggregate, including audit logs and workflow states that clients should never depend on. -
If the domain team renames
dueDate
→deadline
or removesinternalWorkflowState
, all the users (clients) who use the API will now get errors because their applications still expectdueDate
orinternalWorkflowState
to exist, even though these changes are small and internal.
✅ Good Approach: Use Case–Specific Representation
Response (focused, stable view):
-
Clients see only what’s relevant to their use case.
-
Internal model changes (audit logs, workflow states, system fields) don’t affect this contract.
-
The API remains stable and easy to evolve.
👉 This side-by-side example makes it clear why exposing the full Aggregate is fragile, while a use-case-driven response protects both the domain model and API clients.
✅ In summary:
-
Event Sourcing represents Aggregates as a sequence of events, stored in an Event Store.
-
The Event Store is the source of truth, and you can rebuild state or project new views anytime.
-
REST interfaces should not expose Aggregates directly but should provide use-case-specific representations, ensuring clients remain decoupled from domain internals.
Comments
Post a Comment