I am learning CQRS and Commanded by doing a project (so main point is learning), I’ve got to the point where it is already doing something useful on my laptop: you type a news article and then it uses ChatGPT for crafting some clever posts about it + you can tune the response with supposedly cool UI.
Now I want to make it public and let people play with it for free, but within some limits so I don’t get bankrupt on a hobby project (say, some 20-50 generations for user per month or per whole user lifetime) and… how do you do it in CQRS?
Non-CQRS ways
I come from Java/SpringBoot world so a relatively natural way for me would be to have some sort of credits field in the customer or user object. Then before doing something expensive (ChatGPT generation in this case) I’d check if there are credits available and decrease them in the same transaction. If generation fails, then some fail handler may or may not reuse credit back.
Ways for CQRS and specifically commanded
As everything in CQRS is eventually consistent, does it mean that I need to “book” some credits, make it somehow possible to monitor when the “booking” is completed (via some projection with a booking request id?) and only then do a request?
Or is possible to rely on Commanded’s strong consistency (or maybe it’s enough to specify option returning: aggregate_state when dispatching) and once dispatch has happened I can already know if the credits were enough or not.
Or am I missing some other obvious options? Like is it possibly common for exactly “credit checks” kind of use cases live outside CQRS so that DB transactions could be used for consistency protections and access serialization?
Again, my main point here is learning (though I still want to open project to public), so I’ll highly appreciate any hints and ideas, especially if ever did anything like that in CQRS.
P.S.
Even though it’s mostly self-education project almost invented just so I could have a real use case to play with CQRS, there is still some reasoning for exactly CQRS. My main technical motivation is about separating read-write models so I could have as many and as convenient read models as I like. Access serialization and consistency are way less important for me in this particular case.
Caveat: I’m a major beginner in EventSourcing and CQRS.
What about something like the following?
Manage an Event Stream (aggregate) per User per Refresh Period (month, year, all time).
eg generation-account-bob-202402
This tracks not just the count, but the lifecycle of each generation request.
pending, completed, failed to complete, rejected.
Also can have event that indicates the number of allowed credits.
Use a SubmitGenerationCommand that includes
request identifier
request payload/details about the generation TBD
Handling the SubmitGenerationCommand can emit Events
GenerationSubmittedEvent – with request identifier
GenerationRequestRejectedEvent – with details such as:
max pending requests limit reached
max credits reached.
An Event Handler or Process Manager can watch for GenerationSubmittedEvent and then issue commands or execute API requests?
Then somehow events get back into the “generation-accounts” stream that would indicate success or failure?
And more Event Handlers would watch for Success/Failure events to then send message to update the “conversation”?
Basically, requests go into a “rate limiter” aggregate for that user-period. The Events of Submitted/Completed/Failed/Rejected requests allow the single aggregate to track the used credits count, pending credits such that used credits + pending <= allowed credits ? and can reject requests over the limit?
And follow-on Event Handlers will eventual consistently process (succeed or fail) the submitted requests?
The above uses patterns:
Closing the Books – like accounting, so we can reset the user’s credit each period
Many small streams, instead of big streams – A small stream of data for each user-time-period instead of a single stream for the user of all time.
Write Ahead Pattern – we are writing the “request submitted” event to hold the pending place, then a Completion/Failed event to confirm the credit usage
We avoid:
Reservation Pattern (book keeping) – because if the credit is reserved/booked, that reservation has an expiration because that ticket may never have been used/completed… And even then, the generation processor that receives the ticket and executes for a GenerationCompleted response MAY race condition with the ExpirationEvent.
[edit]: Here’s a suggested video that also talks about how to do different kinds of “business transactions” that spans multiple streams: