Clean Architecture, Domain Driven Design
Table of Contents
2.
The
Strategic Phase of Domain-Driven Design (DDD)
o Purpose of the Strategic Phase
o Example Dialogue: Aligning on a Ubiquitous Language
o Why is the Strategic Phase Important?
3.
The
Tactical Phase of Domain-Driven Design (DDD)
o What Tactical DDD Covers (Scope)
o Minimal Example – E-commerce Order Context
o Programming Paradigms in Tactical DDD
4.
The DDD
Four Conceptual Layers
5.
Data Transfer
Objects (DTOs)
o Example
o The Responsibility of Each Layer in Generating
DTOs
6.
Modules
8.
Aggregates
9.
Factories
10. Repositories
11. Domain
Services
·
Delegation
from Presentation Model
·
Domain Payload
Objects (DPO)
·
State
Representations in REST
·
Use
Case Optimal Repository Queries
·
Role of
Application Services
·
Composing
Multiple Bounded Contexts
13. Aggregates and Event Sourcing
14. REST and DDD – Why Not Expose Aggregates
Directly?
What is a Domain?
A domain is simply a part of the real world that we want to represent in software. You cannot directly “pour” reality into code; instead, you must build an abstraction—a structured way to represent real-world knowledge in terms that software can understand.
In Domain-Driven Design (DDD), the domain is central:
-
It defines the core concepts, rules, and processes of the business.
-
It gives us the language (Ubiquitous Language) to reason about the system consistently across developers, domain experts, and stakeholders.
- It drives the structure of both the model and the code.
Why Does It Matter?
1. Understanding Abstraction
In Manufacturing and Engineering
Abstraction means simplifying a complex system by focusing only on its essential features while ignoring unnecessary details. This helps engineers and designers manage complexity, improve communication, and design systems more efficiently.
Examples:
-
CAD Modeling: A 3D model of a machine may hide internal parts and only show the dimensions necessary for assembly.
-
System Design: Mechanical, electrical, and software layers are designed separately before being integrated.
-
Simulation Models: A factory process can be simulated without modeling every detail, just enough to test efficiency.
Why it matters:
-
Makes complex systems easier to manage.
-
Improves clarity and problem-solving.
- Encourages modular design (e.g., interchangeable or reusable parts).
Consider an e-commerce system:
- The domain includes concepts like
Product
,Order
,Customer
, andPayment
. - The rules include things like “An order cannot be shipped before it’s paid.”
- The processes cover scenarios such as order placement, cancellation, and fulfillment.
In Software Architecture
In software, abstraction means organizing the system around high-level components and their interactions, while hiding internal implementation details.
This allows us to:
-
Create logical layers (UI, application, domain, infrastructure).
-
Build modules with clear purposes.
-
Define services with stable interfaces.
Eric Evans’ view:
A domain model is not just a diagram. It is an organized representation of domain knowledge—something that can be expressed in diagrams, carefully written code, or even plain language.
2. Building a Domain Model
We learn about the domain by talking to domain experts and collecting raw knowledge. But raw knowledge alone cannot be transformed into software. To make it useful, we must build a mental blueprint—an abstraction of the domain.
-
At first, this blueprint is incomplete.
-
Over time, by refining our understanding, testing assumptions, and collaborating with experts, the model becomes clearer, richer, and more accurate.
3. Example: E-Commerce Domain
Imagine designing an e-commerce system. The domain includes:
-
Entities:
Customer
,Order
. -
Value Objects:
Address
,Money
. -
Aggregates:
OrderAggregate
(manages line items, payments, shipping, and status).
Here, you cannot directly code "shopping behavior," but by abstracting it into entities, value objects, and aggregates, you create a software model that represents real-world shopping processes.
✅ Final Note:
The domain is the foundation. Everything in DDD — strategic and tactical — builds on a clear, shared understanding of the domain. Without it, architecture becomes disconnected from business reality.
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:
-
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.
Why It Matters
-
Clarity: Each team works with its own consistent model.
-
Scalability: Different contexts can evolve independently.
-
Integration: Boundaries are explicit, making communication between systems clear.
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 Domain-Driven Design (DDD)
1. What is Tactical DDD?
Tactical DDD is the hands-on part of Domain-Driven Design. While strategic DDD maps the big picture (contexts, team boundaries, and integrations), tactical DDD works inside a single bounded context and defines the building blocks that turn business language and rules into working code.
It is achieved through a progression of steps: starting with lightweight modeling (e.g., UML), moving to partial coded abstractions, and finally producing executable code. At the end, the final code becomes the true domain model—it is where business rules are enforced.
2. What Tactical DDD Covers (Scope)
Inside one bounded context, tactical DDD specifies:
-
Entities – objects with identity (e.g.,
Order
,Customer
). -
Value Objects – immutable, identity-less concepts (e.g.,
Money
,Address
). -
Aggregates – small clusters of Entities/Value Objects with one Aggregate Root that enforces rules (e.g.,
Order
as root ofOrderLine
s). -
Domain Services – stateless domain operations that don’t fit neatly in a single entity (e.g.,
TaxCalculator
). -
Domain Events – facts that happened (e.g.,
OrderPaid
,OrderShipped
) used to coordinate across aggregates/contexts. -
Repositories – collection-like access to Aggregates (e.g.,
OrderRepository
). -
Factories – controlled creation for complex objects/aggregates.
The goal is to:
-
Make business rules explicit and enforced.
-
Keep consistency inside an Aggregate and coordinate across Aggregates with events.
-
Produce a model that’s expressive, testable, safe to evolve, and aligned with the business language.
3. Tactical DDD Phases
Tactical DDD often unfolds in three practical phases:
Phase 1 – UML / Lightweight Modeling
-
Purpose: Capture the first abstraction of the domain.
-
Method: Use UML diagrams, sketches, or whiteboards.
-
Example:A diagram showing
Order
→OrderLine
with a rule “total ≥ 0”. -
Role: Communicate structure and rules with domain experts and developers.
-
Note: UML here is only for understanding and sharing ideas, not the final model.
Phase 2 – Partial Code Abstractions
-
Purpose: Translate the model into early code structures.
-
Method: Write stubs, interfaces, and empty classes.
-
Example (C# stub):
-
Role: Validate naming against ubiquitous language, see how the abstractions feel in code, and prepare for rules.
Phase 3 – Final Coding (Executable Domain Model)
-
Purpose: Implement rules, invariants, behaviors, and persistence.
-
Example (C# implementation):
-
Role: The code becomes the true domain model. UML and stubs may remain as documentation, but only the code enforces the business rules.
4. Minimal Example – E-commerce Order Context
Bounded Context: Order Management
This example shows the same model across the three tactical phases:
(1) UML / lightweight modeling → (2) partial code abstractions → (3) final executable code.
Phase 1 — UML / Lightweight Modeling (communication & shape)
Use a small UML sketch to agree on names, relationships, and invariants. Keep it focused—this is for shared understanding, not for code generation.
At this stage, the team sketches the building blocks to communicate with domain experts and align on language.
-
Entity (Aggregate Root):
Order(OrderId)/
Order
as the root containing multipleOrderLine
s -
Value Objects:
Money
,Address
,OrderLine
-
Aggregate:
Order
as the root containing multipleOrderLine
s Rules (invariants):
-
total >= 0
-
cannot pay if
status == Cancelled
-
cannot ship unless
status == Paid
-
-
Domain Services (externalized rules):
TaxCalculatorService (regional rules)
-
Repositories (persistence placeholder):
OrderRepository
-
Factories (creation logic):
OrderFactory.createFromCart(cart, customer)
-
Events (facts):
OrderPlaced
,OrderPaid
,OrderShipped
What UML Does in This Context (final, aligned with the diagram)
-
Names core elements: Order
(Aggregate Root), OrderLine
, Money
, Address
, and supporting IDs (OrderId
, ProductId
, PaymentRef
, TrackingNumber
).
-
Defines boundaries: Shows Order
as the Aggregate Root that composes OrderLine
(1..*), references Address
, and encapsulates the consistency boundary.
-
Documents invariants (as notes):
-
total() >= Money.zero
-
cannot pay if status == Cancelled
-
cannot ship unless status == Paid
-
Flags domain events emitted by the Aggregate: OrderPlaced
, OrderPaid
, OrderShipped
(and OrderCancelled
if included).
-
Positions supporting patterns (externals to the Aggregate):
-
TaxCalculatorService
(Domain Service) used by Order
-
OrderRepository
(Repository) persisting Order
-
OrderFactory
(Factory) creating Order
(e.g., createFromCart
)
- Keeps scope tight: One Aggregate + nearby Value Objects; detailed publishing mechanisms, persistence specifics, or cross-context flows are kept in text, not the class diagram.

Names core elements: Order
(Aggregate Root), OrderLine
, Money
, Address
, and supporting IDs (OrderId
, ProductId
, PaymentRef
, TrackingNumber
).
Defines boundaries: Shows Order
as the Aggregate Root that composes OrderLine
(1..*), references Address
, and encapsulates the consistency boundary.
Documents invariants (as notes):
-
total() >= Money.zero
-
cannot pay if
status == Cancelled
-
cannot ship unless
status == Paid
Flags domain events emitted by the Aggregate: OrderPlaced
, OrderPaid
, OrderShipped
(and OrderCancelled
if included).
Positions supporting patterns (externals to the Aggregate):
-
TaxCalculatorService
(Domain Service) used byOrder
-
OrderRepository
(Repository) persistingOrder
-
OrderFactory
(Factory) creatingOrder
(e.g.,createFromCart
)
Phase 2 — Partial Code Abstractions (shaping the API surface)
Now the abstractions are expressed in code, showing what operations exist but not their internals.
Entity & Aggregate Root (Order):
Value Objects:
Services:
Repositories:
Factory:
Events:
Phase 3 — Final Coding (enforcing invariants & emitting events)
Here, invariants, business rules, and event emission are implemented.
Entity / Aggregate with Rules:
Event-Driven Collaboration Example:
-
OrderPaid
→ triggers Billing microservice to capture funds. -
OrderShipped
→ triggers Shipping microservice to dispatch items.
At this stage, the abstractions become working code, with enforced invariants and events integrated into the system.
Programming Paradigms in Tactical
DDD
When tactical patterns (Entities, Value Objects, Aggregates,
etc.) move from modeling to code, the programming paradigm matters. Not all
paradigms support model-driven design equally well.
Object-Oriented Programming (OOP) –
the natural fit
OOP fits naturally with domain modeling because both
emphasize objects, identity, and relationships.
- Classes
↔ Entities/Value Objects:
You can model an Order class as an Aggregate Root, or a Money class
as a Value Object.
- Associations
↔ Relationships: Order contains OrderLine as part of its Aggregate.
- Messaging
↔ Domain Events: Objects communicate by sending
messages, just like publishing domain events.
This makes OOP languages (Java, C#, Kotlin, etc.) are particularly well-suited for implementing tactical DDD. They provide a natural bridge between the conceptual model and the final code, making the translation from model → code direct and expressive.
Procedural Programming (PP) – when
it falls short
Procedural programming focuses on algorithms and
step-by-step procedures. While it’s effective in domains like mathematics or
scientific computing (where the problem is mostly numerical calculations),
it doesn’t map well to complex, evolving domains like e-commerce or
banking.
For example:
- A
mathematics library for calculating prime numbers or matrix
multiplications is naturally procedural.
- But
modeling a Customer placing an Order requires identity, state,
rules, and interactions — things procedural programming struggles to
capture directly.
Therefore, PP can complement tactical DDD for
calculation-heavy modules, but OOP is generally the better foundation
for implementing rich domain models.
✅ Summary:
-
Phase 1 (UML): Communicates the shape (entities, values, aggregates, services, repos, factories, events).
-
Phase 2 (Partial Code): Defines API surfaces and abstractions.
-
Phase 3 (Final Code): Enforces invariants, raises domain events, and integrates with other contexts.
This phased approach shows how Tactical DDD evolves from sketches, to abstractions, to robust, executable code.
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:
1. User Interface (UI) Layer
-
Responsibility: Handles input/output with the user or external systems (e.g., Web UI, REST API, CLI).
-
Key point: Should not contain business logic. It simply coordinates with the application layer.
-
Example: A React front-end displaying orders, or a REST API controller forwarding a request to the application layer.
2. Application Layer
-
Responsibility: Orchestrates tasks and use cases by coordinating domain objects.
-
Key point: Contains workflows but no business rules.
-
Example: An
OrderService
class that calls the domainOrder
aggregate to validate and process an order.
3. Domain Layer
-
Responsibility: The heart of the system, containing business rules, domain models, and invariants.
-
Includes:
-
Entities: Objects with identity (e.g.,
Order
,Customer
). -
Value Objects: Immutable objects defined by their attributes (e.g.,
Address
,Money
). -
Aggregates: Clusters of entities/values with a single root (e.g.,
OrderAggregate
controllingOrderLine
s). -
Domain Services: Stateless operations that don’t naturally belong inside a single entity (e.g.,
CurrencyConversionService
). -
Domain Events: Capturing facts that happened in the domain (e.g.,
OrderPaid
). -
Factories: Create complex aggregates consistently.
-
Repositories (Interfaces): Abstract persistence concerns (
IOrderRepository
).
-
⚠️ Important: Only interfaces for repositories live here, not the persistence code itself.
4. Infrastructure Layer
-
Responsibility: Provides technical capabilities (databases, messaging, file systems, external services).
-
Implements:
-
Repository implementations (
SqlOrderRepository
). -
Domain service implementations that depend on external systems (e.g., calling a payment gateway).
-
Technical concerns (logging, email sending).
-
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.
-
Dependency Inversion (Tiny Interface vs. Implementation)
A key principle in DDD layering is that the domain layer should never depend on infrastructure. Instead:
-
The domain defines tiny interfaces (e.g.,
IPaymentGateway
,IOrderRepository
). -
The infrastructure layer provides implementations (
StripePaymentGateway
,SqlOrderRepository
). -
This keeps the domain pure and testable.
Sometimes a business rule spans more than one Aggregate and doesn’t fit neatly inside either Aggregate’s root. That’s a classic case for a Domain Service.
Key rules:
-
The service can read from multiple Aggregates in one use case.
-
As a rule of thumb, modify only one Aggregate per transaction; coordinate others by Domain Events (eventual consistency) if needed.
-
Prefer Application to load aggregates and pass them to the Domain Service. That keeps the domain implementation pure and testable.
Where do the interface and implementation live?
-
Interface: always in the Domain layer (it’s part of the Ubiquitous Language).
-
Implementation:
-
If it’s pure domain logic (no external calls), the implementation can live in the Domain layer.
-
If it needs external data (e.g., FX rates, tax API), keep the Domain Service implementation in Domain, but depend on other Domain-defined interfaces (e.g.,
IExchangeRateProvider
) whose implementations live in Infrastructure. -
Alternatively, if you prefer maximum purity, you can keep only the interface in Domain and put the implementation in Application when it’s orchestration-flavored. (But then it’s arguably an Application Service, not a pure Domain Service.)
-
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 represent domain objects that are defined primarily by their identity rather than by their attributes. Their attributes can change over time, but the entity remains the same as long as its identity is preserved.
-
Definition: An entity is unique because of its identity, not its data.
-
Example: Two customers may both be named John Smith and be 35 years old, but they are distinct entities if they have different
CustomerId
values. - Why It Matters: Entities allow us to track and manage things that evolve over time (like Customers, Orders, or Products), ensuring that changes to their attributes don’t create “new” objects accidentally.
Code Illustration (C#)
public class Customer
{
public Guid CustomerId { get; private set; } // Identity
public string Name { get; set; }
public int Age { get; set; }
public Customer(Guid id, string name, int age)
{
CustomerId = id;
Name = name;
Age = age;
}
// Equality based on identity
public override bool Equals(object obj)
=> obj is Customer other && CustomerId == other.CustomerId;
public override int GetHashCode() => CustomerId.GetHashCode();
}
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.
Domain Services
Sometimes, domain behavior (actions) doesn’t
naturally belong to any single Agrrigate, 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:
1. Represent domain
operations that don’t fit naturally into Entities or Value Objects.
2. Work with multiple
domain objects.
3. Are stateless (no
internal state).
Services group related behaviors clearly and
avoid cluttering Entities or Value Objects with unrelated responsibilities.
What a Domain Service Is Not
·
A Domain Service is not the
same as a service in Service-Oriented
Architecture (SOA).
o SOA services are
usually coarse-grained,
remote, system-level APIs, often built with RPC or messaging
technologies for integration between distributed systems.
o 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.
o Application Services orchestrate use cases, coordinate
transactions, and act as clients to the domain model.
o Domain Services encapsulate business
logic that doesn’t naturally belong to a single Aggregate/Entity.
o 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 Aggregate/Entity/Value
Object.
·
Often involves multiple
Aggregates.
·
Encapsulates domain rules, processes, or transformations.
Typical Uses of Domain Services:
1. Perform a significant business process
o Example: TransferMoneyService
handling funds movement between two Account
Aggregates.
2. Transform domain objects
o Example: SchedulingService
converting a draft MeetingRequest
into a confirmed CalendarEntry
.
3. Calculate complex values that require multiple inputs
o Example: BurndownChartCalculator
computing metrics across multiple Sprint
Aggregates in Agile project
management.
Roles & Placement (Dependency Inversion)
-
Domain Layer
-
Domain Service interface and (if pure logic) implementation
-
Aggregate types (
Account
,TransferPolicy
) -
Repository interfaces (
IAccountRepository
)
-
-
Application Layer
-
Orchestrates the transaction (begin/commit/rollback)
-
Loads Aggregates via repositories and calls the Domain Service
-
-
Infrastructure Layer
-
Repository implementations (ORM/SQL)
-
Unit of Work / Transaction plumbing
-
This keeps the Domain pure and the transaction mechanics outside the model.
Example: Atomic Funds Transfer (debit A, credit B) in one transaction
Domain Layer
Aggregates
Repository interface & Domain Service interface
Domain Service (pure logic across two Aggregates)
Why in Domain? The rule (debit A, credit B with validations) is pure business logic and touches multiple Aggregates. It has no infrastructure concerns.
Application Layer (one transaction; orchestrates loading/saving)
The Application layer coordinates the transaction & persistence. It does not implement the transfer rule; that lives in the Domain Service.
Infrastructure Layer (persistence + transaction)
-
EfAccountRepository : IAccountRepository
implementsFindById/Save
, honorsVersion
(optimistic concurrency via rowversion or version column). -
EfUnitOfWork : IUnitOfWork
wraps the ORM/DB transaction.
This keeps transaction mechanics and ORM details out of the Domain.
Concurrency & Locking
-
Prefer optimistic concurrency (version check on
Save
) to avoid unnecessary locks. -
If the business requires pessimistic locking (e.g., serialized transfers), keep locking inside Infrastructure (repository/UoW), not in the Domain code.
-
On concurrency exceptions, the Application layer decides whether to retry or fail.
Design Checklist (strong consistency across Aggregates)
-
Challenge the model: should these objects be one Aggregate? (e.g., introduce
LedgerEntry
as the real Aggregate and keepAccount
read-model-ish) -
If strong consistency is truly required, wrap both modifications in one transaction managed by the Application layer.
-
Keep the multi-aggregate business rule in a Domain Service (interface + pure implementation in Domain).
-
Keep repositories & Unit of Work abstractions in Domain, implementations in Infrastructure.
-
Enforce optimistic concurrency; decide on retries/policies in Application.
When you shouldn’t do this
-
If eventual consistency is acceptable, prefer one Aggregate per transaction + Domain Events (more scalable, simpler).
-
If multi-aggregate modifications become frequent and inevitable, remodel (merge, or introduce a new Aggregate that captures the invariant).
Summary
When you must change multiple Aggregates atomically, keep DDD boundaries intact:
-
Domain: multi-aggregate rule as a Domain Service (pure), plus repository interfaces.
-
Application: transaction orchestration and persistence coordination.
- Infrastructure: repository + unit of work implementations, concurrency control.
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.
Data Transfer Objects
Why Use DTOs?
In a three-layered architecture (Presentation, Business/Domain, Data Access), a key principle is separation of concerns. The presentation layer (UI/API) should not directly depend on the data access layer or domain entities. Doing so would tightly couple the UI to persistence details, making the system fragile and hard to maintain.
To bridge this gap, we use DTOs (Data Transfer Objects).
What Are DTOs?
A DTO is a simple, serializable object whose sole purpose is to carry data between layers or services.
-
Structure: Often resembles entities, but not the same.
-
Fields: May have more, fewer, or reshaped fields compared to domain entities.
-
Behavior: Pure data containers — they do not contain business rules or persistence logic.
Why DTOs Are Important
-
Layer Separation
-
DTOs decouple internal domain models from external contracts (UI or API).
-
This means internal changes don’t immediately break external consumers.
-
-
Controlled Data Exposure
-
DTOs allow selective sharing of information.
-
Example: Excluding sensitive fields like
Password
orAccountBalance
when returning data to the client.
-
-
Adaptation for Clients
-
DTOs can reshape or combine data to fit client needs.
-
Example: An
OrderDTO
might includeCustomerName
andTotalPrice
even though those live in separate objects in the domain model.
-
Trade-offs
The main drawback of using DTOs is the need to map data between domain entities and DTOs. This mapping adds complexity and boilerplate.
➡️ Solution: Use tools like AutoMapper (C#) or MapStruct (Java) to automate mapping, reduce manual effort, and minimize errors.
Example
Entity:
DTO for UI:
👉 The DTO is shaped specifically for what the UI needs, keeping the presentation layer independent from persistence details.
The Responsibility of Each Layer in Generating DTOs
In layered architecture, each layer is responsible for preparing DTOs for the next outer layer:
-
Data Access Layer → Domain Layer
-
Transforms raw persistence objects (e.g., EF entities, SQL rows) into domain entities.
-
At this level, DTOs are not exposed; instead, the Repository ensures only domain objects cross the boundary.
-
-
Domain Layer → Application Layer
-
The domain layer enforces business rules using entities, value objects, and aggregates.
-
The application layer is responsible for mapping domain entities to DTOs that the presentation layer can consume.
-
Example: An
Order
aggregate in the domain becomes anOrderDTO
in the application.
-
-
Application Layer → Presentation Layer (API/UI)
-
The application layer returns DTOs to the presentation layer.
-
The presentation layer should never see domain entities directly.
-
Example: An API endpoint
/orders/123
returnsOrderDTO
to the client, not the rawOrder
entity.
-
-
Repositories return domain entities, not DTOs.
-
Application services return DTOs to the presentation layer.
-
Presentation layer only consumes DTOs.
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
In Domain-Driven Design (DDD), a Value Object represents a descriptive aspect of the domain that has no identity of its own. Instead of being tracked individually, a Value Object is defined entirely by its attributes and the value they represent.
How to Recognize a Value Object
When deciding if a concept should be modeled as a Value Object, check whether it has most of these characteristics:
Describes or measures something in the domain (e.g., Money, Address, Weight).
-
Immutable – once created, its state never changes (updates mean replacing it).
-
Conceptual whole – groups related attributes into one meaningful unit.
-
Replaceable – you don’t modify it in place; you swap it with a new one.
-
Equality by values – two instances are equal if all their attributes match.
-
Side-effect free – provides behavior (like formatting or calculations) without altering state.
Example: Address in an E-Commerce System
The Address is not an entity on its own—it has no identity—but it describes where the order should be shipped.
-
Order (Entity) has an OrderId (identity).
-
Address (Value Object) only matters by its attributes (Street, City, ZipCode, Country).
If a customer moves to a new house, we don’t "update" the old address—we replace it with a new Address
object.
Value Object: Address
(immutable, equality by value)
public record Address(string Street, string City, string ZipCode, string Country){public override string ToString() => $"{Street}, {City}, {ZipCode}, {Country}";}
Order
(has identity; contains Address
as a Value Object)
public class Order
{
public Guid OrderId { get; }
public DateTime OrderDate { get; }
public string Status { get; private set; }
// Value Object (no identity; replace when it changes)
public Address ShippingAddress { get; private set; }
public Order(Guid orderId, DateTime orderDate, Address shippingAddress)
{
OrderId = orderId;
OrderDate = orderDate;
ShippingAddress = shippingAddress;
Status = "Placed";
}
public void ShipTo(Address newAddress)
{
// Replace the VO atomically (immutability: no in-place mutation)
ShippingAddress = newAddress;
}
public void ShipOrder()
{
if (Status != "Placed")
throw new InvalidOperationException("Order not in Placed status.");
Status = "Shipped";
}
}
Usage: (shows equality-by-value & replaceability)
var addr1 = new Address("10 Main St", "Springfield", "12345", "US");
var addr2 = new Address("10 Main St", "Springfield", "12345", "US");
// Value equality: true
bool same = addr1 == addr2;
var order = new Order(Guid.NewGuid(), DateTime.UtcNow, addr1);
// Customer moved → replace the Address with a new instance
var newAddr = new Address("55 Elm Ave", "Springfield", "12345", "US");
order.ShipTo(newAddr);
var addr1 = new Address("10 Main St", "Springfield", "12345", "US"); var addr2 = new Address("10 Main St", "Springfield", "12345", "US"); // Value equality: true bool same = addr1 == addr2; var order = new Order(Guid.NewGuid(), DateTime.UtcNow, addr1); // Customer moved → replace the Address with a new instance var newAddr = new Address("55 Elm Ave", "Springfield", "12345", "US"); order.ShipTo(newAddr);
Now, the Address ValueObject has the following characteristics:
- Descriptive Role: Address describes a customer’s location.
- Immutability: Being a
record
in C#, once created it cannot be modified. To “change” an address, you create a new one. - Conceptual Whole: Street, City, ZipCode, and Country together form a meaningful unit.
- Replaceability: If a customer moves, the old
Address
object is replaced with a new one. - Equality by Value: Two addresses with identical fields are equal.
- Side-Effect-Free Behavior: Its
ToString()
method produces a representation without altering the object or the system.
Why It Matters
Using Value Objects helps keep models clean, expressive, and safe. They capture concepts from the domain precisely while reducing accidental complexity (no identity tracking, no mutable state). This makes them easy to reason about, test, and use as building blocks inside Entities and Aggregates.
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.
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.
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.
Application Layer Responsibilities — In Detail
1) Use cases (orchestrates the flow)
PlaceOrder
, TransferFunds
, ShipOrder
).Validate request shape (not deep domain rules).
Load Aggregates via Repositories (interfaces).
Call domain logic (Aggregates / Domain Services).
Save changes.
Publish domain/outbox events if needed.
Return a DTO to callers.
Why here: Keeps the “story” of the operation in one place; the Domain stays focused on local invariants.
2) Transactions / Unit of Work (UoW)
Start transaction at the beginning of the use case.
Save all modified Aggregates.
Commit once at the end (or rollback on failure).
For events, use the Outbox pattern to persist events atomically with the transaction, then dispatch asynchronously.
Why here: Only the Application layer can see all the steps; the Domain must remain persistence-agnostic.
3) Security
Check policies (e.g., roles/claims/ownership) before calling domain logic.
Keep security out of Aggregates; pass only what the domain needs (e.g.,
CustomerId
).
Why here: Security is a cross-cutting concern tied to the user/request, not to domain invariants.
4) Retries
Use retry policies (exponential backoff; e.g., Polly in .NET).
Only retry idempotent operations (or pair with an idempotency key, below).
Don’t retry business rule violations—only technical transient errors.
Why here: The Application layer sees infrastructure exceptions and can decide policy without polluting domain code.
5) Idempotency
Require an Idempotency-Key (e.g., header) from clients.
Store keys with outcome; if the same key appears again, return the original result instead of performing the operation again.
Combine with retries for at-least-once delivery scenarios.
Why here: Idempotency is a property of the use case execution, not a single Aggregate method.
6) Mapping to DTOs
Input DTOs/Commands → validate → map to domain calls.
Output DTOs/View Models ← map from domain results.
Do not expose Entities/Aggregates directly to clients.
Why here: Keeps the Domain model pure and lets the API evolve safely without breaking internal code.
Mini Example (C#-style pseudo-code)
A strongly consistent money transfer use case showing all concerns together:
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.
Minimal structure (namespaces)
Application
Application.Calendar.ICalendarWeekDataTransformer
Application.Calendar.CalendarAppService
Application.Calendar.CalendarWeekData
(output wrapper)
Presentation/API (using InterfaceAdapters)
InterfaceAdapters.Calendar.CalendarWeekCSVDataTransformer
InterfaceAdapters.Calendar.CalendarWeekJSONDataTransformer
InterfaceAdapters.Calendar.CalendarWeekXMLDataTransformer
Domain
Domain.Calendar.ICalendarRepository
Domain.Calendar.CalendarEntry
Infrastructure
Infrastructure.Calendar.EfCalendarRepository
(or InMemory)
Extra tip: you might not need custom transformers
If your API only serves JSON, you can return an output DTO from the Application layer and let the web framework serialize it automatically. Add CSV/XML transformers only if you truly need multiple media types or special shapes.
Bottom line:
Keep format-specific code (CSV/JSON/XML) in Interface Adapters.
Keep use-case orchestration and the transformer interface in Application.
Keep domain rules and repository interfaces in Domain.
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.
Domain Services
1. What a Domain Service Is
A Domain Service is a stateless operation that belongs to the domain model, expressed in the Ubiquitous Language.
It represents a business concept that:
-
Does not naturally fit into a single Aggregate, Entity, or Value Object.
-
Often involves multiple Aggregates.
-
Encapsulates domain rules, processes, or transformations.
Typical Uses
-
Perform a significant business process
Example:TransferMoneyService
moves funds between twoAccount
Aggregates. -
Transform domain objects
Example:SchedulingService
converts a draftMeetingRequest
into a confirmedCalendarEntry
. -
Calculate complex values
Example:BurndownChartCalculator
computes metrics across multipleSprint
Aggregates in Agile project management.
2. What a Domain Service Is Not
-
❌ Not an Application Service (no orchestration, transactions, or external I/O).
-
❌ Not an infrastructure service (doesn’t talk to databases, HTTP, or messaging).
-
❌ Not a replacement for entity behavior (most business logic should still live inside Aggregates).
⚠️ Overusing domain services risks creating an Anemic Domain Model, where entities are reduced to data containers and all logic lives in services.
3. Guidelines for Using Domain Services
-
✅ Use sparingly; prefer putting logic in Entities and Value Objects.
-
✅ Always name them in the Ubiquitous Language (
PricingPolicyService
,TransferMoneyService
). -
✅ Keep them stateless – no internal state, only input/output behavior.
4. When a Domain Service Uses Multiple Aggregates
Sometimes a business rule spans more than one Aggregate and doesn’t fit neatly inside either Aggregate’s root.
Rules:
-
The service can read multiple Aggregates.
-
Modify only one Aggregate per transaction.
-
Use Domain Events to coordinate changes across Aggregates (eventual consistency).
-
Let the Application Layer load Aggregates via repositories and pass them into the service.
5. Service Interface & Layer Placement
Domain Layer
-
Defines interfaces and pure implementations.
-
Examples:
-
IPricingPolicyService
-
DefaultPricingPolicyService
-
Infrastructure Layer
-
Implements external adapters.
-
Examples:
-
SqlOrderRepository
-
HttpExchangeRateProvider
-
Application Layer
-
Orchestrates transactions, loads aggregates, and calls domain services.
-
Example:
OrderApplicationService
callsIPricingPolicyService
.
Rule of Thumb:
-
Pure business logic → stays in the Domain Layer.
-
External dependencies → interface in Domain, implementation in Infrastructure.
-
Orchestration logic → Application Layer.
6. Example: PricingPolicyService
Domain Layer — Service Interface
Domain Layer — Pure Implementation
Application Layer — Orchestration
7. Example: OrderPaymentService
The OrderPaymentService handles charging a customer for an order once the restaurant accepts it.
This involves multiple Aggregates and an external payment gateway → a Domain Service.
Domain Layer — Interface
Application Layer — Implementation with External Gateway
8. Example: FundTransferService (Strong Consistency Across Aggregates)
Some business rules require strong consistency across Aggregates (e.g., fund transfers).
This can be enforced with a Unit of Work (UoW).
Domain Layer — Interface
Application Layer — Implementation with UoW
9. Quick Placement Guide
-
IPricingPolicyService
(interface) → Domain -
DefaultPricingPolicyService
(pure impl) → Domain -
IExchangeRateProvider
(interface) → Domain -
HttpExchangeRateProvider
(impl) → Infrastructure -
OrderApplicationService
(orchestration) → Application -
Repositories (interfaces) → Domain
-
Repository implementations → Infrastructure
✅ Summary
-
Domain Services capture business rules that don’t belong to one Aggregate.
-
They should stay stateless, pure, and named in Ubiquitous Language.
-
Keep the Domain clean: external dependencies are defined as interfaces in Domain, implemented in Infrastructure.
-
Use Application Services to orchestrate, load Aggregates, and call Domain Services.
-
For multi-aggregate updates, prefer eventual consistency with Domain Events. Use strong consistency only when absolutely necessary.
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