Server Architecture
CQRS (ADR-0008)
Our server architecture uses the the Command and Query Responsibility Segregation (CQRS) 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.
Commands vs. queries
Commands are write operations, e.g. RotateOrganizationApiKeyCommand
. They should never read
from the database.
Queries are read operations, e.g. GetOrganizationApiKeyQuery
. They should never write to the
database.
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.
Each query or command should have a single responsibility. For example: delete a user, get a license
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
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.
The class, interface and public method should be named after the action. For example:
namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys;
public class RotateOrganizationApiKeyCommand : IRotateOrganizationApiKeyCommand
{
public async Task<OrganizationApiKey> RotateApiKeyAsync(OrganizationApiKey organizationApiKey)
{
...
}
}
The query/command should only expose public methods that run the complete action. It should not have public helper methods.
The directory structure and namespaces should be organized by feature. Interfaces should be stored in a separate sub-folder. For example:
Core/
└── OrganizationFeatures/
└── OrganizationApiKeys/
├── Interfaces/
│ └── IRotateOrganizationApiKeyCommand.cs
└── RotateOrganizationApiKeyCommand.cs
Maintaining the command/query distinction
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:
- Commands should never read and queries should never write
- Commands and queries should never call each other
Both of these lead to tight coupling between classes, reduce opportunities for code re-use, and conflate the command/query distinction.
You can generally avoid these problems by:
- 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
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:
var currentApiKey = await _getOrganizationApiKeyQuery.GetOrganizationApiKeyAsync(orgId);
await _rotateOrganizationApiKeyCommand.RotateApiKeyAsync(currentApiKey);
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
Where practical, your commands and queries should take and return whole objects (e.g. User
) rather
than individual properties (e.g. userId
).
Avoid excessive optional parameters
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.