Reputation: 2539
Following a functional programming paradigm, I have a CQRS architecture with event sourcing as the main persistence mechanism.
Currently my aggregates consist of
A command handler does
Example of a command handler
type CommandHandler = (
state: AggregateState,
command: Command
) => E.Either<Err.Err, DomainEvent[] | void>;
Basically steps 1, 2 and 4 are abstracted away in a generic function:
// pseudo-code
const wrapCommandHanler = (handler: CommandHandler) => {
return wrapped = (command: Command) => {
const events = fetchEvents();
const state = applyReducer(events);
const newEvents = handler(state, command);
persistEvents(newEvents);
}
}
So my command handlers are quite lean and focused and only contain the business logic.
I read about DDD, but the given examples were following a OOP paradigm. In these examples the command handler would call an aggregate method where the aggregate is a class that contains state and domain logic.
But in my case the aggregate state and behavior is separated and my command handlers ARE the aggregate behavior. So my command handlers contain the domain logic.
My question(s): Is this approach "correct" / valid DDD or am I shooting myself in the foot with this? If not, what is the main purpose of separating an aggregate function and a command handler?
Upvotes: 2
Views: 3049
Reputation: 20551
When doing pure functional DDD, the commands (I'm deliberately not using "object") correspond to the methods of an aggregate (if using types, you can say that the type corresponds to the interface and each instance to an invocation; the ultimate handler function corresponds to the body of the method).
Technically, if event sourcing, it's the pas de deux of the command and event handler which define the aggregate, though the command handler probably carries more of the load.
These two definitions of an aggregate in Scala are effectively the same thing. For the OO-style, I'm using a more "durable state" approach and the FP-style is event-sourced (OO-style event-sourced (aggregate methods return a Seq[Event]
and you have some means of defining event handlers) and FP-style durable-state (no EventHandler
and the command handler returns a State
) are both possible, but IME feel unnatural). Both are equivalently unit-testable (event-sourced arguably moreso, especially for property-based testing):
// Note that Map here is an immutable Map (i.e. a value object)
// Domain has been simplified: assume that Item includes price and there are no discounts etc.
// OO and "durable state"-style persistence... application basically loads a cart from persistence, maps external commands into method calls, saves cart
class ShoppingCart(val itemCounts: Map[Item, Int], val checkedOut: Boolean = false) {
def addItem(item: Item, qty: Int): Unit =
// Collapsing the failed validations into a single do-nothing case
if (!checkedOut && qty > 0) {
itemCounts.get(item) match {
case Some(count) =>
itemCounts = itemCounts.updated(item, count + qty)
case None =>
itemCounts = itemCounts + (item -> qty)
}
}
def adjustQtyOfItem(item: Item, newQty: Int): Unit =
if (!checkedOut && itemCounts.contains(item)) {
newQty match {
case neg if neg < 0 =>
// do nothing
()
case pos if pos > 0 =>
itemCounts = itemCounts.updated(item, newQty)
case 0 =>
itemCounts = itemCounts - item
}
}
def removeAllOfItem(item: Item): Unit =
adjustQtyOfItem(item, 0)
def checkOut(): Unit =
if (!checkedOut) {
checkedOut = true
}
}
// FP and event-sourced persistence
object ShoppingCart {
case class State(itemCounts: Map[Item, Int], checkedOut: Boolean)
sealed trait Command
case class AddItem(item: Item, qty: Int) extends Command
case class AdjustQtyOfItem(item: Item, newQty: Int) extends Command
case object CheckOut extends Command
val RemoveAllOfItem: Item => Command = AdjustQtyOfItem(_, 0)
sealed trait Event
case class ItemsAdded(item: Item, qty: Int) extends Event
case class ItemsRemoved(item: Item, qtyRemoved: Int) extends Event
case class AllOfItemRemoved(item: Item) extends Event
case object CheckedOut extends Event
val CommandHandler: (State, Command) => Seq[Event] = handleCommand(_, _)
val EventHandler: (State, Event) => State = handleEvent(_, _)
val InitialState = State(Map.empty, false)
private def handleCommand(state: State, cmd: Command): Seq[Event] =
if (!state.checkedOut) {
cmd match {
case AddItem(item, qty) if qty > 0 => Seq(ItemAdded(item, qty))
case AdjustQtyOfItem(item, newQty) if state.itemCounts.contains(item) && newQty >= 0 =>
val currentQty = state.itemCounts(item)
if (newQty > currentQty) {
handleCommand(state, AddItem(item, newQty - currentQty))
} else if (newQty == 0) {
Seq(AllOfItemRemoved(item))
} else {
Seq(ItemsRemoved(item, currentQty - newQty))
}
case CheckOut => Seq(CheckedOut)
case _ => Seq.empty
}
} else Seq.empty
private def handleEvent(state: State, evt: Event): State =
evt match {
case ItemsAdded(item, qty) =>
state.get(item)
.map { prevQty =>
state.copy(itemCounts = state.itemCounts.updated(item, prevQty + qty))
}
.getOrElse {
state.copy(itemCounts = state.itemCounts + (item, qty))
}
case ItemsRemoved(item, qtyRemoved) =>
state.get(item)
.map { prevQty =>
state.copy(itemCounts = state.itemCounts.updated(item, prevQty - qtyRemoved))
}
.getOrElse(state)
case AllOfItemRemoved(item) =>
state.copy(itemCounts = state.itemCounts - item)
case CheckedOut =>
state.copy(checkedOut = true)
}
}
Part of the confusion probably stems from "command handler" having a specific meaning in the application layer (where it's something from outside) and a slightly different meaning in the context of an event-sourced aggregate (the application layer command handler in an event-sourced application is basically just an anti-corruption layer translating external commands into commands against the aggregate (for instance the commands against the aggregate probably shouldn't contain an ID for the aggregate: the aggregate knows its ID)).
Upvotes: 1
Reputation: 57239
You'll probably want to review Jérémie Chassaing's recent work on Decider
My question(s): Is this approach "correct" / valid DDD or am I shooting myself in the foot with this?
It's fine - there's no particular reason that you need your functional design to align with "Java best practices" circa 2003.
If not, what is the main purpose of separating an aggregate function and a command handler?
Primarily to create a clear boundary between the abstractions of the problem domain (ex: "Cargo Shipping") and the "plumbing" - the application logic that knows about I/O, messaging, databases and transactions, HTTP, and so on.
Among other things, that means you can take the aggregate "module" (so to speak) and move it to other contexts, without disturbing the relationships of the different domain functions.
That said, there's nothing magic going on - you could refactor your "functional" design and create a slightly different design the gives you similar benefits to what you get from "aggregates".
Upvotes: 4
Reputation: 1722
Is this approach "correct" / valid DDD or am I shooting myself in the foot with this?
DDD is based on two principles :
By putting your business logic in your reducers you have failed the DDD principles and have achieved an anemic domain model. Your domain is indeed so anemic that it is even not modeled using OOP. This is important because by doing so, you violate the single responsibility principle (SRP) by having your reducers having two responsibilities : translating a series of event into state and validating business rules.
If not, what is the main purpose of separating an aggregate function and a command handler?
With the query handlers, command handlers implement parts of the interface specification, and lies in the application layer. It receive information from the clients (commands) and does some low level validation (for instance, reject malformed messages or unauthenticated requests). The command handler then calls other layers for them to do their job: the infrastructure layer for event store access, reducers for event to aggregates translation, and the domain layer for business rules validation and integrity. The code in these handlers is application specific as another application in the same domain will inherently have different interface specification and different commands to handle.
Aggregates are responsible for business logic and business rules. It is an abstraction of the actual concepts you are trying to manipulate. Good domain modeling try to be as application ignorant as possible, in order to increase reusability. A domain model could be used for multiple applications that does similar business. Whether you implement a piece of software used by pharmacists when delivering medications, or another one used by medicine doctors to prescribe them, you can use the same domain layer modeling drug interactions. Using OOP in your domain layer allows to model very complex business logic using very simple code. By putting the business logic in a separate layer, you can have a small team of developers working closely with business experts of the matter, to model all the business logic, constraints and processes, relevant to a set of applications. You can even unit test your domain.
Please note that your approach is perfectly acceptable, and can be very efficient. Doing DDD for the purpose of doing DDD is not a good practice. Doing good DDD modeling is not an easy task and should be considered as a mean of reducing complexity of large domain models.
Upvotes: -3