@@ -4,22 +4,22 @@ sidebar_position: 6
44
55# Server Architecture
66
7- ## CQRS ( [ ADR-0008 ] ( ../adr/0008-server-CQRS-pattern.md ) )
7+ ## Command Query Separation (CQS )
88
9- Our server architecture uses the the Command and Query Responsibility Segregation (CQRS ) pattern.
9+ Our server architecture uses the Command Query Separation (CQS ) pattern.
1010
11- The main goal of this pattern is to break up large services focused on a single entity (e.g.
12- ` CipherService ` ) and move towards smaller, reusable classes based on actions or tasks (e.g.
13- ` CreateCipher ` ). In the future, this may enable other benefits such as enqueuing commands for
14- execution, but for now the focus is on having smaller, reusable chunks of code.
11+ We adopted this pattern in order to break up large services focused on a single entity (e.g.
12+ ` CipherService ` ) into smaller classes based on discrete actions (e.g. ` CreateCipherCommand ` ). This
13+ results in smaller classes with fewer interdependencies that are easier to change and test.
1514
1615### Commands vs. queries
1716
18- ** Commands** are write operations, e.g. ` RotateOrganizationApiKeyCommand ` . They should never read
19- from the database.
17+ ** Commands** are write operations, e.g. ` RotateOrganizationApiKeyCommand ` . They change the state of
18+ the system. They may have no return value, or may return the operation result only (e.g. the updated
19+ object or an error message).
2020
21- ** Queries** are read operations, e.g. ` GetOrganizationApiKeyQuery ` . They should never write to the
22- database .
21+ ** Queries** are read operations, e.g. ` GetOrganizationApiKeyQuery ` . They should only return a value
22+ and should never change the state of the system .
2323
2424The database is the most common data source we deal with, but others are possible. For example, a
2525query could also get data from a remote server.
@@ -28,18 +28,15 @@ Each query or command should have a single responsibility. For example: delete a
2828file, rotate an API key. They are designed around verbs or actions (e.g.
2929` RotateOrganizationApiKeyCommand ` ), not domains or entities (e.g. ` ApiKeyService ` ).
3030
31- ### Writing commands or queries
31+ Which you use will often follow the HTTP verb: a POST operation will generally call a command,
32+ whereas a GET operation will generally call a query.
3233
33- A simple query may just be a repository call to fetch data from the database. (We already use
34- repositories, and this is not what we're concerned about here.) However, more complex queries can
35- require additional logic around the repository call, which will require their own class. Commands
36- always need their own class.
34+ ### Structure of a command
3735
38- The class, interface and public method should be named after the action. For example:
36+ A command is just a class. The class, interface and public method should be named after the action.
37+ For example:
3938
4039``` csharp
41- namespace Bit .Core .OrganizationFeatures .OrganizationApiKeys ;
42-
4340public class RotateOrganizationApiKeyCommand : IRotateOrganizationApiKeyCommand
4441{
4542 public async Task <OrganizationApiKey > RotateApiKeyAsync (OrganizationApiKey organizationApiKey )
@@ -49,51 +46,43 @@ public class RotateOrganizationApiKeyCommand : IRotateOrganizationApiKeyCommand
4946}
5047```
5148
52- The query/ command should only expose public methods that run the complete action. It should not have
49+ The command should only expose public methods that run the complete action. It should not have
5350public helper methods.
5451
55- The directory structure and namespaces should be organized by feature. Interfaces should be stored
56- in a separate sub-folder. For example:
57-
58- ``` text
59- Core/
60- └── OrganizationFeatures/
61- └── OrganizationApiKeys/
62- ├── Interfaces/
63- │ └── IRotateOrganizationApiKeyCommand.cs
64- └── RotateOrganizationApiKeyCommand.cs
65- ```
52+ A command will usually follow these steps:
6653
67- ### Maintaining the command/query distinction
54+ 1 . Fetch additional data required to process the request (if required)
55+ 2 . Validate the request
56+ 3 . Perform the action (state change)
57+ 4 . Perform any side effects (e.g. sending emails or push notifications)
58+ 5 . Return information about the outcome to the user (e.g. an error message or the successfully
59+ created or updated object)
6860
69- By separating read and write operations, CQRS encourages us to maintain loose coupling between
70- classes. There are two golden rules to follow when using CQRS in our codebase:
61+ If you have complex validation logic, it can be useful to move it to a separate validator class.
62+ This makes the validator and the command easier to understand, test and maintain.
7163
72- - ** Commands should never read and queries should never write**
73- - ** Commands and queries should never call each other**
64+ Some teams have defined their own request and result objects to pass data to and from commands and
65+ validators. This is optional but can be useful to avoid primitive obsession and have strongly typed
66+ interfaces.
7467
75- Both of these lead to tight coupling between classes, reduce opportunities for code re-use, and
76- conflate the command/query distinction.
68+ ### Structure of a query
7769
78- You can generally avoid these problems by:
70+ A simple query may not require its own class if it is appropriately encapsulated by a single
71+ database call. In that case, the "query" is just a repository method.
7972
80- - writing your commands so that they receive all the data they need in their arguments, rather than
81- fetching the data themselves
82- - calling queries and commands sequentially (one after the other), passing the results along the
83- call chain
73+ However, more complex queries can require additional logic in addition to the repository call. In
74+ this case, it is appropriate to define a separate query class.
8475
85- For example, if we need to update an API key for an organization, it might be tempting to have an
86- ` UpdateApiKeyCommand ` which fetches the current API key and then updates it. However, we can break
87- this down into two separate queries/commands, which are called separately:
76+ A query is just a class. The class, interface and public method should be named after the data being
77+ queried. For example:
8878
8979``` csharp
90- var currentApiKey = await _getOrganizationApiKeyQuery .GetOrganizationApiKeyAsync (orgId );
91- await _rotateOrganizationApiKeyCommand .RotateApiKeyAsync (currentApiKey );
80+ public interface IGetOrganizationApiKeyQuery
81+ {
82+ Task <OrganizationApiKey > GetOrganizationApiKeyAsync (Guid organizationId , OrganizationApiKeyType organizationApiKeyType );
83+ }
9284```
9385
94- This has unit testing benefits as well - instead of having lengthy "arrange" phases where you mock
95- query results, you can simply supply different argument values using the ` Autodata ` attribute.
96-
9786### Avoid [ primitive obsession] ( https://refactoring.guru/smells/primitive-obsession )
9887
9988Where practical, your commands and queries should take and return whole objects (e.g. ` User ` ) rather
@@ -103,3 +92,8 @@ than individual properties (e.g. `userId`).
10392
10493Lots of optional parameters can quickly become difficult to work with. Instead, consider using
10594method overloading to provide different entry points into your command or query.
95+
96+ ## Further reading
97+
98+ - [ ADR-0008: Server CQS Pattern] ( ../adr/0008-server-CQRS-pattern.md ) - Architectural decision to
99+ adopt CQS for breaking up large service classes
0 commit comments