Aggregates are the heart of your system. They hold domain logic and are responsible for emitting events that will eventually make your data consistent across multiples data projections and bounded contexts. Because aggregates are so important, it is crucial to keep them small and easy to understand, but how to this when the domain gets bigger and bigger? In this article, I will show you a way to reduce complexity from your aggregates and make them scale better. Write-side domain overview Before getting started, let review how commands are executed and turned into events. After receiving the command sent by the user, the command handler invokes the repository to load the aggregate state in memory. Then, the command is executed on the aggregate which emits either domain events or failures. Finally, those events or failures are persisted using the repository. e.g. [👤 Coder] -> [✉️ Comment issue command] -> [📫 Comment issue handler] -> [🍇 Issue aggregate] -> [⚡️ Issue commented event] First attempt In the first place, I will show you briefly how the write-side domain was implemented in a real-world project a worked on. Then, we will discuss the scaling issues of this solution and how it can be improved to keep your aggregates clean. Here is how it looks: : { { IssueId = issueId; Message = message; } Guid IssueId { ; } Message { ; } } public class CommentIssue ICommand ( ) public CommentIssue Guid issueId, message string public get public string get : < > { IRepository<Issue> _repository; => _repository = repository; { issue = _repository.Find(command.IssueId); issue.Comment(command.message); _repository.Save(issue); } } public class CommentIssueHandler ICommandHandler CommentIssue private readonly ( ) public CommentIssueHandler IRepository<Issue> repository ( ) public void Handle CommentIssue command // 1. Loads the aggregate in memory. var // 2. Invokes the aggregate method. // 3. Saves the aggregate. : < , > { ISet<IssueComment> _comments = HashSet<IssueComment>(); { } IEnumerable<IssueComment> Comments => _comments; { ( .IsNullOrWhiteSpace(message)) ; Emit( IssueCommented( IssueComment(message))); } { (@ ) { IssueCommented e: Apply(e); ; } } => _comments.Add( IssueComment(@ .CommentId, @ .Message)); } public class Issue AggregateRoot Issue Guid private readonly new ( ) : ( ) public Issue Guid id base id public ( ) public void Comment message string if string return // `Emit()` internally invokes `Apply()` to avoid code duplication. new new // All other command related methods go here e.g. // `public void Edit() { }` // `public void Close() { }` // `public void Unsubscribe() { }` ( ) protected override void Apply IDomainEvent @ event // This could be done using reflexion, but be aware of performance issues. switch event case break ( ) private void Apply IssueCommented @ event new event event : { { CommentId = comment.Id; Message = comment.Message; } Guid CommentId { ; } Message { ; } } public class IssueCommented IDomainEvent ( ) public IssueCommented Comment comment public get public string get As you can see, using this technique, the aggregate grows for every command we add. Because of this, it is easy to imagine how fast this newly created aggregate will turn into a hard to maintain mess as our domain grows. Also, all command handlers will do the exact same things i.e.: Loads the aggregate in memory. Invokes the aggregate method. Saves the aggregate. Let see how we can fix this! Refactored solution The trick is to let the commands and events execute/apply themselves! : < > { { IssueId = issueId; Message = message; } Guid IssueId { ; } Message { ; } IEnumerable<IDomainEvent<Issue>> ExecuteOn(Issue aggregate) { ( .IsNullOrWhiteSpace(Message)) ; ; } } public class CommentIssue ICommand Issue ( ) public CommentIssue Guid issueId, message string public get public string get // Commands now know how to execute themselves. public if string yield break return new ( )) yield IssueCommented new IssueComment(IssueId, Message : < > { { CommentId = comment.Id; Message = comment.Message; } Guid CommentId { ; } Message { ; } => aggregate.Comments.Add( IssueComment(CommentId, Message)); } public class IssueCommented IDomainEvent Issue ( ) public IssueCommented IssueComment comment public get public string get // Events now know how to apply themselves. ( ) public void ApplyTo Issue aggregate new By doing this, we are able to implement a new method in the aggregate base class which will execute a command and apply the returned events. Execute() Also, it makes it possible to generalize the method and move it up to the aggregate base class. Apply() < , > : < , >, < > : < , > { List<IDomainEvent<TSelf>> _uncommitedEvents = List<IDomainEvent<TSelf>>(); => @ .ApplyTo((TSelf) ); { events = command.ExecuteOn((TSelf) ); _uncommitedEvents.AddRange(events); ( @ events) Apply(@ ); } } public abstract class AggregateRoot TSelf TId Entity TSelf TId IAggregateRoot TId where TSelf AggregateRoot TSelf TId private readonly new // ... ( ) public void Apply IDomainEvent<TSelf> @ event event this ( ) public void Execute ICommand<TSelf> command var this foreach var event in event Now that we added the method we can invoke it right away from the command handler. Execute() Also, you can see below, our command handler is now named instead of . Why is that? This is because we simplified enough that all command handlers will be exactly the same so we can use the same command handle for all commands. IssueCommandsHandler CommentIssueHandler : < , > { IRepository<Issue, Guid> _repository; => _repository = repository; { issue = _repository.Find(command.IssueId); issue.Execute(command); _repository.Save(issue); } } public class IssueCommandsHandler ICommandHandler CommentIssue Issue private readonly ( ) public IssueCommandsHandler IRepository<Issue, Guid> repository ( ) public void Handle CommentIssue command var // The command can now be executed direcly on the aggregate. Our objective was to clean up our aggregate and as you can see below there is nothing left in it so I think we made it! : < , > { { } ISet<IssueComment> Comments { ; } = HashSet<IssueComment>(); } public class Issue AggregateRoot Issue Guid ( ) : ( ) public Issue Guid id base id public get new // Only common validations/business rules go here! Note that I exposed through the interface for simplicity purposes. In a real-world project, consider adding a custom collection class such as to hold custom business rules. Comments.Add() ISet IssueComments The source code can be found . here Resources by Nick Chamberlain Implementing an Event Sourced Aggregate by Nick Chamberlain Command Handlers Previously published at https://dev.to/maximegel/cqrs-scalable-aggregates-731