3

We are developing some context using CQRS approach. We have ended up with command handlers emitting events. It seems to be not a good idea to us. However we can't find any alternative approach. We struggle with two particular group of scenarios:

  1. Creating new aggregate.

Creating an aggregate may result with success of fail. In case of success all is straightforward - aggregate keeps event AggregateCreated (build within its constructor). But in case of fail, aggregate can not emit/publih anything because it doesn't even exist. In such case our command handler emtis CreatingAggregateFailed which we percieve as domain leak.

  1. Resource not found

Second scenario regard to not founding particular resource. For example, removing non-existing resource. In our implementation Repository::find() raise NotFound exception. The excetpinon in turn gets catched in handler which emits AggregateNotFound event.

Based on those events we build process managers and responses. But it looks bit awkward that domain (application?) events are emitted outside the aggregates. Although those cumbersome events apply to non-existence of such aggregate.

Let me consider the simplest scenario. I dispatch command AddTeamMember($teamGuid, $userGuid, $role). If team, user and role exist but command violates any of aggregate invariant aggregate may register MemberRejected event - as far all is good. But neither user, team or given role may dont even exist. So there is no aggregate able to register an event. I need feedback about this failure to take an appropriate actions (either in process manager or to just inform my command issuer). I consider MembershipRequest aggregate as command reciever. Then I have always valid aggregate to publish events and the events are meaningful within the agregate. But this introduces additional compelxity. I need intermediate aggregate to handle "resource not found" for each possible command.

I have come up with new idea. I will ilustrate this with code samples.

Old handler version

/**
 * @param CreateChannel $command
 */
protected function handleCreate(CreateChannel $command): void
{
    try {
        $channel = new Channel($command->getGuid(), $command->getSymbol(), $command->getLangCodes());
        $this->channelRepository->save($channel);
    } catch (InvalidData $e) {
        $this->eventBus->publish($channel, new ChannelCreationFailed($command->getGuid()));
    }
}

New idea

/**
 * @param CreateChannel $command
 */
protected function handleCreate(CreateChannel $command): void
{
    try {
        $channel = new Channel($command->getGuid());
        $channel->create($command->getSymbol(), $command->getLangCodes()))
        $this->channelRepository->save($channel);
    } catch (InvalidChannelData $e) {
        // ?
    } finally {
        $this->eventBus->publish($channel, new ChannelCreationFailed($command->getGuid()));
    }
}

But this has some obvious drawbacks. First of all, service layer affects aggregate design. It also reinventing creating object language concept. It enforces to check within any other method if aggregte is in "created" state. Maybe it is good point to differentiate between object creation and aggregate creation? This approach assumes no exceptions raised from constructor. Constructor always would get only valid (guranteed by command) guid and nothing else.

Always having aggregate with guid solves also not-existing referenced aggregates. Using double dispatch it is possible to raise "not found" exceptions from source agregate

$team->addMember($userGuid, $usersRepository);

But this makse aggregate's api ugly compared to

$team->addMember($user);
  • Just to be sure, because you didn't add the tag - you don't have Event Sourcing as a persistence, but only use domain events for cross-aggregate processes, right? – guillaume31 Aug 31 '18 at 12:52
  • @guillaume31 yes, this looks exactly as you said – ayeo Aug 31 '18 at 13:24
1

Don't publish domain events that your domain experts wouldn't care about.

If a specific failure case is an identified part of a business process (typically something that would come up during an Event Storming session with the business people), find a term for it in the Ubiquitous Language and definitely publish an event for it.

But in non-nominal cases such as network interruptions, system outages, configuration mistakes and the like, I wouldn't go through the regular pub-sub cycle. In the case of a command prompted by a user through a UI, notify them that an error occurred. If the command was executed by a process manager, it will know that something went wrong and maybe

  • retry
  • execute a compensation command
  • or notify the admins

depending on the situation. If a compensation command is sent and a new event emitted as a result, correlation ids can help you retrace which original event or command triggered the compensation and with the help of logs, diagnose and fix the problem.

  • You've put lot of important insights in simple words. I fully realize that from bussiness/domain point of view such events are worhtless. The domain state reamians unchanged. Even more those events mean that something has not happened. I percieve those exactly as way of notifing user or process manager. I think they should inhabit in upper (application not domain layer). So in fact my problem is about how to notify the failure? – ayeo Aug 31 '18 at 16:28
  • "Notify" in my answer refers to notifying a human being, not a system. If you're talking about the specific case where the process manager and command handler communicate asynchronously (which is not always the case) and an error happens in the latter, you could use the same channel of communication as for events, except it's not domain events. Or you could log the error and wait for the situation to be solved manually. It's a question of tradeoffs - which failures require compensation and which ones can just leave the domain in its current state and wait to be fixed? – guillaume31 Sep 1 '18 at 10:13
2

In event sourcing, we normally only record events if the state of the model changes. So a "WeDidn'tChangeTheStateOfTheModel" event doesn't make a lot of sense.

However, there is a cross cutting concern called telemetry that does care very much about requests handled, failures, and so on. But that's a different domain, with completely different cost tradeoffs

(Example: losing an order for a customer is bad, so the cost to the business of losing the domain event is high. But if the count of telemetry events telling us that an order was successfully processed doesn't exactly match the count of corresponding domain events, it probably doesn't cost the business much).

The business typically cares a lot more about a durable history of orders than it does the web log.

Yes, I would be expecting the command handler to be emitting telemetry events, but not domain events.

  • I agree with all of that. But I dont found my case apply under term of "telmetry". I am looking for way to get feedback about command processing fail in asynchronous environment. How I can get an event if there is not aggregate to publish it? I have added example in my main post. I consider if there maybe should by some kind of MembershipRequest aggregate :) – ayeo Aug 30 '18 at 21:11
1

For the purposes of your question, lets distinguish between two types of failure, Domain and Technical.

Let’s say a student registers for a course but some backend service fails. In this case the StudentCreated(), StudentDebited() and ClassRoomSeatReserved() events should have been persisted to the event store so that when the failing service is back online, the events could be rerun.

The TECHNICAL exception should have been logged and picked up in by theDevOps and contained some reference to the event batch that failed. This is Eventual Consistency with a possible Saga.

When your service receives the command you need to do validation on the command and send back any DOMAIN errors to the caller and optionally create domain events if it is INTERESTING to the domain.

The 2 processes are separate and serves different purposes.

As to the involvement of the Aggregate in this, for example the StudentAggegate(), it only gets involved after the events have been persisted and then sourced.

The Aggregate is not a record in the database, it is a class that governs al the behaviour of its siblings within the bounded context.

Let me explain with the factory pattern and a fluent interface, something like this:

If(!StudentRegisterCommand.IsModelValid())

// optionally create domain events.
return ModelValidationErrors() 

eventlist=Factory.CreateStudent(StudentRegisterCommand).Debit(StudentRegisterCommand.Cource.Cost, StudentRegisterCommand.Card) .ReserveSeat(StudentRegisterCommand.Cource).Events()

Repository.PersistEvents(eventlist)

Bus.AddEvents(eventlist)

Only now do my RegistrarService receive the event to create my new StudentAggegate. If the debit fails, the seat reservation will reversed and the FinanceService will:

Student = StudentService.GetStudent()

eventlist= Factory.NotifyStudent(Student.Name).Message(“Please contact us as your card was declined.”).ByMail(Student.email).SMS(Student.Phone).UnReserveSeat(Student, Student.Cource)

Repository.PersistEvents(eventlist)

Bus.AddEvents(eventlist)

The Aggregate is both a class and

Hope this helps.

  • All you said is really valuable. My real problem is how to get feedback about such failure (for saga or gui) if there is no aggregate able to publish an event? I have added example in my main post. – ayeo Aug 30 '18 at 21:08
  • Hi @ayeo, the Aggregate is not a record in the database, it is a class that governs all the behavior of its siblings within the bounded context. Also, you seem to not differentiate between commands and events in your example. A command can result in many events and only the events are persisted. Remember that it should be possible to delete all your Domain data and re-run the events to recreate your domain data. I have updated my post. – Marius Aug 31 '18 at 4:42
  • I like your statement that aggregate is not a database record. Howerer it does not change the fact that such aggregate may or may not exist. There is no efficient way to determine that in advance (I am not satisfied with command pre validation). My problem focus on CQRS itself there is no ES involved. I still dont now how to get feedback of such failure from command handler. – ayeo Aug 31 '18 at 8:07
  • Forget about the aggregate for a minute. You "command handler" will throw an exception that you must write to a database or another persistent store (NOT your Domain event queue). From there you must make a plan to notify someone. @VoiceOfUnreason put it very well; if your domain state did not change, another channel needs to be followed for telemetry. – Marius Aug 31 '18 at 10:23
  • I am not sure if I get the point. Do you suggest to use two distinct and seperate buses? The domain event bus and the other on application level? – ayeo Aug 31 '18 at 13:37
0

It seems to me that the problem caused by mixing Object Orientated and Service approaches.

If you take an OO approach and want the event to be emitted by your object, then the object should encapsulate the Command and Query objects.

  • Object.Save() / ObjectCollection.New() should create the Command and throw the Event if the command fails

  • Object.Get() / ObjectCollection.Find() should create the query and throw the not found event if it fails.

If you take a Service approach then the events belong to the service or repository and are not part of the aggregate at all.

I dont think its a problem for these objects to create orphan Domain Events as they will need to reference the Domain anyway. Doing so doesn't cause any coupling between different Domains/Aggregates

If you want to stick with the OO approach, then I would inject the Repository into ObjectCollection. Have the Repository encapsulate the CQRS stuff and throw its own errors which are converted to Events inside ObjectCollection.

ObjectCollection itself can always exist as an empty collection.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.