Let's imagine that I have a Use Case to transfer money, and one to pay a service using my bank account. In both cases, money needs to be deducted from an account. Let's also imagine that an account can deduct balance if it does not have any security concern (provided by another service).
There are two ways to do this (at least that's how I see it).
- You create a DeductBalanceUseCase and AddBalanceUseCase:
export class DeductBalanceUseCase { async execute(accountId: string, amount: number) { const account = await this.accountRepository.findById(accountId); const hasRisk = await this.accountRiskAdvisor.hasRisk(accountId); if (hasRisk) { throw new Error(`This account has risks`); } account.deductBalance(amount); await this.accountRepository.save(account); }}
And then call this Use Case from wherever is needed.
class TransferMoneyUseCase { async execute(sourceAccountId: string, destinationAccountId: string, amount: number) { await this.deductBalanceUseCase.execute(sourceAccountId, amount); await this.addBalanceUseCase.execute(destionationAccountId, amount); }}class PayServiceUseCase { async execute(sourceAccountId: string, servceId: string, amount: number) { await this.deductBalanceUseCase.execute(sourceAccountId, amount); await this.servicePaymentProvider.makePayment(serviceId, amount); }}
The problem with this approach, of course, is the transactional behavior, mostly in the transfer money use case. You could still use some kind of decorator that spans the transaction accross use cases (even though I don't consider this to be a good practice).
- You create a DeductBalanceService that acts like a Domain Service and that takes Account entity as an argument.
export class DeductBalanceService { async deduct(account: Account, amount: number) { const hasRisk = await this.accountRiskAdvisor.hasRisk(account.id); if (hasRisk) { throw new Error(`This account has risks`); } account.deductBalance(amount); }}
And then use this service where needed
class TransferMoneyUseCase { async execute(sourceAccountId: string, destinationAccountId: string, amount: number) { const account = await this.accountRepository.findById(sourceAccountId); const destinationAccount = await this.accountRepository.findById(destinationAccountId); await this.deductBalanceService.deduct(account, amount); destionationAccount.addBalance(amount); await this.transaction.save([account, destinationAccount]); }}class PayServiceUseCase { async execute(sourceAccountId: string, servceId: string, amount: number) { const account = await this.accountRepository.findById(sourceAccountId); await this.deductBalanceService.deduct(account, amount); await this.servicePaymentProvider.makePayment(serviceId, amount); await this.accountRepository.save(account); }}
What's your take on this? I know that there are a lot of details lacking (compensatory transactions, locks, etc) but mostly I want to know your take on this. I've found articles to both sides of this.
I'm using the second approach mostly now, encapsulating the business rules without dealing with retrieval or storage and letting that to the client.