I am writing my first application using DDD (in Node with TS) and I started writing all the domain first -- before starting the repositories/DB and then the application, while writing unit tests for each entity. As my domain developed, I started to have more doubts about my business logic. I'll give examples below.
One of my business logic states that a "requester" can create and delete tags. So, becuase I am writing all the domain first, I created a method createSectorTag inside my Requester entity. The method is defined below as follows:
public createSectorTag(data: ISectorTagData): number {
this.checkIfIsActive()
const sector_tag = new SectorTag(data)
return this.sector_tags.push(sector_tag)
}
As you can see, the method creates a SectorTag entity and adds it to a private array in my Requester entity. Since there's a bit of logic in this method, the this.checkIfIsActive(), I think it makes sense for this piece of code exist inside my domain. But, at the same time, I think if it isn't too much work for a simple thing: I mean, every time my application calls my domain for a requester to add a tag, it'll have to create a Requester entity, and then add it to requester.sector_tags; and after, I'll have to get the new SectorTag and persist it to the DB with a repository.
Another example is the deleteSectorTag method. This action has a logic that should validate if any of the request inside a requester have that tag, and if any has, an exception should be raised. The method's definition:
public deleteSectorTag(index: number): void {
/**
* Here I'll have to check if any of the requests inside the Request entity have
* the tag specified by the index in the parameter, and if so, raise an exception
*/
this.sector_tags.splice(index, 1)
}
But again, I believe that all this business logic adds a bit too much weight on processing. I'll have to get all the requests of a requester from the DB, create a Requester entity, add the requests inside the entity and then make the validation. But it all seems like it could be a query in the DB for the validation.
Well, I really like the idea of putting all logic inside the domain; it makes me happy because it seems like my code gets closer and closer to reallity. It adapts to the way I think, but at the same time I am worried about performance.
If anyone can give me hints on this, I'd be very thankful.
Here is the full Requester entity:
import { BudgetRequest, SectorTag, BudgetEstimator } from '@/domain/BudgetRequest/entities'
import { RequesterIsInactiveError } from '@/domain/BudgetRequest/exceptions'
import {
IBudgetRequestData,
IRequesterData,
ISectorTagData,
} from '@/domain/BudgetRequest/interfaces'
export class Requester {
private readonly active: boolean
private readonly name: string
private readonly cnpj: number
private budget_requests: Array<BudgetRequest> = []
private sector_tags: Array<SectorTag> = []
private budget_estimators: Array<BudgetEstimator> = []
constructor(data: IRequesterData) {
this.active = data.active
this.name = data.name
this.cnpj = data.cnpj
}
get budgets_list(): Array<BudgetRequest> {
return [...this.budget_requests]
}
get sector_tags_list(): Array<SectorTag> {
return [...this.sector_tags]
}
public createBudgetRequest(data: IBudgetRequestData): number {
this.checkIfIsActive()
const budget_request: BudgetRequest = new BudgetRequest(data)
return this.budget_requests.push(budget_request)
}
public createSectorTag(data: ISectorTagData): number {
this.checkIfIsActive()
const sector_tag = new SectorTag(data)
return this.sector_tags.push(sector_tag)
}
public deleteSectorTag(index: number): void {
/**
* Here I'll have to check if any of the requests inside the Request entity have
* the tag specified by the index in the parameter, and if so, raise an exception
*/
this.sector_tags.splice(index, 1)
}
public bindSectorTagToBudgetRequest(i: number, j: number): void {
this.budget_requests[i].addSectorTag(this.sector_tags[j])
}
private checkIfIsActive(): void {
if (!this.active) {
throw new RequesterIsInactiveError()
}
}
}
Implementing a domain model doesn't mean that we have to give up on a sensible data model.
If your implementation requires loading into memory a bunch of information that isn't actually useful - that's a sign that your model isn't currently a good fit to your problem.
When you have a relationship between two pieces of information, you need to think carefully about three cases
A quick heuristic to understand whether two pieces of information belong together: should making changes to one of them require locking out all changes to the other?
In your example: should modifying tags block changes to the budget requests?
If the answer is yes - it's critical to the business that that information always be in agreement - then it makes sense to group that information together.
If the answer is no, then you should be thinking about locking the information independently, which will usually involve more entities.
(Hint: the answer is usually "no".)
A question you can use to distinguish the two: how quickly does the data need to be in agreement. If you can offer to change all of the information somewhere between 10 seconds and a minute, and the domain experts say "yes, that's fine", then the information certainly doesn't need to share a lock, and you should consider candidate designs that don't, just to see if it makes things better.
Also, it's worth noting that some parts of our data model are really "just" caches of information that lives somewhere else, and all we really need to do is update our locally cached copy using general purpose tools (ex: cascade delete).
In those places, building out a domain model can be more effort than it is worth.
Don't create a domain model to address problems better solved by an anemic data store.