jnm2
jnm2

Reputation: 8364

Do you apply events to the domain model immediately when there is still the possibility of useless events or undo?

Every example of event sourcing that I see is for the web. It seems to click especially well with the MVC architecture where views on the client side aren't running domain code and interactivity is limited. I'm not entirely sure how to extrapolate to a rich desktop application, where a user might be editing a list or performing some other long running task.

The domain model is persistence-agnostic and presentation-agnostic and is only able to be mutated by applying domain events to an aggregate root. My specific question is should the presentation code mutate the domain model while the user makes uncommitted changes?

  1. If the presentation code does not mutate the domain model, how do you enforce domain logic? It would be nice to have instant domain validation and domain calculations to bubble up to the presentation model as the user edits. Otherwise you have to duplicate non-trivial logic in the view model.

  2. If the presentation code does mutate the domain model, how do you implement undo? There's no domain undelete event since the concept of undo only exists in an uncommited editing session, and I'd be loathe to add an undo version of every event. Even worse, I need the ability to undo events out-of-order. Do you just remove the event and replay everything on each undo? (An undo also happens if a text field returns to its previous state, for example.)

  3. If the presentation code does mutate the domain model, is it better to persist every event the user performs or just condense the user's activity to the simplest set of events possible? For a simple example, imagine changing a comments field over and over before saving. Would you really persist each intermediate CommentChangedEvent on the same field during the same editing session? Or for a more complicated example, the user will be changing parameters, running an optimization calculation, adjusting parameters, rerunning the calculation, etc until this user is satisfied with the most recent result and commits the changes. I don't think anyone would consider all the intermediate events worth storing. How would you keep this condensed?

There is complicated collaborative domain logic, which made me think DDD/ES was the way to go. I need a picture of how rich client view models and domain models interact and I'm hoping for simplicity and elegance.

Upvotes: 4

Views: 665

Answers (2)

guillaume31
guillaume31

Reputation: 14080

I don't see desktop DDD applications as much different from MVC apps, you can basically have the same layers except they're mostly not network separated.

CQRS/ES applications work best with a task-based UI where you issue commands that reflect the user's intent. But by task we don't mean each action the user can take on the screen, it has to have a meaning and purpose in the domain. As you rightly point out in 3., no need to model each micro modification as a full fledged DDD command and the associated event. It could pollute your event stream.

So you would basically have two levels :

UI level action

These can be managed in the presentation layer entirely. They stack up to eventually be mapped to a single command, but you can undo them individually quite easily. Nothing prevents you from modelling them as micro-events that encapsulate closures for do and undo for instance. I've never seen "cherrypickable" undos in any UI, nor do I really see the point, but this should be feasible and user comprehensible as long as the actions are commutative (their effect is not dependent on the order of execution).

Domain level task

Coarser-grained activity represented by a command and a corresponding event. If you need to undo these, I would rather append a new rollback event to the event stream than try to remove an existing one ("don't change the past").

Reflecting domain invariants and calculations in the UI

This is where you really have to get the distinction between the two types of tasks right, because UI actions will typically not update anything on the screen apart from a few basic validations (required fields, string and number formats, etc.) Issuing commands, on the other hand, will result in a refreshed view of the model, but you may have to materialize the action by some confirmation button.

If your UIs are mostly about displaying calculated numbers and projections to the user, this can be a problem. You could put the calculations in a separate service called by the UI, then issue a modification command with all the updated calculated values when the user saves. Or, you could just submit a command with the one parameter you change and have the domain call the same calculation service. Both are actually closer to CRUD and will probably lead to an anemic domain model though IMO.

Upvotes: 1

Mathieson
Mathieson

Reputation: 1948

I've wound up doing something like this, though having the repository manage transactions.

Basically my repositories all implement

public interface IEntityRepository<TEntityType, TEventType>{
    TEntityType ApplyEvents(IEnumerable<TEventType> events);
    Task Commit();
    Task Cancel();
}

So while ApplyEvents will update and return the entity, I also keep the start version internally, until Commit is called. If Cancel is called, I just swap them back, discarding the events.

A very nice feature of this is I only push events to the web, DB, etc once the transaction is complete. The implementation of the repository will have a dependency on the database or web service services - but all the calling code needs to know is to Commit or Cancel.

EDIT To do the cancel you store the old entity, updated entity, and events since in a memory structure. Something like

 public class EntityTransaction<TEntityType, TEventType>
{ 
    public TEntityType oldVersion{get;set;}
    public TEntityType newVersion{get;set;}
    public List<TEventType> events{get;set;}
}

Then your ApplyEvents would look like, for a user

private Dictionary<Guid, EntityTransaction<IUser, IUserEvents>> transactions;

public IUser ApplyEvents(IEnumerable<IUserEvent> events)
{
    //get the id somehow
    var id = GetUserID(events);
    if(transactions.ContainsKey(id) == false){
        var user = GetByID(id);
        transactions.Add(id, new EntityTransaction{
             oldVersion = user;
             newVersion = user;
             events = new List<IUserEvent>()
        });
    }

    var transaction = transactions[id];
    foreach(var ev in events){
        transaction.newVersion.When(ev);
        transaction.events.Add(ev);
    }
}

Then in your cancel, you simply substitute the old version for the new if you're cancelling a transaction.

Make sense?

Upvotes: 0

Related Questions