-
Notifications
You must be signed in to change notification settings - Fork 21
Amend ADR 8: Clarify CQS Terminology #719
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
eb25eb0
9083ddd
c0926df
d433d5b
a602d14
6246617
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ clickjacking | |
| codebases | ||
| CODEOWNERS | ||
| CQRS | ||
| CQS | ||
| CTAP2 | ||
| deinitializer | ||
| deinitializers | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,22 +4,33 @@ sidebar_position: 6 | |
|
|
||
| # Server Architecture | ||
|
|
||
| ## CQRS ([ADR-0008](../adr/0008-server-CQRS-pattern.md)) | ||
| ## Command Query Separation (CQS) | ||
|
|
||
| Our server architecture uses the the Command and Query Responsibility Segregation (CQRS) pattern. | ||
| Our server architecture uses the Command Query Separation (CQS) pattern. | ||
|
|
||
| The main goal of this pattern is to break up large services focused on a single entity (e.g. | ||
| `CipherService`) and move towards smaller, reusable classes based on actions or tasks (e.g. | ||
| `CreateCipher`). In the future, this may enable other benefits such as enqueuing commands for | ||
| execution, but for now the focus is on having smaller, reusable chunks of code. | ||
| `CipherService`, which handles everything to do with a cipher) and move towards smaller classes | ||
| based on discrete actions (e.g. `CreateCipherCommand`, which handles only creating a cipher). This | ||
|
||
| results in smaller classes with fewer interdependencies that are easier to change and test. | ||
|
|
||
| :::note | ||
|
||
|
|
||
| Previously this pattern was referred to as Command Query Responsibility Segregation (CQRS). However, | ||
| CQRS is a far more complex architecture with larger impacts than we wanted or needed. Our | ||
| implementation in practice more closely resembled CQS, which has a more modest (but still effective) | ||
| aim of simply breaking up large classes. This documentation has been rewritten to better reflect our | ||
| practice and therefore refers only to CQS. | ||
|
|
||
| ::: | ||
|
|
||
| ### Commands vs. queries | ||
|
|
||
| **Commands** are write operations, e.g. `RotateOrganizationApiKeyCommand`. They should never read | ||
| from the database. | ||
| **Commands** are write operations, e.g. `RotateOrganizationApiKeyCommand`. They change the state of | ||
theMickster marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| the system. They may return data about the operation result (e.g. the updated object or an error | ||
| message), but otherwise should not be used to return data to its caller. | ||
|
|
||
| **Queries** are read operations, e.g. `GetOrganizationApiKeyQuery`. They should never write to the | ||
| database. | ||
| **Queries** are read operations, e.g. `GetOrganizationApiKeyQuery`. They should only return a value | ||
| and should never change the state of the system. | ||
|
|
||
| The database is the most common data source we deal with, but others are possible. For example, a | ||
| query could also get data from a remote server. | ||
|
|
@@ -28,18 +39,17 @@ Each query or command should have a single responsibility. For example: delete a | |
| file, rotate an API key. They are designed around verbs or actions (e.g. | ||
| `RotateOrganizationApiKeyCommand`), not domains or entities (e.g. `ApiKeyService`). | ||
|
|
||
| ### Writing commands or queries | ||
| Which you use will often follow the HTTP verb: a POST operation will generally call a command, | ||
| whereas a GET operation will generally call a query. | ||
|
|
||
| A simple query may just be a repository call to fetch data from the database. (We already use | ||
| repositories, and this is not what we're concerned about here.) However, more complex queries can | ||
| require additional logic around the repository call, which will require their own class. Commands | ||
| always need their own class. | ||
| Teams have wide discretion in how they structure their commands and queries. | ||
|
||
|
|
||
| The class, interface and public method should be named after the action. For example: | ||
| ### Structure of a command | ||
|
|
||
| ```csharp | ||
| namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys; | ||
| A command is just a class. The class, interface and public method should be named after the action. | ||
| For example: | ||
|
|
||
| ```csharp | ||
| public class RotateOrganizationApiKeyCommand : IRotateOrganizationApiKeyCommand | ||
| { | ||
| public async Task<OrganizationApiKey> RotateApiKeyAsync(OrganizationApiKey organizationApiKey) | ||
|
|
@@ -49,51 +59,43 @@ public class RotateOrganizationApiKeyCommand : IRotateOrganizationApiKeyCommand | |
| } | ||
| ``` | ||
|
|
||
| The query/command should only expose public methods that run the complete action. It should not have | ||
| The command should only expose public methods that run the complete action. It should not have | ||
theMickster marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| public helper methods. | ||
|
|
||
| The directory structure and namespaces should be organized by feature. Interfaces should be stored | ||
| in a separate sub-folder. For example: | ||
|
|
||
| ```text | ||
| Core/ | ||
| โโโ OrganizationFeatures/ | ||
| โโโ OrganizationApiKeys/ | ||
| โโโ Interfaces/ | ||
| โ โโโ IRotateOrganizationApiKeyCommand.cs | ||
| โโโ RotateOrganizationApiKeyCommand.cs | ||
| ``` | ||
| A command will usually follow these steps: | ||
|
|
||
| ### Maintaining the command/query distinction | ||
| 1. Fetch additional data required to process the request (if required) | ||
theMickster marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 1. Validate the request | ||
| 1. Perform the action (state change) | ||
| 1. Perform any side effects (e.g. sending emails or push notifications) | ||
| 1. Return information about the outcome to the user (e.g. an error message or the successfully | ||
| created or updated object) | ||
|
|
||
| By separating read and write operations, CQRS encourages us to maintain loose coupling between | ||
| classes. There are two golden rules to follow when using CQRS in our codebase: | ||
| If you have complex validation logic, it can be useful to move it to a separate validator class. | ||
| This makes the validator and the command easier to understand, test and maintain. | ||
|
|
||
| - **Commands should never read and queries should never write** | ||
| - **Commands and queries should never call each other** | ||
| Some teams have defined their own request and result objects to pass data to and from commands and | ||
| validators. This is optional but can be useful to avoid primitive obsession and have strongly typed | ||
| interfaces. | ||
|
|
||
| Both of these lead to tight coupling between classes, reduce opportunities for code re-use, and | ||
| conflate the command/query distinction. | ||
| ### Structure of a query | ||
theMickster marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| You can generally avoid these problems by: | ||
| A simple query may not require its own class if it is appropriately encapsulated by a single | ||
| database call. In that case, the "query" is just a repository method. | ||
|
|
||
| - writing your commands so that they receive all the data they need in their arguments, rather than | ||
| fetching the data themselves | ||
| - calling queries and commands sequentially (one after the other), passing the results along the | ||
| call chain | ||
| However, more complex queries can require additional logic in addition to the repository call. In | ||
| this case, it is appropriate to define a separate query class. | ||
|
|
||
| For example, if we need to update an API key for an organization, it might be tempting to have an | ||
| `UpdateApiKeyCommand` which fetches the current API key and then updates it. However, we can break | ||
| this down into two separate queries/commands, which are called separately: | ||
| A query is just a class. The class, interface and public method should be named after the data being | ||
| queried. For example: | ||
|
|
||
| ```csharp | ||
| var currentApiKey = await _getOrganizationApiKeyQuery.GetOrganizationApiKeyAsync(orgId); | ||
| await _rotateOrganizationApiKeyCommand.RotateApiKeyAsync(currentApiKey); | ||
| public interface IGetOrganizationApiKeyQuery | ||
| { | ||
| Task<OrganizationApiKey> GetOrganizationApiKeyAsync(Guid organizationId, OrganizationApiKeyType organizationApiKeyType); | ||
| } | ||
| ``` | ||
|
|
||
| This has unit testing benefits as well - instead of having lengthy "arrange" phases where you mock | ||
| query results, you can simply supply different argument values using the `Autodata` attribute. | ||
|
|
||
| ### Avoid [primitive obsession](https://refactoring.guru/smells/primitive-obsession) | ||
|
|
||
| Where practical, your commands and queries should take and return whole objects (e.g. `User`) rather | ||
|
|
@@ -103,3 +105,8 @@ than individual properties (e.g. `userId`). | |
|
|
||
| Lots of optional parameters can quickly become difficult to work with. Instead, consider using | ||
| method overloading to provide different entry points into your command or query. | ||
|
|
||
| ## Further reading | ||
|
|
||
| - [ADR-0008: Server CQS Pattern](../adr/0008-server-CQRS-pattern.md) - Architectural decision to | ||
| adopt CQS for breaking up large service classes | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -212,7 +212,7 @@ component "Organization Reports Module" { | |
| @enduml | ||
| ``` | ||
|
|
||
| ## Reactivity ([ADR-0003](../../../architecture/adr/0003-observable-data-services.md) & ADR-0028) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Broken reference that was caught as part of this work. There is no ADR 28, this was presumably meant to reference the Signals ADR here: #632 however that has not been merged yet.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd omit this personally as it's not part of this PR's scope. This mention has been sitting for a bit because the Angular docs are a work-in-progress. |
||
| ## Reactivity ([ADR-0003](../../../architecture/adr/0003-observable-data-services.md)) | ||
|
|
||
| We make heavy use of reactive programming using [Angular Signals][signals] & [RxJS][rxjs]. Generally | ||
| components should always derive their state reactively from services whenever possible. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.