Reputation: 13
Suppose I want to make an e-commerce system. I have 2 aggregates here ProductAggregate and UserAggregate. Product aggregate contains productId, price. User aggregate contains userId and balance. Here's the problem, in event-sourcing we should not rely on the read model since there might be eventual consistency problem. Ok so we should rely on the command model right I guess?, but this two command model is different. I read from somewhere else they told me that aggregate should only rely on its state. Let's say the user want to buy a product I have to check if he has enough balance and in order to do that I need to know the price of product. So read model not allowed, aggregate query not allowed. what options do I have here?
const ProductAggregate = {
state: {
productId: "product-1",
price: 100
}
}
const UserAggregate = {
state: {
userId: "userId-1",
balance: 50
},
handlePurchase: ({ userId, productId }) => {
// todo I got productId from the client, but how can I retrieve its price ?
if (this.state.balance < price) {
throw "Insufficient balance bro."
}
}
}
So I though it must be my bad aggregate design which makes UserAggregate requires state from outside of its context. So in this situation how do I properly design an Aggregate for User and Product.
edited:
I have been thinking all day long for the solution and I came up with this approach. So instead of putting purchase command in the UserAggregate I put it in the ProductAggregate and call it OrderProductCommand which is a bit weird for me since the product itself can't create an order, but the user can (it seems to work anyway I don't even know?). So with this approach I can now retrieve the price and send another command DeductBalanceCommand which will deduct amount of money from the user.
const ProductAggregate = {
state: {
productId: "product-1",
price: 100
},
handleOrder: ({productId, userId}) => {
await commandBus.send({
command: "handleDeduct",
params: {
userId: userId,
amount: this.state.price
}
})
.then(r => eventBus.publish({
event: "OrderCreated",
params: {
productId: productId,
userId: userId
}
}))
.catch(e => {
throw "Unable to create order due to " + e.message
})
}
}
const UserAggregate = {
state: {
userId: "userId-1",
balance: 50
},
handleDeduct: ({ userId, amount }) => {
if (this.state.balance < amount) {
throw "Insufficient balance bro."
}
eventBus.publish({
event: "BalanceDeducted",
params: {
userId: userId,
amount: amount
}
})
}
}
Is it fine and correct to use this approach? it's a bit weird for me or maybe it's just a way of thinking in DDD world?
ps. I added javascript tag so my code can have colors and easy to read.
Upvotes: 0
Views: 566
Reputation: 20561
First of all, regarding your handle, you're not stupid :)
A few points:
In many situations you can query the read model even though there's eventual consistency. If you reject a command that would have been accepted had a pending update become visible in the read model, that can typically be retried. If you accept a command that would have been rejected, there's often a compensating action that can be applied after the fact (e.g. a delay between ordering a physical product and that product being delivered).
There are a couple of patterns that can be useful. One is the saga pattern where you would model the process of a purchase. Rather than "user A buys product X", you might have an aggregate corresponding to "user A's attempt to purchase product X", which validates and reserves that user A is able to buy X and that X is able to be purchased.
Every write model with an aggregate implies the existence of one sufficiently consistent read model for that aggregate. One can thus define queries or "read-only" commands against the write model. CQRS (IMO) shouldn't be interpreted as "don't query the write model" but "before trying to optimize the write model for reads (whether ease, performance, etc.), give strong consideration to handling that query with a read model": i.e. if you're querying the write model, you give up some of the right to complain about the queries being slow or difficult. Depending on how you're implementing aggregates this option may or may not be easy to do.
Upvotes: 0