Simplest CQRS Implementation: Initial implementation
On the rare occasion that CQRS is used correctly, it can be absolutely brilliant; it offers complete decoupling of read and write operations, simplifies access to the data layer, and enables architecturally simple designs. Unfortunately though it’s often used as a wheel to break a butterfly, and it’s simplicity is lost as it’s not matched with the underlying requirements.
In this post I want to explore the simplest implementation of CQRS possible: one that I’ll write as I write this blog post. It wont be a bulletproof real-world solution complete with unit-tests or comprehensive error handling, quite the opposite! But it should act as a blueprint for just how simple an implementation can be.
To understand this post, it makes sense to also look at the associated repository
- each of the implementations outlined below are tagged with the version (i.e v1
), this is to minimise just how much code I need to duplicate here.
We’re going to write the simplest-dirtiest version we can, and iteratively improve it until it becomes acceptable; just like we would “in real life”.
Implementation One : Naive but functional
Our first implementation
is easy: we create two packages commands
and queries
, and we provide them both a connection for database operations from main.go
.
db, err := gorm.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
defer db.Close()
commands.Database = db
queries.Database = db
A Command
struct embeds the actual Model
, but decorates it with the function Do() error
. This allows us to have commands like CreateMessage
, UpdateMessage
, and DeleteMessage
, all of which can be run very simply:
command := &commands.DeleteMessage{}
command.ID = 1
command.Do()
In a similar vein, the Query
interface requires a Get()
function - and populates an internal property - Results []Model
on the struct. This allows us to perform retrievals from the database like this:
query := &queries.MessageEntries{}
if err := query.Get(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
The overall structure of this codebase looks something like this:
/
- main.go
- queries/
- - audit.go
- - messages.go
- commands/
- - audit.go
- - messages.go
- data/
- - audit.go
- - messages.go
Obviously though - we can do better.
Implementation Two : Idiomatic package structure
Whilst v1
gave us a working implementation, it suffered from one obvious issue: the directory structure was poor. It’s more idiomatic to separate code in to modules based upon the functionality rather than responsibility, so with that in mind, we’ll do some moving and refactoring. Our new directory structure
is a rather simpler one:
/
- main.go
- audit/
- - audit.go
- - commands.go
- - queries.go
- message/
- - message.go
- - commands.go
- - queries.go
Although the directory structure was an obvious issue, there’s still a few other patterns that aren’t quite right. Just like v1
we’re still injecting our database connection directly in to the submodules:
db, err := gorm.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
defer db.Close()
message.Database = db
audit.Database = db
Functionally this behaves OK, but in reality it’s only really suitable for quite simple applications. What happens if you need command/query - i.e read and write - specific connections, something that’s a major selling point of CQRS?
In a similar way, our Commands and Queries aren’t actually decoupled from the calling code. Consider this snippet:
query := &audit.AuditEntries{}
if err := query.Get(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
We’re not using the Query
interface at all: we’re creating an AuditEntries
struct and calling Get()
directly on it. If there’s no decoupling, then just what is our implementation providing us?
Implementation Three : Decoupled via the introduction of a Dispatcher
To really complete our simple implementation
, we should ideally have a system whereby we can create a Command
or Query
and then pass it off without having to worry about implementation specific details like the underlying interface or database connection. To achieve this we’re going to write a very simple dispatcher.
First of all we update our Command
and Query
interfaces, they need to be able to accept configuration settings - i.e database connections - from the dispatcher.
type DispatchableConfig struct {
DatabaseConn *gorm.DB
}
type Command interface {
Do(*DispatchableConfig) error
}
type Query interface {
Get(*DispatchableConfig) error
}
With this done, we write a very simple function - Dispatch(interface{}) error
- that checks to see if the input is a valid Command
or Query
, before calling it with our configuration struct.
func Dispatch(dispatchable interface{}) error {
cfg := &DispatchableConfig{Database}
if cmd, isCommand := dispatchable.(Command); isCommand {
return cmd.Do(cfg)
}
if query, isQuery := dispatchable.(Query); isQuery {
return query.Get(cfg)
}
return ErrorInvalidCommandOrQuery
}
There is now no need to call functions that belong to a given Command
or Query
directly; as far as the caller is aware, these are dumb structs that are only present to hold data - not perform any actions.
query := &message.MessageEntries{}
if err := dispatcher.Dispatch(query); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
Additionally, by making our dispatcher responsible for passing the database connection on, we no longer need to inject it in to every module that contains either a Command
or Query
. An example Command
can be something as simple as:
type DeleteMessage struct {
Message
}
func (c *DeleteMessage) Do(cfg *dispatcher.DispatchableConfig) error {
cfg.DatabaseConn.Delete(c)
auditEntry := &audit.CreateAuditEntry{}
auditEntry.User = "DummyUser"
auditEntry.ActionType = audit.AuditActionDelete
auditEntry.Do(cfg)
return nil
}
Finishing up#
In this post we’ve walked through an idea that’s often overly complicated, written a very simply implementation, and then refactored it to produce something more flexible and well-designed.
For the sake of tidiness, I finished up by doing a minor “re-jig” - i.e moving HTTP handlers. Our example uses HTTP for the simplicity of demonstration, but in reality CQRS is also well suited to receiving incoming events/commands from the likes of a message queue or pub/sub mechanism.