Domain Layer

Entity

  • Object with unique identitity (e.g. Customer and Order)

  • Represents the business object in the problem domain

  • Have business rules associated

  • Mutable (from the methods)

Notes

  • Similar entities can exist on multiple different Bounded Context, but those entities must be contextualized with the BC.

  • If you need data from an entity which is located on another BC, you might need to:

    • use anti corruption layer (ACL)

    • use shared kernel

    • use integration event (e.g. pubsub or other form of messaging)

Example

Here's an example of SavingsAccount class which is an entity. Even if the same customer has 2 savings account with the account type, they're still two different savings account which have different account number.

class SavingsAccount(BankAccount):
	account_number: str
	type: SavingAccountType
	customer: Customer
	status: LoanStatus
	balance: Balance

	def __init__(...):
		...  # constructor here

	def deposit_money(self, amount: float, currency: Currency):
		if self.balance.currency != currency:
			amount = convert_currency(amount, currency)
		self.balance += amount
		
	def withdraw_money(self, amount: float):
		if amount < 5:
			raise Exception("minimum transfer to other bank is $5")
		self.balance -= amount

Value Object

  • Object which represents a simple value (e.g. balance of savings account)

  • Doesn't have unique identity

  • Doesn't contain business logic

  • Should be immutable

Note

  • Can contains method for non business logic

Example

class Balance:
	amount: float
	currency: Currency

Aggregate

  • Aggregate is a way to group related entities and value objects

  • Each aggregate must have one aggregate root, the aggregate root must be entity

  • The aggregate root is the only one accessible from outside

  • Here are a few guidelines that can help you decide whether multiple entities should be grouped into a single aggregate:

    • Cohesion: The entities in an aggregate should be closely related and should form a single, cohesive concept in the domain.

    • Consistency: The entities in an aggregate should have a consistency boundary, meaning that their invariants must be maintained within a single transaction.

    • Bounded Context: The entities in an aggregate should belong to the same bounded context, meaning that they should be part of the same sphere of knowledge in the domain.

    • Access Patterns: The entities in an aggregate should be accessed together frequently, and it should be common to load all of them in a single query.

Note

  • If there's some data from another entity from another aggregate (but same BC) that you might need, you can refer to the other entity outside the aggregate by copying the entity id instead the whole entity

  • Loading the whole aggregate from the database can be more expensive in terms of performance, especially if the aggregate is large or contains a lot of data. However, in DDD the aggregate is considered as a consistency boundary, and it's recommended to load the whole aggregate in a single transaction, this helps to maintain the consistency of the aggregate's invariants, and to avoid inconsistencies between aggregate's parts. Other option is to use lazy loading.

  • How we group aggregates might change over time, it might be a good idea to split aggregates if it's getting too big

Domain Service

  • Business logic which doesn't naturally fit into an entity goes here

  • Usually business logic which involves a multiple domain object (entities)

Note

  • Domain service operations can be represented by method (inside object) or function, using function is great if you're not planning to make the service stateful

Example

def transfer(sender: BankAccount, recipient: BankAccount, amount: float):
	sender.deduct(amount)
	sender.add(amount)

Factory

  • To create entities and value object consistently

  • Purely a domain model, different with repository, factory has no infra concern at all

Note

  • Usually it's needed when the construction is complex and involves creation of other objects, if it's simple then you might not need it

  • It violates some of the abstraction on the domain object, so use it only when needed

  • Can also be responsible for creating objects when retreiving the data from data storage layer

  • Use it when needed, you MIGHT NOT need it if:

    • construction is fairly simple

    • construction doesn't involve creation of other objects and all the needed data can be easily passed via object constructor

    • client is interested with the construction implementation strategy

    • the class is the sole type, there's no hierarchy (parent-child class relation) involved, which means there's no need to make a constructor with construction type options

Repository

  • Allows aggregate roots insertion, retrieval, modification, and deletion at storage

  • Abstraction over the data storage (infrastructure layer)

Note

  • Usually only the interface is at the domain layer (the interface is considered purely a domain model)

  • The implementations are at the infrastructure layer

  • Might have cache layer

Last updated