Confused about commands, domain events and external events in event sourcing

aggregatedomain-driven-designevent-sourcingmicroservices

I'm a little confused about

  • commands role in event sourcing
  • distinction between domain and external events

If my understanding is right

  • a command represents an action initiated by an actor in terms of the domain
  • a domain event is an event can be consumed and produced by aggregate roots
  • an external event is just like a DTO – a data contract and it needs to be translated to either a domain event or a command

Example

I have a Product aggregate root. A product can have multiple active special offers. In order to manage it's SpecialOffers, the product accepts 2 domain events:

  • SpecialOfferActivated
  • SpecialOfferDeactivated

So it's public interface is just 2 overloaded Apply methods:

class Product{
    ...
    Apply(SpecialOfferActivated){...}
    Apply(SpecialOfferDeactivated){...}
}

1st case: The request comes form front-end to the api

So a Controller is first in the line. It basically translates caller intention from data contract (DTO) to the domain language (command):

class ProductController{
    Post(SpecialOfferDto dto){
        ActivateSpecialOfferCommand command = Map(dto)
        _commandBus.Send(command)
    }
}

Command sent, now we need a command handler

class ActivateSpecialOfferCommandHandler{
    Handle(ActivateSpecialOfferCommand command){
        SpecialOfferActivated domainEvent = Map(command)
        _eventBus.Publish(domainEvent)
    }
}

Event published, now time for the event handler

class SpecialOfferActivatedDomainEventHandler{
    Handle(SpecialOfferActivated domainEvent){
        var product = GetFromDatabase()
        product.Apply(domainEvent)
        Save(product)
    }
}

Done.

2nd case: The process is initiated by an external event published to the service bus.

This time NewPromotionExternalEvent is the data contract (ExternalEvent) and again we need to translate it to the domain language (Command)

class NewPromotionExternalEventHandler{
    Handle(NewPromotionExternalEvent extenalEvent){
        ActivateSpecialOfferCommand command = Map(extenalEvent)
        _commandBus.Send(command)
    }
}

And then it falls back to the ActivateSpecialOfferCommandHandler from the fist case. So it's the same case as the first one basically.

3rd case: Skip the domain events layer (variation of either the 1st or the 2nd case)

So either by an api or an external event a command was produced. We simply create a domain event in order to apply it to the aggregate root. We do not publish the event to the service bus.

class ActivateSpecialOfferCommandHandler{
    Handle(ActivateSpecialOfferCommand command){
        SpecialOfferActivated domainEvent = Map(command)

        var product = GetFromDatabase()
        product.Apply(domainEvent )
        Save(product)
    }
}

Done.

4th case: Skip the commands layer (variation of the 1st case)

We can easily skip the commands layer

class ProductController{
    Post(SpecialOfferDto dto){
        SpecialOfferActivated domainEvent = Map(dto)
        _eventBus.Publish(domainEvent)
    }
}

and fallback to the SpecialOfferActivatedDomainEventHandler

5th case: Aggregate root creation.

So either by an api or an external event a command CreateNewProductCommand was produced. And we need another handler:

CreateNewProductCommandHandler{
    Handle(CreateNewProductCommand command){
        var product = Map(command)
        SaveToDatabase(product)

        NewProductCreated domainEvent = Map(product)
        _eventBus.Publish(domainEvent) // in case somebody is interested
    }
}

In this case there's really no place to stick the domain events layer.

6th case: Domain event produced by Product (aggregate root)

class Product{
    Apply(SpecialOfferActivated domainEvent){
        var specialOffer = Map(domainEvent)
        _specialOffers.Add(specialOffer)
        if(...){
            // For simplicity sake, assume the aggregate root can access _eventBus
            _eventBus.Publish(new ProductReceivedTooManySpromotionsDomainEvent(this.Id))
        }
    }
}

Questions

  1. The events layer is cool, it allows us to distribute jobs across multiple instances or other microservices. However, what's the point of the command layer? I could easily produce domain events right away (in a controller or external an event handler – 4th case).
  2. Is 3rd case legit (create a domain event just to apply it to the aggregate root, without publishing it)?
  3. Does command layer only make sense in 5th case where it gives us the benefit of delegating product creation to another microservice while domain events layer is not applicable?
  4. Where is the line between external and domain events? Is NewPromotionExternalEvent from the 2nd really an external event or is it rather a domain event?
  5. Who can produce domain events? Aggregate root? Command handler? Domain event handler? External event handler? Another microservice? All of them?
  6. Can domain events be dispatched to another micro-service or would it become an external event then?
  7. What is the proper way of handling product creation and special offer activation when it the request comes form controller or external event?

Best Answer

I'm a little confused about

That's not your fault. The literature sucks.

What people are really talking about is the way that information flows around our "domain model".

  • Commands carry new information toward our domain model
  • "Domain Events" describe changes to the state of the model
  • "External events" carry information away from the model.

The first and last of these are really about messaging.

But the spelling "domain events" is kind of a mess - they are both floor wax and dessert topping. Within the context of "event sourcing", they are really about persistence. However, the phrase "domain events" are also used to describe messages used to share information between aggregates in the same model.

what's the point of the command layer?

Mostly, it gives you a way to decouple application concerns (ensuring the message is delivered to the correct aggregate, dealing with transactions), from controller concerns. It also gives you an easy way to apply certain cross cutting concerns (timing, for example) before the domain model goes to work.

create a domain event just to apply it to the aggregate root, without publishing it

Yes, but I would spell it somewhat differently - we don't normally create events outside of the aggregate; the aggregate owns the business logic that determines how it changes state (as opposed to being a dumb bag of data). But it is perfectly normal for an aggregate to change state without notifying the world that it happened.

Where is the line between external and domain events?

"External events" are usually a lot thinner than domain events - we don't usually want to be sharing the private information of a service everywhere, in much the same way that we don't normally make all of our internal implementation details public. Also, external events are part of the contract between services - the strategies for changing that contract need to satisfy more stake holders. But data maintained within a single service is less expensive to change.

Who can produce domain events?

Aggregate root. In theory, also other entities contained within the same aggregate.

What is the proper way of handling product creation and special offer activation when it the request comes form controller or external event?

Information coming in -> you load the appropriate part of the domain model into memory, and pass the information to it so that the changes can be calculated.

If Aggregate root is the only one producing domain events, we've got a problem. The only way interact with an aggregate (and let it produce its own domain events) is by applying a domain event to it.

Normally, the way you interact with an aggregate root is by transmitting information to it via a command.

For instance, take a look at the DDD sample app. In particular, pay attention to how little business logic is done by the application layer vs the domain model (here and here).

Also you mentioned that domain events are useful to talk to other aggregates within the same domain model. I would say, a domain model is not restricted to a single microservice.

Historically, domain events were being used to talk to other aggregates participating in the same transaction. It follows that if they are participating in the same transaction, they are part of the same microservice.

Sharing information across transaction boundaries has different design considerations from sharing information within a transaction.

I think these days you are a lot more likely to hear that people are aligning their microservice boundaries with their bounded contexts, which means that the models are not shared (which should make sense -- part of the point of microservices is that they should be able to evolve independently).

Related Topic