Command Query Separation | Object-Oriented Design Principles w/ TypeScript

I recently discovered a programming term, Heisenbug.
No, I'm not talking about about a certain chemistry teacher turned criminal drug-lord.
HeisenBUG.
It's a pun on the name of a certain German physicist, Werner Karl Heisenberg.
Heisenberg's discovery in particle physics was the uncertainty principle. It stated that "the very act of observing [a particle] alters the position of the particle being observed, making it impossible (in theory) to accurately predict its behavior" 1.
Is it just me or does that remind you of your worst debugging nightmares?
HeisenBUG takes after the uncertainty principle, referring to scenarios where it's incredibly hard to find a bug. It specifically refers to scenarios where the bug seems to disappear or act differently when we make attempts to take a closer look at it.
In programming, unexpected side-effects are largely the cause of such bugs.
If asking a question changes the answer, we're likely to run into problems.
Command-Query Separation (CQS)
Command-Query Separation (CQS) is a design principle (while not strictly object-oriented) that states that:
a method is either a
COMMAND
that performs an action OR aQUERY
that returns data to the caller, but never both.
In simpler terms, "asking a question shouldn't change the answer".
Why does this matter?
The idea is to separate the code paths for operations that change the system from those that simply request data from the system.
By enforcing this separation, the code becomes simpler to understand. Is this changing something, or just fetching something? When a method does both (changes the state of the application and retrieves data), it becomes a lot harder to understand it's true purpose.
This can lead to really hard to reason about application state.
A messy Job Recommendations API Design
Imagine we were building a Job Recommendations Service.
Assume we have two API routes:
GET /jobs - Returns jobs for me to view
GET /jobs/:jobId - Returns a particular job
Consider if everytime I did a GET
to /jobs/:jobId
, it changed the state of the system by changing what comes back when I do GET
to /jobs
.
Eventually, as I view more jobs with /jobs/:jobId
, I'll see more relevant jobs in my calls to /jobs
.
In theory, that's one way to build out job recommendations in our system.
But this type of design makes the /jobs
API incredibly inconsistent.
Consider how hard it would be to test and validate it's working properly.
Also, consider the actors in this system are as follows:
JobSeekers
: Job seekers are people who are actually looking for jobsRecruiters
: Recruiters work for companies and try to get job seekers to apply to jobsEmployers
: Employers are the people who post the jobsPublic
: Anonymous users can also view jobs on the job board without an account
Assume that every actor was able to use the /jobs/:jobId
API call to retrieve a posting.
Should the system change for every actor that fetches a job posting with /jobs/:jobId
?
Of course not, JobSeekers
are probably the only group that this should apply against.
This feels really complex because several user groups are reliant on the same resource, but we're trying to apply side-effects for one of them in particular.
You can be sure that using a design like this, there would be a dirty if
statement in there somewhere:
interface Request {
userId?: string;
jobId: JobId;
}
class GetJobByJobIdUseCase implements UseCase<Request, Promise<Job>> {
...
execute (request: Request): Promise<Job> {
const { userId, jobId } = request;
if (!!userId) {
const user = await this.userRepo.findById(userId);
const isJobSeeker = !!user.roles.find((r) => r === 'JobSeeker');
if (isJobSeeker) {
// dirty, log job view
}
}
const job = await this.jobRepo.getJobByJobId(jobId);
...
}
}
A better design
Let's be explicit about the COMMANDS
, QUERIES
, what changes the system, and what retrieves data.
We can improve the design by extracting the side-effect into a COMMAND
-like POST call to log the job view separately from retrieving it.
GET /jobs - Returns all jobs
GET /jobs/:jobId - Returns a particular job
POST /jobs/:jobId/view - 🔥 Logs a job view
This tremendously simplifies the code paths for both logging a job view and retrieving a particular job.
We can improve the design even further by extracting the functionality to see recommended jobs from within it's own /jobs/recommendations
API.
GET /jobs - Returns all jobs
GET /jobs/:jobId - Returns a particular job
POST /jobs/:jobId/view - Logs a job view
GET /jobs/recommendations - 🔥Returns my personal job recommendations
(used by JobSeekers only)
Usage of this API from the UI would mean that everytime we perform a GetJobById
QUERY
, we accompany that with a LogJobView
COMMAND
.
This way, we have two separate code paths: one for changing the system and for pulling data out of the system. We can rest safely knowing that if we change anything with regards to QUERY
-ing, it won't break anything in regards to how we execute COMMAND
s, and vice-versa.
Violation of the principle at the code level
Consider you wrote the following postComment
method in a Comment Moderation System.
interface Comment {
name: string;
url: string;
content: string;
id: string;
}
class CommentRepo {
...
postComment (name: string, url: string, content: string): Promise<Comment> {
const CommentSequelizeModel = this.models.Comment;
// Post comment
const comment = await CommentSequelizeModel.create({ name, url, content });
return CommentMap.toDomain(comment); // with commentId on it
}
}
First of all, take a look at method signature:
postComment (name: string, url: string, content: string): Promise<Comment>
The name implies that the operation will be a COMMAND
, but it returns a value as well, violating the principle.
Perhaps returning the Comment
created can be justified. Assume a CommentService
exists, and in order to craft a Slack channel message notifying us of a new comment, we needed the commentId
of the newly created Comment
returned from the CommentRepo
's postComment
method.
class CommentService {
...
async postCommentAndPostToSlack (name: string, url: string, content: string) {
const comment = await this.commentRepo.postComment(name, url, content);
// Needs comment.commentId in order to craft the message.
this.slackService.sendMessage(`
New comment posted:
=> Name: ${name}
=> Url: ${url}/commentId/${comment.id}
=> Content: ${content}
`)
}
}
So then...
What's wrong with this code?
- While the
CommentRepo
method is deceptively namedpostComment
, it's not only responsible for posting the comment, but also for retrieving the comment that was posted. Developers reading the method signature might get confused as to the single responsibility of this method.QUERY
capability should be delegated to a new method, perhapsgetComment(commentId: string)
. - There's an issue in not generating the id for the
Comment
from within the domain layer, but leaving it up to the persistence layer (Sequelize) as shown here. That can lead to blatant violation of the principle in order to know the identifier of the Entity just saved to the database. That poor design forces calling code executing aCOMMAND
to not only know if theCOMMAND
succeeded or failed, but also forces theCOMMAND
to return the value if successful.
Fixing it
In addition to switching to creating the entire Comment
domain model from within the Domain Layer (using Value Objects and Entities containing the UUID identifier), we can segregate the COMMAND
from the QUERY
aspect of the postComment
method by introducing a new method, getComment
.
class CommentRepo {
...
postComment (comment: Comment): Promise<void> {
const CommentSequelizeModel = this.models.Comment;
// Post comment
await CommentSequelizeModel.create(comment);
return;
}
getComment (commentId: CommentId): Promse<Comment> {
const CommentSequelizeModel = this.models.Comment;
const createQuery = this.createQuery();
createQuery.where['comment_id'] = commentId.id.tostring();
const comment = await CommentSequelizeModel.findOne();
return CommentMap.toDomain(comment)
}
}
And now, from CommentService
, we should be able to send the Slack message without relying on the return value from a COMMAND
.
class CommentService {
...
async postCommentAndPostToSlack (name: string, url: string, content: string) {
const comment: Comment = Comment.create(name, url, content);
await this.commentRepo.postComment(comment);
// Needs comment.commentId in order to craft the message.
this.slackService.sendMessage(`
New comment posted:
=> Name: ${name}
=> Url: ${url}/commentId/${comment.id}
=> Content: ${content}
`)
}
}
Quiz
Let's see how well I explained that.
Which of these are valid COMMAND
s?
getComment (commentId: CommentId): Promise<void>
createJob (job: Job): Promise<Job>
postComment (comment: Comment): Promise<Comment[]>
approveComment (commentId: CommentId): Promise<void>
Which of these are valid QUERIES
?
getAllVinyl (): Promise<Vinyl[]>
getVinylById (vinylId: VinylId): Promise<Vinyl[]>
getAllVinyl (): Promise<void>
getAllVinylById (vinylId: VinylId): Promise<Vinyl>
CQS is a constantly occuring principle in several contexts of software development
CQS in CRUD
Create Read Update and Delete (CRUD) is often how we think about designing trivial MVC applications. Each operation in CRUD perfectly fits the definition of either a COMMAND
or a QUERY
.
- CRUD Commands:
Create
,Update
,Delete
- CRUD Queries:
READ
CQS in RESTful HTTP
CQS also syncs up with the principles of RESTful HTTP. An HTTP method is also either a COMMAND
or a QUERY
.
- HTTP Commands:
POST
,PUT
,DELETE
,PATCH
- HTTP Queries:
GET
In times where it may be challenging to think about the behaviour of a particular RESTful API route, think back to the CQS principle.
- Create User: POST -
/api/users/new
- Get a user: GET -
/api/users/:userId
CQS in SQL
Nearly every operation we do in SQL is a COMMAND
and only one operation is a QUERY
. I'm sure you can guess which one (hint: it rhymes with "REFLECT").
CQS in Use-Case Design
Projects beyond simple MVC are centered around the use cases of the application. These are the features for an individual group of people/Actors
.
Every use case is strictly a command or a query.
In a Blog
subdomain,
- Commands:
CreatePost
,UpdatePost
,DeletePost
,PostComment
,UpdateComment
- Queries:
GetPostById
,GetAllPosts
,GetCommentById
,GetAllCommentsForPost
CQS in Domain-Driven Design Architecture with CQRS
In Domain-Driven Design, it makes sense to separate the READ
models from the WRITE
models. WRITE
s usually take a little bit more time to finish executing because a WRITE
to an aggregate requires the aggregate to be fully-constituted and contain everything within it's consistency boundary in order to enforce invariants.
Using the same READ
model as a WRITE
model can be expensive as we've explored in this article.
It makes more sense to enable the READ
models to be used for all QUERIES
and the WRITE
model to be used for the COMMAND
s.
This separation into two different types of models is an architectural pattern called Command Query Responsibility Separation (CQRS).
If you're doing Domain-Driven Design, it will likely be hard for you to avoid it.
Performing Reads in CQRS
Performing Writes in CQRS
Summary
- In theory, the vast majority of operations that we write in programming only either do one or the other (change the application state, or retrieve some data), but there are times where it can be unclear.
- Being conscious of this principle can clear up any ambiguity towards if a method of a
COMMAND
or aQUERY
and if we expect any side effects to change the state of the app. This reduces bugs, improves readability, and makes code more testable. - CQS usually isn't thought about in simple CRUD applications because each of the operations in Create Read Update and Delete operations are obviously either a command or a query.
- CQS is well-suited towards complex domains where Domain-Driven Design is most useful.
- There exists another technique called CQRS which is essentially CQS, but at the architectural level.
- In DDD apps, use cases can sometimes ambiguously feel like a
COMMAND
and aQUERY
, like theGetJobById
QUERY
(Job board example). It's important, for performance, to be aware of what really is aCOMMAND
and what really is aQUERY
, and use CQRS to separate the read and write models.
-
Uncertainty Principle - Fascinating wikipedia entry on the principle also sometimes known as The Heisenberg Effect.
↩
Discussion
Liked this? Sing it loud and proud 👨🎤.
Stay in touch!
Enjoying so far? Join 15000+ Software Essentialists getting my posts delivered straight to your inbox each week. I won't spam ya. 🖖
View more in Design Principles
You may also enjoy...
A few more related articles




Want to be notified when new content comes out?
Join 15000+ other Software Essentialists learning how to master The Essentials of software design and architecture.
5 Comments
Commenting has been disabled for now. To ask questions and discuss this post, join the community.
Thanks for such cool article!!
One question related to this topic and talking between services. For example I have 2 services 1 service to work with http protocol and another one for working with web sockets. If I have some domain entity changed on http service I am using redis pub/sub to notify about it the second service. Second service have to received it and run some use case.
When I receive the id of changed entity, should I recreate full domain object and use domain events to run use case, or should I run use case directly from pub/sub redis service?
Thanks!
Thanks for the article! I just missed some examples of implementation of read and write models. Doesn't make it confusing to have different models to represent the same data?
I didn't know this concept, it's really makes sense.
I didn't understand from where you got comment.id on commentService.ts, on fixed version. You create a new comment and persist without retrieve created object. How you will use CommentRepo.getComment method if you don't get id after persiste?
Thanks!
This is one of the most clarifying articles I´ve read about CQS and CQRS. Thank you so much <3
Hi, thanks for sharing. I always have a problem with this design (which you do an example in your article). Which is postComment, what if, get a comment is really hard to get (I mean cost you a lot of resources) and you know that in the postComment you need to use that request. You should do that twice, or maybe create a type of cache for speeding up? Isn't too much?
In your example, you create a comment, which is a Factory, which is a command (it shouldn't return something). Let's suppose that Factory is on another layer different from commands and queries. Do you create the ID on the Factory, right? Because if not, if you are assigning the ID in the command, you are modifying an argument, which is something that is not considered a good practice.
Also, when you do a POST on API, you need to return 200 or Error (you are creating and getting there). I understand that complex Commands should be better that way, it simplifies the descriptions. But on simple CRUD it looks like too much not being able to return the ID of the command that you are doing the operation.
I understand the rules, and a lot of them makes sense, but in some areas are like too much (I think)