The criteria to make a better decision in DDD
Overview
In domain-driven development (DDD), it is necessary to choose an approach that suits the specific requirements and context of your project. It is crucial to understand the characteristics and applicability of each method for making the right decision. This article organizes key decision-making criteria as a helpful reference when selecting the appropriate approach. If code examples are provided, they will be written in TypeScript.
Domain Objects
Primitive value vs Object value
- Q. Should you use primitive value or object value to express the attribute of domain object?
class FullName {
// string? or create GivenName/FamilyName class?
constructor(givenName:???, familyName:???) {
...
}
}
- Criteria
- Key1: Does the attribute have any rules to express itself?
- Such as validation or formatting etc.
- Key2: Do you wanna use the attribute by itself?
- Key1: Does the attribute have any rules to express itself?
- Yes: Value Object
- No: Primitive Value
Value Object vs Entity
-
Q. Should you use the Value object or Entity to express the domain object?
-
Criteria
- Key1: Does it have a lifecycle from creation to disposal?
- Key2: Do you need to identify it uniquely?
- Key3: Do you need to transit its state?
-
Yes: Entity
-
No: Value Object
Extraction to Domain Service
-
Q. Should you extract the behavior to a Domain Service?
-
Criteria
- Key1: Does it need to inquire to data store or external service?
- Key2: Does it concern in cross-domain?
- Key3: Is it too complex so that it could overly bloat a Value Object or Entity?
-
Yes: Extract it to Domain Service
-
No: Leave it in Value Object or Entity.
Leave the behaviors in Value Object or Entity as much as possible. Ultimately it leaves only getters/setters if you extract every behavior to Domain Service, which is quite uncomprehensive.
Placement of Factory
-
Where you should define factory?
-
Criteria
- Key1: Do you need to use internal data of the domain object?
- Key2: Is it unlikely to be so big code?
-
Yes: Domain method
-
No: Separated class
Repository vs Unit of Work
-
Should you use Repository or Unit of work as Infrastructure?
-
Criteria
- Key1: Do you need transaction?
- Key2: Is it cross repository?
-
Yes: Unit of Work
-
No: Repository
Direct manipulation to domain object
-
Is it acceptable to manipulate the domain object out of the aggregate?
-
Criteria
- No. Definitely request the modification to Root Aggrecate
Merging aggregate
-
Shold you merge the aggregates?
-
Criteria
- Key1: Do you need to save them in a transaction?
- Key2: Could it keep the consistency of Boundaries?
-
Yes: Merge them.
-
No: Leave them
Splitting aggregate
-
Should you split the aggregate?
-
Criteria
- Key1: Is it possible to save them in separate transaction?
- Key2: Do you need additional and different adjective to express it occasionally?
- For example, the user in order and the user in delivery
-
Yes: Split them
-
No: Leave them
Flexible handling of domain rules
-
Q. How can you switch the complex domain rules?
-
Criteria
-
Case1: Do you wanna switch the judgemental conditions?
- Yes: Specification pattern
-
Case2: Do you wanna switch the abstruct business rules ?
- Yes: Policy pattern
-
Case3: Do you wanna switch the concrete algorithms
- Key1: Do you wanna reduce redundant switch clause?
- Yes: Strategy pattern
- Key1: Do you wanna reduce redundant switch clause?
-
Remember Policy and Strategy patterns are actually the same, but the abstraction of the target. Policy is high and Strategy is low.
DRY principal
-
Q. Should you apply DRY principal and merge the code as common/shared module?
-
Criteria
- Key1: Do you need internal status control after merge?
- Key2: Is the code likely to be modified in the future?
-
Yes: Avoid generize the code
-
No: Merge them.
Remember you would have Control Coupling in the module when you made a mistake to merge the codes.
Sometime you'd better to respect YAGNI or KISS principals.
Application Service
Return value of Application Service
-
Q. Should you use DTO as a return value of the application service?
-
Criteria
- Key1: Do you wanna encapsulate domain object from unexpected manipulation by its caller?
- Key2: Is it acceptable to define DTO class every time?
-
Yes: Use DTO
-
No: Return domain object
Command Object
-
Q. Should you use Command Object as a input value of the application service?
-
Criteria
- Key1: Are you likely to extend the input value in the future?
-
Yes: Use Command Object
-
No: Enough to set separated values.
State of Application Service
-
Q. Should the application contain the field?
-
Criteria
- Key1: Does it control the behavior of Application service?
class AppService() {
shouldSend: bool
public CreateSomething(Command commandObject) {
// Bad case: it changes the behavior of this applicaiton sercice
if(shouldSend) {
...
}
}
}
- Yes: Avoid to keep the field. Consider to remove it
- What about separating it to new service, or moving it to domain object for example
- No: Keep it.
Architectual Design
Cooperation strategy between customer and supplier in Bounded Context
-
Q. How should you integrate APIs between different Bounded Contexts?
-
Criteria
-
Case1: Is Supplier stronger than customer?
- Key1: Can you accept all modification from Supplier?
- Key2: Does Supplier obey Solid Standard and handy to use their APIs from customer?
- Yes: Accept as Conformist
- No
- Key1: Does customer contains the core subdomain?
- Key2: Does Supplier serves tricky response for customer?
- Key3: Does Supplier frequently modify their interface?
- Yes: Insert Anti-Corruption Layer
-
Case2: Is Supplier as strong as customer?
- Yes: Can syncronize the interface timely as Closer Cooperation
- Key1: Are they core subdomain?
- Key2: Can the teams cooperate swiftly enough to accept the cost to adjust Shared module?
- Yes: You may adopt Shared Kernel instead of API integration.
- Yes: Can syncronize the interface timely as Closer Cooperation
-
Case3: Is Supplier weaker than customer?
- Yes: Supply Shared Service
- Key1: Does Supplier supplies to multiple Bounded Contexts?
- Yes: Provide Published languages for each context.
- Key1: Does Supplier supplies to multiple Bounded Contexts?
- Yes: Supply Shared Service
-
Business Logic
*Q. How should you select implementation stragtegy?
- Criteria
-
Case1: Core Subdomain
- Key1: Need traceability for analysis or strict transaction management?
- Yes: Event-Sourced Domain Model
- Test strategy: Pyramid style
- Architecture: CQRS
- No: Domain Model
- Test strategy: Pyramid style
- Architecture: Port and Adopter
- Yes: Event-Sourced Domain Model
- Key1: Need traceability for analysis or strict transaction management?
-
Case2: Supporting/General Subdomain
- Key1: Complex Data structure?
- Yes: Active Record
- Test strategy: Diamond Style
- Architecture: 4-Layered Architecture
- No: Transaction Script
- Test strategy: Inversted Pyramid Style
- Architecture: 3-Layered Architecture
- Yes: Active Record
- Key1: Complex Data structure?
-
CQRS
-
Should you adopt CQRS ?
-
Criteria
- Key1: Adopt Mulitple Persistent Model?
- Key2: Adopt Event Sourcing Domain Model?
- Yes: Adopt CQRS.
Conclusion
Mastering DDD requires making the best decisions at every turn. By carefully evaluating your project's current state and applying the right improvements at each stage, you can unlock the full potential of your business, driving seamless and dynamic growth toward success!
Discussion