Please, note that this is strongly my opinion and I’m more than open to hearing from you! Hopefully, this will help you in your next project 😉.
GitHub code Repo: (Implemented in Deno & TypeScript) - Can run in Node, Bun, and the browser.
Head to the “Simple CQRS” section for the actual implementation.
I've been working on startups ever since 2015. One of the capabilities of a true and tried startup is the ability to move fast and adapt to changes.
It was late 2015, and my application started to suffer 😥. Having limited knowledge about software architecture, I thought that the traditional view-business logic partnered with dependency injection will cut it (I was building with ASP.NET MVC 4.5 at that time).
Unfortunately, that was not the case. After a while, classes, and methods became entangled with each other. It became painful adding and or extending current functionality even though I had no concrete dependencies in most of my files. This meant that my time to market was greatly delayed, and bug fixing could introduce regressions (as I didn’t know why tests were important back then).
This lead me to research and stumble upon Andrea Saltarello’s and Dino Esposito’s mind-blowing Architecting Application for Enterprise book. I learned how to structure complex applications using lesser-known approaches: Domain Driven Design, sagas, event buses, event sourcing, clean architecture, onion architecture, CQRS, and many other patterns. While I didn’t grasp them at first, the more I matured with writing code and working on other startups, I began to realize their importance, and the way I’d do applications today would be wildly different from how I did them 7 years back.
From the 3-layered 2-tier approach, my applications began looking something like this:
This greatly reduced the spaghetti code that surfaced. Breaking functionality into bounded contexts and then serving them through different layers, I was able to define clear boundaries, introduce new features and replace parts of the application with limited damage.
Yet, there were instances in which some of these layers presented a series of challenges:
That’s when I re-read the architecting book and found some videos about clean architecture. It re-introduced me to CQRS through the MediatR library from Jimmy Bogard.
Comand-Query Responsibility Segregation allows you to break your service/application layers into commands and queries, giving you the possibility to program a single responsibility per action. You’d then communicate across the layers through a single dependency, a dispatch, which would fire a command or a query and return the appropriate result (For purists, commands do not return, but I found it a little bit impractical). This is similar to Redux for those who’ve worked in SPAs and GraphQL mutations and queries.
I think it was Greg Young who brought it up for the first time as a successor to the Command Query Separation (CQS) pattern. They are very similar except that CQRS splits the data persistence layer and the DB underneath in two. This would allow you to have one DB for reads (queries), and one for writes (commands). (You’d hydrate/insert data into the query DB through eventual consistency or another syncing mechanism). That alone can become pretty complex, and that is out of the scope of this post.
Fortunately for us, nowadays, with the advent of serverless and powerful services such as DynamoDB, Planet Scale, Supabase, SurrealDB, and many more, those physical DB separations that Greg suggested
As mentioned above, the CQRS pattern allows you to
This brings the following benefits:
You’ll see down below that I’ll be sharing
You can use this as your starting ground, and add more complexity as your project needs it.
This example is written in TypeScript, and there’s a GitHub repo that was built using Deno, but works really well in Node, Bun, the browser, Cloudflare Workers, Edge Workers, Lambda, Azure Functions, or whatever. It’s just plain JavaScript and there’s no magic.
In addition, this can be extrapolated to other languages like Ruby, Python, C#, Java, Rust, and many more.
You can download it here:
https://github.com/superjose/simple-cqrs
As I said previously, if you’ve used Redux or GraphQL, you are already implementing this pattern without you knowing! We will implement our own version of it so we can configure our app to support commands and queries (akin to mutations and queries in GraphQL).
The mediator by itself is a form of an observable. You define a simple object that will hold other objects in a key-value fashion:
// File: mediator.ts
const commands = {}
The commands
object will hold a string key (type
that will refer to the command’s or query’s name) and then a reference to a function, which will be the function to execute (whether it’s a command or a query).
You then populate them by using a register function:
// File: mediator.ts
export function registerCommandOrQuery(type, arg) {
commands[type] = arg
}
In which type
is a string that will hold the command or the query name, and arg
will be the reference to the function that you’d like to execute.
Next, you call the registerCommandOrQuery
for each of the functions to use:
// File: register.ts
registerCommandOrQuery({
type: "REGISTER_USER",
arg: RegisterUserCommandHandler,
});
registerCommandOrQuery({
type: "GET_USERS",
arg: GetUsersQueryHandler,
});
Check the repo to understand how the pieces fit together.
This will add the RegisterUserCommandHandle
under the ”REGISTER_USER”
key and the GetUsersQueryHandle
under the ”GET_USERS”
key.
Finally, you’ll be able to fire them or dispatch them with a dispatch
function:
// File: dispatch.ts
export async function dispatch({ type, arg }) {
return commands[type](arg);
}
In which type
is a string that will hold the command or the query name, and arg
will be the arguments for the function to be passed (for dispatch
).
Note that type
is the same for both the registerCommandOrQuery
and dispatch
Call the dispatch method
dispatch({
type: "REGISTER_USER",
arg: {
userId: "my userId",
password: "my password"
}
});
Where REGISTER_USER
was the one that I used to define RegisterUserCommandHandler
. If I were to have a V2.
It’s as simple as that. No magic and no sorcery. The command
object holds a reference to our command and queries, and we use the dispatch
function to launch them. This simple addition will make your applications more portable.
But wait! We’ll break it down further and spice it up with TypeScript!
With TypeScript, there’s additional work to do, as we need to provide type safety to each of the elements:
// File: mediator.ts
// Same code as previous section, but putting it all together + typings
import { MediatorCommandsAndQueries } from "./mediator-commands.ts";
const commands: {
[x: string]: MediatorCommandsAndQueries["arg"];
} = {};
export function registerCommandOrQuery({
type,
arg,
}: MediatorCommandsAndQueries) {
commands[type] = arg;
}
export async function dispatch<TFunction extends (...args: any) => any>({
type,
arg,
}: MediatorCommandsAndQueries) {
return (commands[type] as Function)(arg) as ReturnType<TFunction>;
}
In TypeScript, we have another file that holds our MediatorCommandsAndQueries
type, which will be the typing glue of our command
and dispatch
:
// File: mediator-commands.ts
import {
CreateNewPostCommand,
CreateNewPostCommandHandler,
GetAllPostsQuery,
GetAllPostsQueryHandler,
GetUsersQuery,
GetUsersQueryHandler,
RegisterUserCommand,
RegisterUserCommandHandler,
} from "../bounded_contexts/index.ts";
export type MediatorCommandsAndQueries =
| {
type: "REGISTER_USER";
arg: RegisterUserCommand | typeof RegisterUserCommandHandler;
}
| {
type: "GET_USERS";
arg: GetUsersQuery | typeof GetUsersQueryHandler;
}
| {
type: "GET_POSTS";
arg: GetAllPostsQuery | typeof GetAllPostsQueryHandler;
}
| {
type: "CREATE_POST";
arg: CreateNewPostCommand | typeof CreateNewPostCommandHandler;
};
You’d find all those function definitions in the GitHub repo.
I’d love to get some feedback on this approach. This was the best I could do that could be shared between registering the commands and queries and dispatching them with the same type.
This will give the dispatch
function typing superpowers. Once you start writing the proper type
the arg
you will be greeted with autocomplete and type-checking functionalities!
Queries and Commands are just POJO (Plain Old JavaScript Objects) simple files. At the bare minimum, it contains a single function that is exported! No external dependency needed, which adds the huge benefit of having your application decoupled from a specific infrastructure or technology.
For code cleanness, I do add the function’s parameter as a separate type, so there are 2 .
In this case, we will call this file registerUserCommand.ts
and will define both its type, and the function, which will be the ones getting registered in the MediatorCommandsAndQueries
type:
// File: registerUserCommand.ts
export type RegisterUserCommand = {
username: string;
password: string;
};
export function RegisterUserCommandHandler(cmd: RegisterUserCommand) {
console.log("Apply the registration user command handler", cmd);
}
We’d then register this command in a register.ts file (like we had above):
// File: register.ts
registerCommandOrQuery({
type: "REGISTER_USER",
arg: RegisterUserCommandHandler,
});
And then call it in our app:
await dispatch({
type: "GET_POSTS",
arg: {
userId: "get user Id from token",
},
});
Once you have your functions defined and the dispatcher ready, you can see magic happen by including them where you’d usually have some application logic. This could be in some express router handlers, or GraphQL resolver.
Here’s an example using Oak, an HTTP library built for Deno.js:
const router = new Router()
.get("/posts", async (ctx) => {
await dispatch({
type: "GET_POSTS",
arg: {
userId: "get user Id from token",
},
});
})
.post("/posts", async (ctx) => {
await dispatch({
type: "CREATE_POST",
arg: {
post: {
content: "the content",
title: "the title",
},
userId: "The user Id",
},
});
})
.get("/users", async (ctx) => {
await dispatch({
type: "GET_USERS",
arg: {
sort: "ascending",
},
});
})
.post("/users", async (ctx) => {
await dispatch({
type: "REGISTER_USER",
arg: {
password: "pass",
username: "user",
},
});
});
You’d see that all of your routes now have become exceptionally thin. In addition, you only have one single dependency in your route folder, clearing out the noise.
In the future, if you decide to swap your REST endpoints or the majority of them for a GraphQL server like Apollo or maybe implement a gRPC you can do them in an instant. Heck, we can go as far as to implement a voice assistant such as the Echo or Google Home using Jovo without modifying most of our code, because they won’t be tied to the framework or the infrastructure.
With this, I think we have achieved a decent level of separation of concerns between our layers!
You can use this as your starting ground, and add more complexity as your project needed it.
Please note that:
ctrl + shift + p
(Win/Lin) or cmd + shift + p
(Mac)
Thank you very much for taking the time to read this post 😊! Let me know if there are any comments or improvements to this approach 🙏!
I love to keep learning new stuff. Hopefully, this will be of great help to structure future projects!
Check me out on my other social media channels to discuss startups and tech in general. Reach out if you want to discuss the post.
Site: javiasilis.com
Email: [email protected]
IG (English): https://instagram.com/javiasilis
TikTok: https://tiktok.com/@javiasilis
Twitter: https://twitter.com/javiasilis
LinkedIn: http://linkedin.com/in/javiasilis