Dealing with side effects/reacting to events

299 views
Skip to first unread message

Bernardo Amorim

unread,
Sep 4, 2017, 7:34:29 PM9/4/17
to DDD/CQRS
Hey guys. I'm having a lot of trouble in modeling my domain, especially when it comes to deal with side effects.
So, the domain that I have is a merchant tool for receiving payments in bitcoin.
The merchant can generate new invoices that are associated with a bitcoin address generated for it.
The buyer can then send bitcoin payments to that address, but it can be more than one.
That being said, at a given time an invoice can be unpaid, underpaid, paid or overpaid and the data associated are a list of confirmed payments(aka transactions)
The unpaid and underpaid status doesn't trigger any side effect, however, when the status goes to paid, we need to send a postback to the merchant. Also, when it goes to overpaid status, we should initiate a refund process.

We are using Event Sourcing and CQRS (or at least trying to do so). So we have two functions: "execute :: Command -> State -> [Event]" and "apply :: Event -> State -> State".
The aggregate we have right now is something like:

Invoice:
  amount_to_pay: Int
  payments: [Payment]
  ...other data...

Payment:
  amount: Int
  txId: String

We have an event "AddPayments" that add any new payments to the aggregate

I had ideas to how to trigger these side effects.
The first two involve sending the events in an event bus once they are generated after a command is handled.
1) Add the previous state and next state to the events, so when the event changes the state we know only by looking at the event that we need to send the postback.
2) Create new events for "InvoicePaid", "InvoiceOverpaid", etc, so the execute command will add two events: AddPayments and InvoicePaid, as an example. Then, we listen to InvoicePaid events and send the postback.

The last one includes modeling side effects in the core of our app
3) Change execute to "execute :: Command -> State -> ([Event], [SideEffect])" and then trigger the side effects in the command handler (outside the aggregate)

What are your thoughts about these options?
I'd really appreciate some feedback because I'm kind of lost here.

Thank you a lot,
Bernardo

Samuel Francisco

unread,
Sep 5, 2017, 12:42:31 PM9/5/17
to DDD/CQRS
I think the state transition should be explicitly modeled as events: InvoiceUnderpaid, InvoicePaid and InvoiceOverpaid and the "side effects" should be modeled as event handlers.
In the case of the refund process a Process Manager shoud be used. As for the postback, a simple event handler should be suficiente.

Bernardo Amorim

unread,
Sep 5, 2017, 12:48:06 PM9/5/17
to DDD/CQRS
Samuel, I was just posting about that and that was what I had in mind.
The thing that were making me not do that was the fact that I needed to carry on the new added payments, but this is simply solved by having a "new payments" field in all of those events.

Bernardo Amorim

unread,
Sep 5, 2017, 12:58:47 PM9/5/17
to DDD/CQRS
Ah, and by the way, thank you for your comment.
As for having a process manager, what I was thinking was something like that

Cmd RequestRefund -> Evt RefundRequested -> (refund the client somewhere) -> Cmd ConfirmRefund -> Evt RefundConfirmed

What do you think?

Cheers,
Bernardo

George Cover

unread,
Sep 6, 2017, 2:17:21 PM9/6/17
to DDD/CQRS
Opposed to Samuel, I'd be more inclined to have a "PaymentReceived" event, then listeners that issue further events such as "InvoiveUnder/Over/Paid". Further listeners to those events can take actions as appropriate. Is "InvoiveUnderpaid" really an event? Does anything really care? It's just an invoice that hasn't been paid yet..it's not a state transition. If a single payment is received that both pays the invoice, and overpays, do you issue two events (InvoicePaid, InvoiceOverpaid), or just the single Overpaid event? If it's overpaid, when the refund is calculated do you issue an "InvoivePaid" event?

I think the source of truth is the "PaymentReceived" part - everything else is derived....granted, it's unlikely that the rules about what under/over/paid means will change, but you might want other actions to happen when a payment is received that you then have to "derive" if you issue "InvoiceUnderpaid" when a payment is received that doesn't complete the invoice...and likewise when "InvoiceOverpaid" is emitted when a payment is received that goes over the invoice amount.

George

Bernardo Amorim

unread,
Sep 6, 2017, 3:10:21 PM9/6/17
to DDD/CQRS
Hi George, thanks for the reply.
Indeed, all of the other events can be just derived from the tuple (PrevState, PaymentReceived). However, the problem is that if I just emit "PaymentReceived" I'd have to get the copy of all previous events in order to know whether it was under, over or just paid, which is an undesirable thing. So if I go with that route, I'd probably have to add the other payments.
As for InvoiceUnderpaid, you are right, nobody cares. One alternative would be to call it InvoicePartiallyPaid (its the same thing, but the point is that we are not saying it will be "Underpaid" as a bad thing, but just because it received payments but is not completed yet)
As for refund, I wouldn't emit a InvoicePaid event, instead I'd probably have a lot of variations of an event for InvoiceRefunded. Maybe it even makes sense to have many events.
We can emit a refund because it was Overpaid (which is just a correction), because the merchant couldn't keep with their agreement (missing products, unhappy customer, etc) and had to do a partially or complete refund. That being said, maybe it makes sense to have one event for each of these cases. As an example, a partially refund can be seen as a correction to pricing as well, which if the buyer add more funds to it we should report that as an overpayment as well.
Oh, how I hate the fact that users can always add more funds to the address... I'm almost considering just accepting all the bitcoins they transfer and HODL. lol

George Cover

unread,
Sep 7, 2017, 5:11:31 AM9/7/17
to DDD/CQRS
Definitely keep all bitcoins they send :)

I still wouldn't fire any event for an invoice not being paid - it's a non-event. There is no event to track. Every invoice, until the "InvoiceOver/Paid" event is seen is considered "Not Paid". If the merchant has to issue a correction to the invoice there should be an event to signify this - this is an important part of the domain. "InvoiceCorrected" would contain the data, and then a listener decides if a refund is required, and triggers that process if necessary. I'd probably do something like this - and we tend to favour the microservice approach at the moment, so it does look like a lot of "things"....we also use event sourcing, so I may be muddying the water by cross contamination of concepts :-)

User send "Payment" object to "ProcessPayment" command handler. "ProcessPayment" emits"PaymentReceied" (if command is valid...you know you're logic here).
"MarkInvoiceAsPaid" listener responds to "PaymentReceived" event and emits "InvoicePaid" event if invoice balance goes through zero (as in the change takes it from a positive balance to zero, or negative). Talk to your domain expert if you ever think the balance of an invoice could increase (dodgy refunds?) and how to handle that. Maybe having two "InvoicePaid" events is fine (probably is).
"IssueRefundIfOverPaid" listener responds to "PaymentReceived" event and issues "Refund" object to "IssueRefund" command handler. "IssueRefund" emits "UserRefunded" (or something) depending on logic...has invoice got negative balance.

Our listeners have an internal state that they build to keep a track of objects they are interested in (and just the bits they need, not the full object). They neatly describe our business rules, keep them encapsulated in a single place and only change if they absolutely have to (I prefer to deploy new things). I'd also have other listeners building view models for the interface...

Federico G

unread,
Sep 13, 2017, 9:50:47 PM9/13/17
to DDD/CQRS
'However, the problem is that if I just emit "PaymentReceived" I'd have to get the copy of all previous events in order to know whether it was under, over or just paid, which is an undesirable thing.'

Isn't that the responsibility of the Aggregate that receives the payment? Instead of passing around the previous state, you may rehydrate the aggregate from the previous events and emit the overpaid event when appropriate. Event handlers don't need to know about the previous state. Do this make any sense to you?

George Cover

unread,
Sep 14, 2017, 6:20:35 AM9/14/17
to DDD/CQRS
Yes, to a certain degree. I don't see any problem with Event Handlers having an understanding of state - it can be important if you consider things like locking accounts where three "LoginFailed" events in a period of time results in a "LockAccount" command being issued. (I would have a "LockAccountOnFailedLogins" service doing this, NOT the "Login" command handler.

I like things only doing one thing only...the "ReceivePayment" command would do multiple things in your scenario - assess whether the command is valid and the state of the account. I'd prefer to see the "account status" evaluation in another service that's has a single responsibility of understanding whether the account has been overpaid. Splitting up the responsibility means that you wouldn't need to deploy the "ReceivePayment" service again, unless it changed (and it would only change if the mechanics of receiving a payment have changed, not what happens to an account if goes overpaid, etc).

The "PaymentReceived" command handler doesn't need to rehydrate the aggregate to understand whether a payment can be made (unless you start not accepting payments on accounts that are paid, but you're not doing that). It also doesn't need to reject a payment if the account would be overpaid. So why bother checking any of that stuff at the "ReceivePayment" command handler...just assess whether the data is valid, then issue the "PaymentReceived" event. Then event handlers can handle the richness of the business domain by assessing state.
Reply all
Reply to author
Forward
0 new messages