User Tools

Site Tools


technology:domainmodel:processmanager

This is an old revision of the document!


Process Manager

Für mich ist ein Process Manager1) keine Saga. Der Process Manager verwendet aber Sagas, kann aber auch weitere Dinge enthalten wie z.B. Aggregates, usw. Ein Prozess wird also dadurch definiert, wie er ausgelöst wird, und was die einzelnen Prozess Schritte sind. Wenn ein Prozess implizit ausgelöst wird, besteht er i.d.R. nur aus einer Saga, wenn er aber explizit ausgelöst wird, besteht er aus einem Aggregate, welches vom Client verwendet wird, um den Prozess explizit auszulösen. Das kann am besten an einem Beispiel erklärt werden.

Benutzerkonten mit eindeutigen Benutzernamen

Die Aufgabe ist es, mit CQRS und Event Sourcing sicher zu stellen, dass Benutzernamen nicht doppelt erstell werden. Das ist gar nicht so einfach. Viele erfolglose Versuche2), technische Lösungen für die einhudert prozentige Einhaltung von Regeln zu finden, die mehrere Aggregate umfassen, waren zum Scheitern verurteilt.

Entweder läßt man zu, dass die Regeln nur mit einem gewissen Grad an Sicherheit eingehalten werden, und findet Lösungen in der Sprache des Businesses, um die Ausnahmefälle zu lösen, oder aber man muss Aggregates definieren, um die Regeln einzuhalten. Das ist auch immer ein trade-off zwischen garantierter Datenkonsistenz und Aufwand/Kosten, diese Konsistenz einzuhalten vs. der Kosten, um die Fälle zu lösen, in denen die Regel mal nicht eingehalten wird3).

Hier nun meine Abhandlung zu der Aufgabe4)

public class Account: AggregateRoot
{
  public Account(Guid id, string password)
  {
      ApplyChange(new AccountAdded(id, password));
  }
}
public class UniqueUserNames: AggregateRoot
{
  private IDictionary<Guid, string> _names = new Dictionary<Guid, string>();
 
  public UniqueUserNames(Guid id)
  {
      if (!id.Equals(Guid.Empty)) throw new ArgumentException("Illegal UniqueUsernames Aggregate ID"); // probable better ways to make it "singleton", i.e. a global rule over all usernames. Works with "real" ids just as well for non-global rules
      ApplyChange(new UniqueUserNamesAdded(id));
  }
 
  public void AddUserName(Guid id, string name)
  {
    if (_names.ContainsKey(id)) throw new DuplicateAccountIdException(); // this really should not happen, because accounts with unique ids are always created before the name is added...
    if (_names.Values.Contains(name)) throw new DuplicateUserNameException();
    ApplyChange(new UniqueUserNameAdded(id, name)); // (the event is straight forward, no need to show its declaration in this example)
  }
 
  public void RemoveUserName(Guid id)
  {
    ApplyChange(new UniqueUserNameRemoved(id)); // (the event is straight forward, no need to show its declaration in this example)
  }
 
  private void Apply(UniqueUserNameAdded @event)
  {
    _names.Add(@event.AccountId, @event.Username);
  }
 
  private void Apply(UniqueUserNameRemoved @event)
  {
    _names.Remove(@event.AccountId);
  }
}

To add an account you need to start an AccountCreationProcess. I see a process as an AggregateRoot together with a SAGA. The creation event of the process AggregateRoot will start the SAGA, which then coordinates the process. The following AggregateRoot is needed:

public class AccountCreationProcess: AggregateRoot
{
  public AccountCreationProcess(Guid id, string username, string password)
  {
      ApplyChange(new AccountCreationProcessInitiated(id, username, password));
  }
}

The AccountCreationSaga will do the following once the AccountCreationProcessInitiated is observed

public void Handle(AccountCreationProcessInitiated message)
{
  // excuse my terminology, i am not using a bus myself, but it should work something like this, i guess
  Bus.Publish(new AddAccount(message.id, message.password));
  Bus.Publish(new AddUniqueUserName(Guid.Empty, message.id, message.username));
}

- In case of a success of both commands you have a new Account and a new username added to UniqueUserName. An event handler for AccountAdded and UniqueUserNameAdded can denormalize the combination into a view holding the id of the Account, together with the username. Only if both events are handled will the complete account exist, it can be kept out of sight of the front-end until that has happened. If that's not good enough, include the reservation pattern to make sure the Account cannot be used until the Username has been added, for example.

The Sagas work is not yet done, though. It needs to react to the success or failure of the two commands it sent:

- If both commands succeed, the saga has done its work. It could send a command to the AccountCreationProcess to tell it that it was successful. - If AccountAdded was the first received event, and AddUniqueUsernameRejected was received, the saga sends a command to remove the account. A second command can be sent to the AccountCreationProcess to let it know that it failed. - On the other hand, if UniqueUsernameAdded was the first event, and AddAccountRejected was observed, the saga sends a command to remove the username, and possibly a failure command to the AccountCreationProcess.

The Saga needs no timeout. It needs to be sure that both commands were published and received. If neccessary, it will send them as often as neccessary until it knows for sure that they were both received (making the commands idempotent). Now the Saga can be certain that it will (eventually) observe either an acceptance or a rejection of the events, i.e. one of the 3 possible cases above will happen with certainty and the process will come to an end. If the Saga does timeout, then the Aggregates the Commands are sent to must be able to reserve the requested changes, and only once all reservations have been made, the Saga will send commands to confirm the reservations. The timeout will only be allowed to happen during the reservation process, not during the confirmation process, and the confirmation commands must be guaranteed to succeed - if either one of these constraints is not met, you could end up with an Account without a Username or vice versa, and again, you would have to prepare the domain for that :-(

- By the way, once the process succeeded, you do not need to use the UniqueUserName aggregate every time you access the account. Only once when creating it. If you want to authenticate a username and password, query a view of UniqueUsername using the username to obtain the AccountId, then retrieve the Account and do something like account.Authenticate(password).

1) oder auch Workflow Manager
2) eigene und wie man nachlesen kann auch von anderen
3) mein Problem ist, dass die Wahrscheinlichkeiten der Regelverletzungen - vor allem durch Eventual Consistency - , und somit die Kosten überhaupt nicht einschätzbar sind
4) In Englisch - weil es eine Antwort in der DDDCQRS Gruppe werden sollte
technology/domainmodel/processmanager.1357929047.txt.gz · Last modified: 2013/01/11 19:30 by rtavassoli