3

We are developing some context using CQRS approach. We ended up with command handlers emitting events. It seems to be a poor idea for 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 in either success or failure. In the case of success, it is straightforward - the aggregate keeps event AggregateCreated (built within its constructor). But in the case of failure, the aggregate cannot emit/publish anything because it doesn't exist. In this case our command handler emits CreatingAggregateFailed which we percieve as a domain leak.

  1. Resource not found

The second scenario is concerned with the inability to find a particular resource. For example, we may want to remove non-existing resources. In our implementation, Repository::find() throws the NotFound exception. The exception gets caught in the handler, which emits the AggregateNotFound event.

Based on those events, we build the relevant process managers and responses. But it seems awkward that the domain (or application) events are emitted from outside the aggregates. However, these events also apply to non-existing instances of such aggregate.

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

I have come up with a new idea. I will illustrate this with code.

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 obvious drawbacks. First of all, service layer affects aggregate design. It is also reinventing object language concepts. It forces you to check within any other method if aggregte is in "created" state. Perhaps it is important to differentiate between object creation and aggregate creation? This approach assumes no exceptions are raised from the constructor. The constructor would always only get valid (guranteed by command) guidance, and nothing else.

Always having aggregate with guidance also solves the issue with non-existing referenced aggregates. Using double dispatch, it is possible to raise the "not found" exceptions from source aggregate.

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

But this makes the aggregate's api ugly compared to

$team->addMember($user);
2
  • 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? Aug 31, 2018 at 12:52
  • @guillaume31 yes, this looks exactly as you said
    – ayeo
    Aug 31, 2018 at 13:24

4 Answers 4

3

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.

2
  • 1
    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, 2018 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? Sep 1, 2018 at 10:13
4

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.

1
  • 1
    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, 2018 at 21:11
1

For the purposes of your question, let's 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 StudentAggregate(), 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 StudentAggregate. 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)

Hope this helps.

5
  • 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, 2018 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, 2018 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, 2018 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, 2018 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, 2018 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 and acknowledge that you have read and understand our privacy policy and code of conduct.

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