Say we have an aggregate called Appointment. This appointment has a start time and a duration. Our requirements state that Appointments must not overlap other Appointments in time. (There are also other requirements, such as blackout dates, holidays, etc). Before an Appointment can be booked, we must confirm it doesn’t overlap other Appointments, or violate any of the other booking constraints.
From an Axon Framework perspective, how do you model this?
The more general question is "When BL for an Aggregate involves other instances of that same Aggregate (or other Aggregates), how do you model this?
Essentially, the solution falls back to having a small command side projection (query model of your aggregate/lookup table) that is immediately consistent with you Aggregate entity (command model of your aggregate). You would like to use this projection in your command model (Aggregate entity) to validate your commands and make further decisions. I hope the blog post will clarify it further.
A followup question. What are best practices for preventing the size of an Aggregate class from growing to a very large number of lines? Do people externalize the business logic BL to external classes to help keep the sizes down?
For example, I like the ideas from Functional Programming where a “Use Case” contains the “whats” of what needs to happen to satisfy that use case, but the “hows” are contained elsewhere.
Example:
@CommandHandler
fun handle(
command: RegisterNewUserCommand,
userService: UserService,
emailService: Email Service
) {
userService.validateNewUser(command.userData)
.onSuccess {
// create user registered event
}
.andThen { emailService.emailNewUser(command.userData.email) }
.andThen { /* other steps to satisfy use case */ }
}
In the above when creating a new user, we must email the new user, among other steps required to satisfy the use case.
Is this the correct place to put this business logic?
What is the best practice for organizing business logic to help prevent aggregate classes from exploding in size?
This aggregate User can have a command handler method that will handle RegisterNewUserCommand. You want to do essential business validation as the first step in this command handler method, and publish/apply a NewUserRegisteredEvent as a success, or NewUserNotRegisteredEvent as an error (you could throw an exception in this case as well).
You don’t want to call third-party services from your aggregate command handler. It will introduce runtime dependencies.
You can have a query side component (event handler) that will subscribe to these events in async way and use an Email Service to send emails to users. This approach is utilizing CQRS better and can scale better. It will keep your command model focused and smaller.
Once the email service finishes sending the email, you can send the second command (MarkNewUserEmailedCommand) back to your User aggregate so it can update its state.
I’d like to run some thoughts by you on this. What you described is a fully event-driven system. I think a consequence of this design is that the steps required to satisfy various use cases becomes “hidden”.
It takes time for a developer to follow the codepath from event to event to get the full picture to see all the steps required to satisfy any given use case. You can’t rely on IntelliJ call hierarchy code paths because of the decoupled nature of event systems.
Thus it becomes harder for an engineer to say that the code successfully satisfies all the steps required to fulfill any given use case because that code is distributed throughout the codebase.
As an alternative, we’ve formalized the notion of a UseCase (or Workflow). It’s essentially similar to Function<T,R> but the apply() method is reactive. The UseCase coordinates the all steps required to satisfy the use case. Using reactive APIs (Project Reactor) and Kotlin coroutines, we can ensure none of the code is blocking, which gives a similar benefit to the fully event driven system, but it also brings all the code into a single location so that engineers can see in one place all the steps required to satisfy that use case.
Each step in that use case is a function that follows the Single Responsibility Principle. So, it’s a bit like assembling legos. You don’t have to worry about side effects because the “step functions” only do what the function says it does, nothing else.
With Axon, the plan is combine the approach you outlined in your last response with the UseCase pattern to try and get the best of the event-driven system, while also making the code required to fulfill a use case explicit.