Implementing Domain-Driven Design Cheat Sheet

Implementing Domain-Driven Design Cheat Sheet

What's this?

The aim of this article is to provide a cheat sheet of the book Implementing Domain-Driven Design. It can specially be useful for when you are arguing with your colleagues over the concepts, things like "Can Domain Services use Repository interfaces?" and you don't feel like searching the whole book again

Do you need to migrate to DDD

Answer these questions and sum up your score. If you score 7 or above, then you need to move into DDD:

  • 0 points: Your app is a simple CRUD application
  • 1 points: You have less than 30 business operations in total (not the number of services)
  • 2 points: You have between 30 and 40 business operations in total (not the number of services)
  • 3 points: Lots of new features expected
  • 4 points: Frequent changes in requirements
  • 5 points: You don't fully understand the domain

----------------------

Anemic Domain Model

If your answer to these questions are yes, then most probably you have an anemic domain model:

  • Do domain objects contain only getter/setters?
  • Is your logic inside service layer?

----------------------

Design is the code and the code is the design

Problem space:

  • Core domain: Company's main focus
  • Subdomains: Supporting or Generic

Solution space:

  • Bounded Contexts: Each has its own Ubiquitous Language

No alt text provided for this image

The Context Map above can be considered an ideal design in a sense that each Subdomain is covered with only one Bounded Context and each Bounded Context spans across only one Subdomain, but this is not the case for most complex domains.

Bounded Context contains:

  • Domain Models
  • Application Services: Acting as a Fa?ade to provide security and transaction management. They transform use cases into execution of domain models.
  • Open-Host Service (OHS) & Published Language (PL): Some form of remote call either SOAP, REST, gRPC or with a messaging infrastructure is OHS. PL is basically the XML/JSON/proto representation of domain concepts in the Bounded Context
  • Anti-corruption Layer (ACL): Some domain services, that call an up-stream Bounded Context's Open-Host Service, using Adapters and translate their Published Language into it's own Ubiquitous Language, using Translators.

No alt text provided for this image

In this picture D means: down-stream and U means up-stream (down-stream depends on up-stream)

----------------------

Entity: Has Identity

Don't expose properties with setters. Instead have operations that mutate the Entity and its invariants.

Identity Generation

Use Value Objects for an Entity's Identity, e.g.: a ProductId class/struct or record (in C#)

  • After Insert: mostly in databases using "auto increment" or "UUID"
  • Early: a method in Repository that generates ID locally or uses "sequences" from database to get a new incremented number ID.

Surrogate Identity

For the sake of fast joins and other persistence performance considerations, we may create an ID for this purpose on an Entity (usually Integer or Long datatype)

Self Encapsulation

Access to properties goes through accessor methods, even setting properties in a constructor (Martin Fowler). These setter methods, ensure data validity.

Validation

  • Single Entity: When validating properties, throw specifically-named exceptions
  • Multi Entity: Domain Services can use Repositories to fetch multiple entities and validate them as a whole

class Book {
? ? private _title: string = ''
? ? private _isPublished: boolean = false
? ? private _publishedAt: Date | undefined
? constructor(
? ? ? title: string,
? ? ? isPublished: boolean,
? ? ? publishedAt: Date | undefined
? ? ) {
? ? this.setTitle(title)
? ? this._isPublished = isPublished
? ? this._publishedAt = publishedAt
? }
? 
? public title() {
? ? return this._title
? }
? 
? // self encapsulation
? private setTitle(title: string): void {
? ? if (!title) {
? ? ? throw new InvalidBookTitleError('Book title not specified')
? ? }
? ? if (title.length > 128) {
? ? ? throw new LongBookTitleError('Book title is too long')
? ? }
? ? this._title = title
? }
? 
? public isPublished(): boolean {
? ? return this._isPublished
? }


? public publishedAt(): Date | undefined {
? ? return this._publishedAt
? }


? // an example operation instead of two separate setters
? public publish(): void {
? ? ? this._isPublished = true
? ? ? this._publishedAt = new Date()
? }
}        

Look at comments in the example Entity above. We encapsulate the logic that sets 'title' property and this allows us to first check that it always has some value and second make sure that it's not a long string. Most of the times you may want to avoid checking things like length or security things like making sure a string is a valid Email address, because this is an Application Service concern.

----------------------

Value Object: immutable and measures, quantifies or describes

  • We should also use self encapsulation in Value Objects, but due to their immutability, setters are only and only used in the constructor
  • Replaceability: Just like assigning a number, when even changing a part of a value object, instantiate a new one and assign it:

class Name {
? ? private _firstName: string = ''
? ? private _lastName: string = ''


? ? constructor(
? ? ? ? firstName: string,
? ? ? ? lastName: string
? ? ) {
? ? ? ? this.setFirstName(firstName)
? ? ? ? this.setLastName(lastName)
? ? }


? ? private setFirstName(firstName: string): void {
? ? ? ? // some validation
? ? ? ? this._firstName = firstName
? ? }


? ? private setLastName(lastName: string): void {
? ? ? ? // some validation
? ? ? ? this._lastName = lastName
? ? }


? ? public get firstName(): string {
? ? ? ? return this._firstName
? ? }


? ? public get lastName(): string {
? ? ? ? return this._lastName
? ? }
}



entity.fullName = new Name('Saeed', 'Farahi')

// now we want to change the last name
entity.fullName = new Name('Saeed', 'Farahi Mohassel')        

  • Equality: Value Objects should implement "equals" in Java or "GetHashCode and Equals" in C#.
  • Integral Minimalism: If you want to model an up-stream entity in a down-stream, you can Map (Translator in ACL) it to a 1. Value Object with minimal properties or 2. Entity if you want to have an eventually consistent image of that up-stream Entity.

----------------------

Domain Services: Multi-Entity calculations, validation or operation

  • Can call repositories
  • Should not be named after entities, i.e.: UserService but instead be named like AuthenticationService
  • Can be interfaces with implementations in Infrastructure Layer or can be a single class implementation without interfaces
  • We should not create lots of them
  • They can act be Adapters of another Bounded Context's, Validators of multiple entities, or Calculate/Operate on some Entities in a manner that it does not make sense to put the method inside either of those entities

----------------------

Domain Events: in the whole Domain

  • Are a solution to eventual consistency across Bounded Contexts. Specially in an environment that does not support two phase commit.
  • There are not limited to a Bounded Context, meaning they are domain-wide concepts
  • Event handlers should call application services as those can handle transactions
  • Event should have a globally unique ID such as UUID/ObjectID
  • Event should have the ID of the aggregator that has published it
  • Event should have a timestamp which indicates when the Event has occurred. This timestamp is useful to prevent Events from being consumed out of order.
  • Events can be published with a lightweight Observer
  • Local subscriber should not modify another aggregate instance, because this would violate Aggregates rule of thumb: "In one transaction modify only one aggregate instance". Consistency of multiple aggregate instances must be enforced Asynchronously
  • A local subscriber called "Event Storing Subscriber" is notified by the lightweight Observer synchronously, then inserts events into Event Store. This MUST be synchronous, as we need to make sure that the event for an operation is persisted in the same transaction, therefore consistent with our model persistence.
  • Have a Table/Collection/Set in your domain's persistence store as the Event Store. Then an out-of-band event forwarder must be developed to read from this Event Store and feed the messaging infrastructure so that other Bounded Contexts are informed about new events.

No alt text provided for this image

----------------------

Aggregates: Cluster of Entities & Value Objects

  • Aggregate must stay consistent with its invariants in one transaction
  • Rule of thumb: Bounded Context must modify one aggregate instance per transaction in all cases.
  • UI must adapt itself to the rule of thumb
  • Rule 1: Design Small Aggregates
  • Rule 2: Reference other Aggregates using their ID
  • Rule 3: Eventual Consistency outside Bounded Context
  • Possible reasons to break rule of thumb: 1. UI complexity e.g. inserting multiple Aggregates at the same time or 2. Lack of timers, background tasks, messaging infrastructure or any other technical mechanisms

----------------------

Factories

  • Factory Methods: Instantiate entities in their Aggregate Root
  • Factory Services: Domain Services that comply with ACL. They use Adapters and Translators to bring a domain concept from another Bounded Context. These Domain Services are our factories.

----------------------

Repositories: Collection-Oriented or Persistence-Oriented

Repositories host only Aggregates

  • Collection-Oriented: Can be thought as an in-memory Set of Entities which can track object changes
  • Persistence-Oriented: As a List or Table of Entities where you cannot track their changes. You may have methods like save to abstract away details of how something is persisted
  • Methods: update, add, addAll, remove, removeAll, findById, nextIdentity (which gives new identity for Entity construction), finder methods e.g. findByName, findByNameAndFamily, count, countByName
  • Sometimes due to performance considerations, we need to do joins instead of pulling objects in memory and match them. Here we need to design an optimal use case query that is a complex query and returns a Value Object instead of an Aggregate. This methods returns a Value Object and not a DTO, because the query is domain specific, not application specific. Going this path is OK, but you can also consider CQRS if you see a lot of optimal use case queries being implemented in your design.

----------------------

Integrating Bounded Contexts

Using RESTful, SOAP, RPC

No alt text provided for this image

  • CollaboratorService is a special Domain Service. We should avoid calling this from other Domain Services and Aggregates as this one can have a performance penalty. Instead we should call it in an Application Service and pass the result Value Object/Aggregate to an Aggregate or Domain Service.
  • Interface of UserInRoleAdapter resides in domain layer (inner part of hexagon in Hexagonal/Ports and Adapters architecture), but it's implementation in belongs to infrastructure layer (outer part of hexagon) as it's a technicality concern. This Adapter returns either a Value Object or Aggregate. It returns an Aggregate only when we want to keep an eventual consistent reference to the original Aggregate in up-stream Bounded Context and in this case we can instead of naming it Adapter, name it Repository, remember that Repositories are in charge of Aggregate persistence/reconstitution.
  • There's NO shared library (and there shouldn't be) exposed from Identity and Access Bounded Context that contains interfaces of input/outputs of its Published Language. This makes CollaboratorTranslator a class that would convert XML, JSON, or any other serialized format to a domain model of Collaboration Context.

Using Messaging

No alt text provided for this image


  • Entity in Identity and Access Context publishes and event to event store.
  • A Forwarder reads un-published (un-acknowledged) events and sends them to Messaging Infrastructure
  • MessageListener from Collaboration Context is informed about new messages. It creates a command and then calls an Application Service
  • Application Service normally does its job managing transactions and security, and executes a business operation which is executing Domain Models.
  • The Entity in Domain Models executed

This is how a Domain Event is propagated in a system using messaging infrastructure. There are however two important requirements: 1. ensure handling events in the order they were occurred and 2. not applying an event twice. For requirement number 1 each Event can have a Date field and consumers can track the dates of events applied in their systems. For requirement number 2 an Event can have a globally unique ID (UUID/ObjectID) to allow idempotency in consumers.

----------------------

UI & Application

User Interface

In a application with remote clients e.g. in Web 2.0 applications, a User Interface can have Controllers (RESTful), Queries and Mutations (GraphQL), ..

In Web 1.0 applications such as ASP.NET or Spring MVC, UI will also contain Presentation Model (or View Model)

Data Transfer Objects

We return data to clients in the form of Data Transfer Objects (DTOs). The Application Service will use Repositories to read the necessary Aggregate instance or call custom optimal use case query to read some Value Objects, then delegates to a DTO Assembler to map the attributes of the DTO.

DTOs are always trivially serializable and never contain any behavior.

Now, Aggregates must provide some methods for DTO Assemblers so that they can query for necessary data. These methods can reveal internal state of Aggregates to outer world which is a dangerous path. Tight coupling between DTO Assembler and Aggregates can be avoided with Mediators (aka Double Dispatch and Callback).

class Customer {
  provideCustomerInterest(interest: CustomerInterest): void {
    interest.informId(this.customerId().value)
    interest.informName(this.name())
  }
}        

In this example, interest is the Mediator and Customer Aggregate informs it with various internal information.

Application Services

Application services should accept/return primitive data types and DTOs and should avoid returning Domain Objects, because all clients will need to handle them separately.

They manage transactions and security and implement use cases by executing domain models.

要查看或添加评论,请登录

社区洞察

其他会员也浏览了