Tuesday, January 24, 2012

My summary of CQRS and DDD

Recent epiphany: sagas are just ways to turn domain events into commands.

Here's my as-of-now summary of the concepts I see in many EventSourcing, CQRS, and DDD videos and blog posts.

Command: A simple message that represents a user's request to make something happen. Just data with no behavior and often named in the imperative tense.
class CloseAccount implements Command
{
  public AccountId accountId;
  public String reason;
 
  public CloseAccount(AccountId accountId, String reason){
    this.accountId = accountId;
    this.reason = reason;
  }
}

CommandHandler: A domain service that turns a command into events by calling methods on an aggregate root, publishes the resulting events, and handles transactions and persistence. Stateless with just behavior; just a task based method and sort of procedural.
class AccountCommandHandler extends CommandHandler
{
  private AccountRepository _repository;
  private MessageBus _messageBus;
 
  public AccountCommandHandler(AccountRepository repository, MessageBus messageBus){
    this._repository = repository;
    this._messageBus = messageBus;
  }
 
  public void handle(CloseAccount command){
    Account account = _repository.get(command.accountId);

    account.close(command.reason);
  
    persistAndPublishEvents(account);
  }
}

Aggregate Root: A persistent domain object that turns method calls into domain events. May contain references to other domain objects as part of a parent/child relationship. Stateful and behaviorful and guaranteed to be internally consistent.
class Account extends Aggregate
{
  private AccountId id;

  public Account(AccountId id){
    this.id = id;
    super.storeEvent(new AccountCreated(this.id));
  }

  public void Close(CloseReason reason){
    super.storeEvent(new AccountClosed(this.id, reason));
  }
}

Domain Event: A simple message that represents a change to the state of an aggregate. Just data with no behavior and often named in the past tense.
class AccountClosed implements DomainEvent
{
  public AccountId accountId;
  public CloseReason reason;
 
  public AccountClosed(AccountId id, CloseReason reason){
    this.accountId = id;
    this.reason = reason;
  }
}

MessageBus: A mechanism for publishing commands and events and subscribing handlers to commands and events. May be a separate EventBus and CommandBus. The only behavior is publishing events and subscribing event and command handlers to events; the only state is what's required to track which services have subscribed to which events.
public class SimplestEventBus implements EventBus
{
  private List<Handler> handlers = new ArrayList<Handler>();
  
  public void subscribe(Handler handler)
  {
    handlers.add(handler);
  }
  
  public void publish(DomainEvent event)
  {
    for (Handler handler : handlers)
      handler.handle(event);
  }
}
Saga: A persistent object that turns domain events into commands. Stateful and behaviorful. The only state is what's required for knowing when to create the commands.
// Send a notice to the account's owner once the account is
// both paid off and closed. (ignore thread safety since this is a simple example)
class OwnerNotificationSaga implements Saga
{
  private AccountRepository _repository;
  private MessageBus _messageBus;
 
  private List<Account> closedAccounts = new ArrayList<Account>();
  private List<Account> paidOffAccounts = new ArrayList<Account>();
 
  public OwnerNotificationSaga(AccountRepository repository, MessageBus messageBus){
    this._repository = repository;
    this._messageBus = messageBus;
  }
 
  public void handle(AccountClosed event){
    Account account = _repository.get(event.accountId);
  
    if (closedAccounts.contains(account))
      return;
  
    if (paidOffAccounts.contains(account)){
      sendNotice(account);
      paidOffAccounts.remove(account);
    } else {
      closedAccounts.add(account);
    }
  }
 
  public void handle(AccountPaidOff event){
    Account account = _repository.get(event.accountId);
  
    if (paidOffAccounts.contains(account))
      return;
  
    if (closedAccounts.contains(account)){
      sendNotice(account);
      closedAccounts.remove(account);
    } else {
      paidOffAccounts.add(account);
    }
  }
 
  private void sendNotice(Account account){
    _messageBus.publish(new NotifyOwner(account.ownerId));
  }
}

So, for many of the examples I've seen, a user creates Commands that are handed to a MessageBus that dispatches it to a subscribed CommandHandler. The CommandHandler loads the relevant Aggregate and calls methods on it. The resulting events are gathered, persisted (if doing Event Sourcing), and passed to the MessageBus. The subscribers to that event will often update some read model (effectively turning domain events into updates on report tables) but they may be Sagas that track events and create related commands when necessary.

None of that is the essence of DDD or the essence of CQRS, but it's so common to the discussions and examples that this little glossary helps me to keep things straight.

3 comments:

  1. Very well said. I'm learning DDD+CQRS myself and I see it exactly how you just stated it - in a way validates that I am understanding this correctly.

    ReplyDelete
  2. As Udi Dahan points out "Sagas ARE domain models". This an important incite as the point of the domain model (Saga) is to accurately represent time in relation to the domain objects for a context. I would say that "sagas are just ways to turn domain events into commands" as a canned statement is incomplete in that it fails to capture the idea of a domain model.

    ReplyDelete
    Replies
    1. I suppose sagas represent business rules so they could be considered domain models that coordinate other domain models. That's something to think about.

      Delete