Is it sometimes necessary to duplicate business logic between commands and queries in CQRS? For example, if a DELETE /projects/{id} endpoint enforces deletion rules (e.g., active status, no assigned users, no payment options), while a GET /projects/{id} endpoint needs to return an isDeletable flag for the UI, the same logic must be implemented in both places. This ensures consistency but can lead to maintenance challenges as complexity grows. An alternative is maintaining isDeletable flag in the write model, but this requires tracking events that affect it, adding overhead. How should this trade-off be handled in CQRS?
Is it sometimes necessary to duplicate business logic between commands and queries in CQRS? For example, if a DELETE /projects/{id} endpoint enforces deletion rules (e.g., active status, no assigned users, no payment options), while a GET /projects/{id} endpoint needs to return an isDeletable flag for the UI, the same logic must be implemented in both places. This ensures consistency but can lead to maintenance challenges as complexity grows. An alternative is maintaining isDeletable flag in the write model, but this requires tracking events that affect it, adding overhead. How should this trade-off be handled in CQRS?
Share Improve this question asked Mar 31 at 22:12 Stefan MilivojevicStefan Milivojevic 311 bronze badge New contributor Stefan Milivojevic is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct. 1- 2 To answer your question the whole point of Queries vs Commands is to separate queries from mutations and CQRS to not tie components and calls together. There is no overlap, one selects data the other changes data. I think you're confusing the fact that the DELETE validation requires a GET and that's the way it works. – Jeremy Thompson Commented Apr 1 at 0:33
1 Answer
Reset to default 0In CQRS, I normally think of the flow of information as a triangle.
- Command processing brings information from the outside world to the book of record
- Query processing delivers information from the cache to the outside world
- Background processing (?!) copies information from the book of record to the cache.
If you were doing "event sourcing", for example, your book of record would be a storage for sequences of events, and your cache would be a document store, or a RDBMS, or some such thing.
It's "normal" that the processes that interact with the event store (command processing and background processing) share a lot of code, because they need to have a common understanding of the information that is written to the store.
Therefore, we shouldn't be afraid of the fact that some "logic" is being shared between command processing and background processing.
It is still important, however, to be careful not to confuse policy with fact.
The problem with treating isDeletable
as a fact to be tracked by your system is that (in the general case)_ your policies for when a project can be deleted may change between when you wrote the most recent fact into the history of the project and when the operator's interface is rendered.
Having isDeletable
as a flag in your "read model" is fine (assuming that you can re-build those models cost effectively) because when you deploy a change to policy you can "just" invalidate the cached documents and rebuild them all.