Search code examples
domain-driven-design

What are the real world, production grade cases for domain events (understood as events within same bounded context & process)?


If we have a bounded context with lets say 2 aggregates where aggregate1 publishes event1 and aggregate2 wants to react to it, we have 1 ways of doing it:

  1. in process raising event1 > aggregate2 reacting to it
  2. publish event1 to message bus and have some separate process pick it up & invoke aggregate2 method(s)

regardless of being within the same bounded context, if we want to make sure we don't lose event1 (application crashes between aggregate1 is saved, and aggregate2 is saved in reaction to event1, e.g.) i have a hard time finding examples of when would option 1 be better than option 2 (beyond maybe validation)?

i must be missing something but at this point of my knowledge, it seems like a pure theoretical concept to me without some real world value in terms of reliability and ability to maintain correct state.

of course that publishing a message and having separate process listen/react to it might seem like an overkill but is there any practical use of domain events that are not persisted somewhere (even within local DB which gets polled in which case i'd call that a primitive message bus)?

what am i missing?


Solution

  • What is a real world application of domain events within a bounded context and process?

    Requirements:

    • User can create categories.
    • Category names must be unique.
    • User can rename categories.

    (Category will have a number of other properties unrelated to naming).

    DDD Concepts:

    • A Category aggregate should be responsible for its own internal invariants, but cannot know the details about other Category aggregates.

    How are you going to ensure that the Category Name for the current Category is globally unique without the Category having access to all other categories?

    Answer: Domain Events

    DomainEvent

    public CategoryRenamed : DomainEvent
    {
        public Category Category { get; }
    
        internal CategoryRenamed(Category category)
        {
            this.Category = category;
        }
    }
    

    DomainEventHandler

    public CategoryRenamedHandler : IDomainEventHandler<CategoryRenamed>
    {
        public CategoryRenamedHandler(CategoryRenamed domainEvent)
        {
            string proposedName = domainEvent.Category.Name;
    
            // query database to ensure that proposedName is not already in use
            if (inUse)
                throw new Exception($"Name {proposedName} already in use." ;
        }
    }
    

    Entity

    public abstract class Entity
    {
        List<DomainEvent> _domainEvents = new List<DomainEvent>();
    
        protected AddDomainEvent(DomainEvent domainEvent)
        {
            _domainEvents.Add(domainEvent);
        }
    
        public List<DomainEvent> DomainEvents => _domainEvents;
    }
    

    Category

    public class Category : Entity
    {
        public Guid Id { get; private set; }
        public string Name { get; private set; }
    
        public Category(Guid id, string name)
        {
            Id = id;
            SetName(name);
        }
    
        public Rename(string name)
        {
            SetName(name);
        }
    
        void SetName(string name)
        {
            // Local Invariants
            if (string.IsNullOrWhitespace(name))
                throw new Exception("Invalid name");
    
            Name = name;
    
            // Add a domain event for the infrastructure to process
            AddDomainEvent(new CategoryRenamed(this));
        }
    }
    

    Command

    public class AddCategoryCommand
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
    }
    

    CommandHandler

    public class CommandHandler : ICommandHandler<AddCategoryCommand>
    {
        readonly ICategoryRepository _categoryRepository;
    
        public CommandHandler(ICategoryRepository categoryRepository)
        {
            _categoryRepository = categoryRepository;
        }
    
        public void HandleCommand(AndCategoryCommand command)
        {
            Category newCategory = new(command.Id, command.Name);
    
            // Check for domain events before committing to repository
            DomainEventDispatcher.DispatchEvents(newCategory.DomainEvents);
            // Dispatcher will find the CategoryRenamed event and send 'in-process'
            // to CategoryRenamedHandler
            // If name was is in use an error will be thrown by the handler (see above)
    
            _categoryRepository.Add(newCategory);
        }
    }
    

    Outcome

    Your Category aggregate has enforced its own local invariants and the domain command and domainevent handling infrastructure has been leveraged to ensure uniqueness of name across all categories.