Ludovic C
Ludovic C

Reputation: 3065

Domain logic in command handler or event handler?

I am using cqrs and ddd to build my application.

I have an account entity, a transaction entity and a transactionLine entity. A transaction contains multiple transactionLines. Each transactionLine has an amount and points to an account.

If a user adds a transactionLine in a transaction that already has a transactionLine that points to the same account as the one the new transactionLine, I want to simply add the new transactionLine amount to the existing one, preventing a transaction from having two transactionLines that point to the same account.

Ex :

Before command :
    transaction
        transactionLine1(amount=100, account=2)
        transactionLine2(amount=50, account=1)

Command :
    addNewTransaction(amount=25, account=1)

Desired result :
    transaction
        transactionLine1(amount=100, account=2)
        transactionLine2(amount=75, account=1) // Add amount (50+25) instead of two different transactionLines

instead of

transaction
    transactionLine1(amount=100, account=2)
    transactionLine2(amount=50, account=1)
    transactionLine3(amount=25, account=1) // Error, two different transactionLines point to the same account

But I wonder if it is best to handle this in the command or the event handler.

If this case is handled by the command handler

Before command :
    transaction
        transactionLine1(amount=100, account=2)
        transactionLine2(amount=50, account=1)

Command :
    addNewTransaction(amount=25, account=1)  // Detects the case

Dispatches event
    transactionLineAmountChanged(transactionLine=2, amount=75)
  1. AddTransactionLine command is received

  2. Check if a transactionLine exists in the new transactionLine's transaction with the same account

  3. If so, emit a transactionAmountChangedEvt event

  4. Otherwise, emit a transactionAddedEvt event

  5. Corresponding event handler handles the right event

If this case is handled by the event handler

Before command :
    transaction
        transactionLine1(amount=100, account=2)
        transactionLine2(amount=50, account=1)

Command :
    addNewTransaction(amount=25, account=1)

Dispatches event
    transactionLineAdded(transactionLine=3, amount=25)

Handler  // Detects the case
    transactionLine2.amount = 75 
  1. AddTransactionLine command is received

  2. TransactionLineAdded event is dispatched

  3. TransactionLineAdded is handled

  4. Check if the added transaction's transactionLine points to the same account as an existing transactionLine in this account

  5. If so, just add the amount of the new transactionLine to the existing transactionLine

  6. Otherwise, add a new transactionLine

Upvotes: 4

Views: 2079

Answers (2)

Pepito Fernandez
Pepito Fernandez

Reputation: 2440

Think of commands and events as 'containers', 'dtos' of data that you are going to need in order to hydrate your AggregateRoots or send out to the world (event) for other Bounded Contexts to consume them. That's it. Any other operation that is strictly related to your Domain has no place but your AggregateRoots, Entities and Value Objects.

You can add some 'validation' to your Commands, either by using DataAnnotations or your own implementation of a validate method.

public interface ICommand
{
    void Validate();
}

public class ChangeCustomerName : ICommand
{
    public string Name {get;set;}

    public void Validate()
    {
        if(Name == "No one")
        {
            throw new InvalidOperationException("Sorry Aria Stark... we need a name here!");
        }
    }
}

Upvotes: 1

Lev
Lev

Reputation: 309

Neither commands nor events should contain domain logic, only the domain should contain the domain logic. In your domain, aggregate roots represent transaction boundaries (not your transaction entities, but transactions for the logic). Handling logic within commands or events would bypass those boundaries and make your system very brittle.

The right place for that logic is the transaction entity.

So the best way would be

AddTransactionCommand finds the correct transaction entity and calls
Transaction.AddLine(...), which does the logic and publishes events of what happened
TransactionLineAddedEvent or TransactionLineChangedEvent depending on what happened.

Upvotes: 3

Related Questions