Implementing Domain Driven Design
Overall
Domain-Driven Design (DDD) is an approach to software development that focuses on the development of programming a domain model that has a rich understanding of the processes and logic of a domain. The term is referred from a 2003 book by Eric Evans that depicts the approach through a list of patterns.
Today’s contents:
- Domains, SubDomains
- Bounded Context
- Context Maps
- Architecture
- Entites
- Value Objects
- Aggregates
- Repositories
- Factories
- Services
- Integrating Bouded Context
- Example
Notes:
- Almost all the knowledge I'm going to share here is from the book "Implementing Domain-Driven Design" by Vaughn Vernon, it provides steps to implement DDD with Java code examples, and it helps you get ideas and apply them to other languages.
- I also took the reference when applying DDD using Go language from https://threedots.tech/tags/domain-driven-design/
Domain
What's a Domain Model
- Is often implemented as object models
- Has both data and behaviors
- The problem space enables us to think of a strategic business challenge to be solved.
- The solution space focuses on how we will implement the software to solve the problem of the business challenge.
Why you should do DDD
- The design is the code, and the code is the design.
- It provides software development technicals that address both strategic design and tactical design.
How to do DDD
- Ubiquitous Language
- Is a shared team language
- Language shared by domain experts and developers (developed by the team)
- Bounded Context
- As a conceptual boundary around a whole application
- Every use of a given domain term, phrase, or sentence - the Ubiquitous Language - inside the boundary has a specific contextual meaning.
- There is one Ubiquitous Language per Bounded Context
Strategic Design
Domain and Subdomains
Domain
- Can refer to both the entire domain of the business, as well as just one core or supporting area of it
- CORE DOMAIN
- SUB DOMAIN
Bounded Context
What's Bounded Context
- A specific solution, a realization view used to realize a solution as software
- Is Explicit and Linguistic (Boundary)
- Is an explicit boundary within which a domain model is exists
- Inside the boundary all terms and phrases of the Ubiquitous Language have specific meanings and the model reflects the Language with exactness
- Models are developed in a Bounded Context
- A Bounded Context is a logical and physical boundary around a coherent and consistent domain or subdomain
Context Map
Purpose
- Illustrates how the actual software-bounded contexts in the solution space are related to one another through integration
- What is the relationship between these Bounded Contexts
- Partnership
- Shared Kernel
- Customer-Supplier Development
- Conformist
- Anticorruption Layer: As a downstream client, create an isolating layer to provide your system with functionality of the upstream system in terms of your own domain model. This layer talks to the other system through its existing interface, requiring little or no modification to the other system. Internally, the layer translates in one or both directions as necessary between the two models
- Open Host Service: Define a protocol that gives access to your subsystem as a set of services. Open the protocol so that all who need to integrate with you can use it
- Published Language: The translation between the models of two Bounded Contexts requires a common language. Use a well-documented shared language that can express the necessary domain information as a common medium of communication, translating as necessary into and out of that language. Published Language is often combined with Open Host Service
- Separate Ways
- Big Ball of Mud
- For detail explaination of Partnership, Shared Kernel, Customer-Supplier Development, Conformist, Separate Ways, Big Ball of Mud can read more in the reference book I listed at the ending of the article
Abbreviations
- U: Upstream
- D: Downstream
- OHS: Open Host Service
- PL: Published Language
- ACL: Anticorruption Layer
IDP and Digitial Wallet
- Login
Architecture
Big Advantages of DDD
- Doesn’t require the use of any specific architecture
- Use just the right choices and combinations of architecture and architectural patterns
Layer Architecture
Hexagonal or Ports and Adapters
A Combination of Many Architectures
Tactical Design
- How to implement the solution by code
- OOP good practices
Terms
- Entities
- Value Objects
- Aggregates
- Repositories
- Factories
- Services
Entities
- When we care about individuality
- Mutable (change state)
Value Objects
Immutable Unidentifiable Structs
- No Identifier
- Measures, Quantifies, or Describes a thing in Domain
- Can be maintained as Immutable
- Models a Conceptual Whole by composing related attributes as an integral unit
- Completely replaceable when the measurement or description changes
- Can be compared with others using Value equality
- Supplied its collaborators with Side-Effect-Free Behavior
Persisting Value Objects
- Single Value Objects
- Store each of the attributes of the Value in separate columns of the row where its parent Entity is stored. Said another way, a single Value Object is denormalized into its parent Entity’s row
- Many Values Serialized into a Single Column
- There are some potential drawbacks to consider:
- Column Width
- Must query: If any of the Value attributes must be queryable, you may not use this option (nowadays MySQL and other DB types can support JSON match)
- Many Values Backed by a Database Entity
- Treat the Value type as an entity in the data model, as a noted reiteration, it's persisted as an entity in the data model does not mean it should be presented as an Entity in the domain model - Design your data model for the sake of your domain model, not your domain model for the sake of your data model.
- The database's primary key will be implemented in storage persistence and hidden from the domain model.
- Many Values Backed by a Join Table (In the example section when implementing the data persistence for Tenant-Member-Roles I use this approach)
Aggregates
- An Aggregate is composed of either a single Entity or a cluster of Entities and Value Objects that must remain transactionally consistent throughout the Aggregate's lifetime
- Unique Identifier by Root Entity (Aggregate root)
- Is synonymous with transactional consistency boundary
- A properly designed Aggregate is one that can be modified in any way required by the business with its invariants completely consistent within a single transaction
- A properly designed Bounded Context modifies only one Aggregate instance per transaction in all cases
- Is persisted using it's Repository
Rules
- Model True Invariants in Consistency Boundaries
- An invariant is a business rule they must always be consistent
- Design Small Aggregates
- Reference Other Aggregates by Identity
- Use Eventual Consistency Outside the Boundary
Reasons to Break the Rules
- User Interface Convenience
- Lack of Technical Mechanisms
- Global Transactions
- Query Performance
- ...
Repositories
- An implementation of storing aggregates or other information
- Store and manage aggregates
- 1-1 relationship between an Aggregate type and a Repository
- However, sometimes when two or more Aggregate types share an object hierarchy, the types may share a single Repository
- Designs
- Collection-oriented design
- Persistence-oriented design
Factories
- Creating complex, value objects, entities, aggregates
Services
INTEGRATING BOUNDED CONTEXT
- Using RESTful Resource
- Using Messaging
- .....
Example
Recognize Domain & Sub Domain
Recognize Bounded Context
Recognize Context Map
- Simple Context Map
- Zoom in on Context map
Tactical Design
Note: Code example provided using Go language
The Data Model Should Be Subordinate
1st example: let's walk to the first example of a User in the system. As you can see in the Login diagram we model a User as Aggregate (Root) along with Value Objects: InternalUserIdentifier, ExternalUserIdentifier, Email, UserToken. Whenever we need to access an Entity, Value Object requires us to always access via AggergateRoot (User). This is the rule in DDD we should keep in mind, and to access an Aggregate we need to provide a repository:
- User (Aggregate) <---> UserRepository (Repository)
2nd example: let's model a concept of a wallet group that has many wallet details. In this business requirement maybe we will have 2 approaches:
1st approach: If every time we add/update a wallet group, it requires us to add/update the group's wallet details as well then we should design WalletGroup as an Aggregate Root and Wallet Details as Entities (or Value Objects - it depends on the business if there is no unique identifier required). As a result:
- Aggregate: Wallet Group
- Entities: Wallet Detail
- Value Objects: Group Info, Apply Info, Approval Info, ...
- Repositories: WalletGroupRepo
2nd approach: if the operation on the wallet group and wallet detail don't happen at the same time, eg: after creating a wallet group then we will add wallet details to the wallet group later (another intuitive example is: after a Product Owner creates a User Story then developers will add Tasks to a User Story later) or
- Aggregate: Wallet Group, Wallet Detail
- Value Objects: Group Info, Apply Info, Approval Info, ...
- Repositories: WalletGroupRepo, WalletDetailRepo
3rd example: let's model the concept of a tenant's member and a member having some roles.
- Aggregates: Member
- Value Objects: Display Name, Email Address, Roles
- Repositories: MemberRepo
Notes:
Inside the Domain & Application Layer we won’t see any usage of primitive data type in a direct way: string, integer, … except for boolean value (but rarely)
- The issue of passing the wrong order of params goes away due to strong type-checking
- Func func1(a int, b int)
- var a int =1
- var b int =1
- We can not detect we pass wrong order: func1(a, b) and func1(b,a)
- Func func2(a INTA, b INTB)
- type INTA int
- type INTB int
- func2(a, b) and func2(b,a)With custom type we can detect the wrong order of pass in a & b variable:
- Func func1(a int, b int)
- Contribute to the Rich Model
- We can add more business logic on user-defined type