Organising Your MVC Models with Commands, Queries, & Events: A Domain-Driven Design Approach
--
In this blog post, I’m excited to share with you the culmination of over 10 years of experience working with Ruby on Rails and 4 years of building Event Sourcing (CQRS/ES) applications. What you’ll find here is the essence of the most useful elements from both the worlds of Rails and Event Sourcing, combined into a practical solution that unlocks a host of benefits and improved scalability for your software development endeavours.
Dive into this blog post to discover the Commands, Queries, and Events pattern that not only streamlines your code organisation but also naturally adheres to key SOLID and Clean Code principles, elevating your code quality and maintainability.
We’ll also explore efficient and comprehensive testing strategies tailored for this pattern, ensuring robustness and reliability in your applications.
Furthermore, learn how to successfully adopt this approach within your development team to maximise its benefits.
Don’t miss out on this insightful journey towards better software development practices and improved code bases, both for individuals and teams!
In the realm of software systems, interactions can typically be classified into two distinct types: Commands and Queries. Commands are responsible for altering the state of an application, whereas queries focus on retrieving and presenting the current state without causing any changes.
Combining these interaction types, we can observe their effective implementation within the widely adopted MVC (Model-View-Controller) web frameworks. Let’s take a closer look at how Commands and Queries manifest themselves in this prevalent pattern:
- Views serve as the HTML display of data
In views we renders pages (query), while buttons & forms usually induce a state change in the application (commands). - Controllers act as the HTTP request handlers
In controllers, you have HTTP verbs for GET requests (query), and PUT, POST, DELETE, etc. verbs (commands). - Models represent the DATABASE entities
In models, you have query scopes & json presenter (queries) and create/update methods, validations, sanitisers and initialisers (commands).
Views display & update data via Controllers.
Controllers query & trigger commands onto the Models.
Models respond to queries & commands and process events.
As a first observation, deciding where to put business logic (state change & query logic) between controllers or the models is sometimes challenging.
The trade off is as follow:
- By putting the logic down to the entity model, you make it reusable for other controllers & integrations.
- By leaving the logic up in the controllers, you can keep the code focused while not supercharging the model with isolated use-cases.
But in most cases (even for simpler application), having all possible Domain Commands and Queries listed cleanly in the model (fat model principle) help push down logic to entities and understanding the domain access of how can affect and query the data. Furthermore it will enable focused unit-tests on the models instead of the controllers.
Note that, when the command’s implementation gets complicated, the meat of the logic can still be extracted into its own CommandHandler class (aka: Services), extracting out of fat models, but in this pattern, the command will stay in the model but delegate the execution to the extracted class. We’ll see an example below.
Also note that, while this approach doesn’t protect against god objects, it does encourage the usage of the domain API instead of relying on the generic update method and other ActiveRecord magic outside of the model.
To leverage the knowledge from Domain-Driven Design & Event-Sourcing ideas, we can introduce Domain Events concept into the model. As we will explore later in this article, Events act as a missing piece, seamlessly connecting state changes to corresponding effects.
By clearly segregating the behaviour into those 3 categories, we clarify the API (Application Programming Interface) to be more focused on the domain.
By consolidating all Commands, Queries, and their emitting Events within the entity model, we effectively capture the core interactions with the world.
This approach centralises these interactions into the entity model, instead of scattering them across multiple service classes, controllers, background jobs or other entity models, resulting in a more cohesive and manageable code structure.
In short, we are introducing a pattern that accomplishes the following objectives:
- Organises all business logic into Commands, Queries, or Events, as opposed to using a CRUD API that is domain-agnostic.
- Consolidates these Commands, Queries, and Events within the model definition.
By applying this pattern, we define a Domain API for each entity model, fostering a more organised and maintainable code structure
Definitions of Commands / Queries / Events
Considering the functional building blocks of Event Sourcing as defined by Mathias Verraes here, we can outline the key components of our MVC model as follows:
We have Queries, which only return state, without changing it:
Given State
When Query
Then State
We have Commands, which change the state, and trigger Events:
Given State
When Command
Then NewState
Then Event
Then, Events eventually (usually asynchronously) get handled and call other commands, reacting to state change:
Given State
When Event
Then Command
With these, we have the fundamental building blocks of our models, with clear separation of responsibility.
Implementation concepts
So why not grouping Command related concerns together in the model, same for Queries, and define after save callbacks with domain events naming?
That’s what this post is suggesting: as a very simple pattern for MVC frameworks to clean the models. With minimal effort.
First, let’s group Command related things, that would include (from controller to database concerns):
- params validations (optionally kept in the controller)
- create & update, as class or instance methods
- datatype formatting/validations/initialisation (validations & before_save callbacks)
- uniqueness/foreign-key database validations (usually defined at the database-level, but some may be defined at the application level)
Then queries:
- active record reusable scopes
- query class methods for complex queries
- query instance methods for helpers or related object loading
- as_json/to_json definition
- API wrapper for a deeply related remote entity (debatable)
Finally Events:
- after save/commit calling
on_<this_happened>
methods on_<this_happened>
triggering or delaying other commands
Implementation in Ruby on Rails
What does it look like?
A cheatsheet version of the structure for Rails would looks like that:
class Project < ApplicationRecord
# custom table name
# associations: belongs_to & has_many
# enums definitions
# include/extend Concerns of gems
module Commands
# validations (`included do`)
# before callbacks (`included do`)
# class commands (`class_methods do`)
# instance commands
# private command helpers
end
module Events
# after save callbacks (`included do`)
# events handlers
end
module Queries
# scopes (`included do`)
# class queries (`class_methods do`)
# instance queries including as_json
end
end
With a concrete implementation:
class Project < ApplicationRecord
belongs_to :creator, class_name: 'User'
has_many :task, class_name: 'Project::Task'
has_many :followers
has_many :following_users, through: :followers, class_name: 'User', source: :users
enum status: { draft: 0, published: 1 }, _prefix: true
module Commands
extend ActiveSupport::Concern
included do
validates_presence_of :title
validates_uniqueness_of :title, scope: :user
before_save :init_project_url
end
class_methods do
def find_or_init(creator, params)
Project.find_or_initialize_by(
creator: creator, title: params[:title]
)
end
end
def title=(name)
Rails.logger.info "Updating project title: `#{self.title}` to `#{name}`"
super(name)
end
def follow!(user)
following_users.find_or_initialize_by(user: user).tap do |ref|
ref.follower = user
ref.save!
end
touch!
end
def unfollow(user)
following_users.find_by(user: user)&.destroy!
touch!
end
def publish!
status_published!
end
private
def init_project_url
self.project_url = project_name.parameterize
end
def validates_at_least_one_reference?
errors.add(:base, 'You must add at least one reference!')
tasks.any?
end
end
include Commands
module Events
included do
after_update_commit :on_project_published, if: -> { saved_change_to_status? && status_published? }
end
def on_project_published
following_users.each do
UserMailer.deliver_later(:project_pulished, self)
end
end
end
include Events
module Queries
extend ActiveSupport::Concern
included do
scope :followed_by, ->(user) { includes(:following_users).where('following_users.user_id' => user.id) }
end
class_methods do
def user_feed(user, limit: 25)
followed_by(user).status_published.limit(limit)
end
end
def published?
status_published?
end
def followers_count
followers.count
end
def as_json
{
only: [:title],
methods: [:followers_count]
}
end
end
include Queries
end
A few observations:
- Including concerns at the end of the module: This clearly identifies where the module ends and provides a unified structure throughout the codebase.
- Consistent organisation within concerns: We always define the
included do
configuration first, followed by theclass_methods do
and then the instance methods, finally concluding with the private methods. - Keeping modules within the same file: This makes it easy to find everything, as all methods are in the same file. Plus, in any IDE, you can collapse the concerns to focus on what’s relevant.
- Nothing left outside the three concerns: Apart from shared configuration, such as relationships and enums, which apply to both Commands and Queries. Neat.
- Private methods in concerns: These typically support command implementations. For simple implementations or shared behavior, private methods can be used; otherwise, extract the commands into a separate CommandHandler class.
Let’s say we have complexpublish!
command logic which is extracted into a class, we’d keep thepublish!
method in the model Command concern and call the external class like so:
module Commands
extend ActiveSupport::Concern
# ...
def publish!
Projects::Publisher.new(self).execute!
end
# ...
end
include Commands
What are Events ? And why ?
While this concept can be mixed with messages being published on message queues or webhooks, the simple idea that a reaction to a state change should have a name, a domain event name, creates many benefits.
It helps developers organise and group behaviours into a domain concepts, ie Domain Events.
Without such definition, the alternative is to trigger service or jobs based on a status_changed? && status_active? && status_was_active?
, which is ugly and abstract, instead, the code now triggers :on_project_was_activated
, which is clear.
With this, clean code principles are applied by default.
First: it has the “documentation as code” benefit.
But also, when discussing integrating other systems, we already have the Events concepts in place.
We could schedule a job to execute a webhook call on the remote system.
Second: it’s ready to integrate with other systems
Or, within our own system, trigger an event on the message queue of the system using the same name (taking the last diff as payload).
Third: it’s ready to scale to Service Oriented Architecture (SOA) & micro-services
Also, when discussing the system with the product team, stakeholders and our users, defining clear events help communication. Which is key to the Domain Driven Design principles.
Last: it’s supports the discussion & understandability of the system with stakeholders
In short, Events can be:
- a method name to organise a model after saves
- a webhook call to a third-party service
- a message on a pub-sub queue for a distributed system
- a software engineering & product concept to support conversation
And it can be all or any subset of these options at the same time.
Efficient and Comprehensive Testing Strategies
In this Commands, Queries, and Events pattern, a well-structured testing approach can significantly improve the quality and maintainability of your code.
An important aspect to consider when applying this pattern is that you may not need to write separate tests for the Commands
module if their effects are already being tested in the Queries
and Events
specs.
The reasoning behind this approach is that the Queries
module provides the only means to retrieve the current state of the system. Since the goal of the Commands
module is to change the state, it becomes crucial to ensure that the expected side effects of these changes are adequately tested through the Queries
.
Similarly, the Events
module deals with triggering various actions as a result of state changes. It is essential to test that the expected behaviors are executed correctly when certain events occur. As a consequence, Commands
can also be tested through the test suites of the Events
module because they’re used to define the setup state for the tests.
By concentrating on testing the Queries
and Events
specs with the setup state provided by the Commands
, you can effectively verify the correct functionality of the Commands
module without the need for explicit additional tests. This approach leads to a more efficient and organised test suite, as well as a clearer understanding of the connections between state changes, data retrieval, and triggered behaviours in your code.
Let’s see this in action in the Queries spec:
RSpec.describe Project::Queries, type: :model do
let(:user) { create(:user) }
let(:project) { Project.find_or_init(user, title: 'Test Project') }
before do
# Set up the state using Commands
project.follow!(user)
project.publish!
end
describe '.user_feed' do
it 'returns projects followed by the user and are published' do
feed = Project::Queries.user_feed(user)
expect(feed.first).to eq(project)
end
end
describe '#followers_count' do
it 'returns the correct number of followers' do
expect(project.followers_count).to eq(1)
end
end
end
In this example, the before
block sets up the initial state by using the follow!(user)
method and publishing the project. The user_feed
query method is then tested in the describe
block to ensure it returns the published projects followed by the user. Additionally, the followers_count
method is tested in a separate describe
block.
And the Events specs:
RSpec.describe Project::Events, type: :model do
let(:user) { create(:user) }
let(:following_user) { create(:user, email: 'following_user@example.com') }
let(:project) { Project.find_or_init(user, title: 'Test Project') }
before do
# Set up the state using Commands
project.follow!(following_user)
end
describe '#on_project_published' do
before do
allow(UserMailer).to receive(:deliver_later)
end
it 'sends notifications to all following users when the project is published' do
project.publish!
# Expect that an email is sent to the following user(s)
expect(UserMailer).to have_received(:deliver_later).with(UserMailer.project_published, project)
end
it 'does not send notifications when the project is not published' do
project.save! # Saving without publishing
# Expect that no email is sent
expect(UserMailer).not_to have_received(:deliver_later)
end
end
end
In this example, the before
block sets up the initial state by using the follow!
method to add a follower. The on_project_published
event is tested in the describe
block to ensure that notifications are sent to following users when the project is published and not sent if the project remains unpublished. The test checks whether the UserMailer
receives the :deliver_later
message with the correct arguments when the project is published.
While focusing on testing the Queries
and Events
modules with the setup state provided by Commands
can be a practical and efficient approach, there can be instances where you might want to unit-test validations and before_save
callbacks in a separate command_spec
file.
Command specs are useful for sanity checks and testing more complex branching situations. But in such case, you’ll probably test the CommandHandler class into which the implementation has been extracted to. In these cases, you may want to bypass any queries, except for the essential find
query method, and ensures that the appropriate state is persisted on the record as unit test.
In conclusion, by applying these testing strategies, you can develop a thorough and comprehensive test suite that covers all aspects of the Commands, Queries, and Events pattern, ultimately enhancing the quality, maintainability, and robustness of your code
AggregateRoot
It's crucial to understand that this pattern is most effective when applied to Aggregate Roots within a Bounded Context, rather than all models indiscriminately.
Aggregate Roots are the primary entities that encompass several related sub-entities and govern interactions with and between them.
By focusing on these central entities, the pattern simplifies and hides the complex relationships and rules governing the domain's sub-entities, consequently streamlining the management and maintenance of the involved components.
This targeted approach ensures that the pattern is applied effectively where it adds the most value, leading to more organised and maintainable code.
Team adoption
While existing code provides a foundation, it’s crucial to be open to new patterns that could make our job easier and improve our development process.
This pattern can be successful only with team adoption. To facilitate this, we have initiated code guideline meetings within our backend team. These meetings create a collaborative space for discussing code and structure, enabling us to align on terminology and a shared direction.
We all enjoy talking about code, and through these discussions, we work together to determine the best patterns to adopt collectively.
To promote the Commands, Queries, and Events structure to your team, follow these steps:
- Educate your team: Explain the benefits, downsides, and rationale behind adopting this structure. Share resources and examples.
- Create documentation: Provide guidelines on how to implement this structure with examples from your codebase.
- Workshop or training: Organise sessions for team members to practice utilising the structure with pair-programming or group exercises.
- Code review process: Include the structure in your team’s code review process and encourage feedback on its usage.
- Standardise the structure: Incorporate the structure into your team’s style guide or coding standards for consistency.
- Update existing code: Gradually refactor existing code to conform to the new structure.
- Lead by example: Consistently use and promote the structure in your own code, setting an example for the rest of the team.
- Evaluate and iterate: Assess the impact of the new structure on productivity, codebase quality, and maintainability. Gather feedback and adjust as needed.
Conclusion
In conclusion, this pattern offers several advantages:
- Easily refactored: Code can be adapted to fit this structure by mainly rearranging existing elements.
- Reversible: Changes can be quickly reverted if needed.
- Grouped private methods: Commands’ private methods are organised together.
- Intuitive: The structure is simple to understand, making sense to developers and stakeholders.
- Low friction for adoption: Developers can easily adopt this pattern with minimal resistance.
- “Skinnier” model: Collapsible modules contribute to a more streamlined and tidy model, allowing developers to focus.
- Introduces Event concept: Incorporates the beneficial Event concept, as discussed earlier.
- Supports service concept: Natively supports the idea of service, reframed as CommandHandler, which delegated from the model keep the complete API within the model even when command’s implementation gets extracted.
- Introduces a domain-specific language: This pattern moves away from the generic CRUD language and establishes a domain-specific language within the system. This enhances the expressiveness, clarity, and maintainability of the code by aligning it more closely with the business requirements and real-world processes.
- Simplified background jobs as asynchronous commands: With the centralization of logic within the model behind commands, any background job effectively becomes a call to the command. This eliminates the need for custom job classes, streamlining the process with two generic job handlers: a class command job handler and an instance command handler.
- Encourages the use of the domain API: By implementing this pattern, the reliance on generic
update
methods and other ActiveRecord magic outside the model is reduced, ensuring that the model serves as the gatekeeper for its own access and interactions via it’s limited domain API. Protecting against god objects. - Adherence to SOLID Principles: The Single Responsibility Principle (SRP), the Dependency Inversion Principle (DIP) (via Events), and the Open-Closed Principle (OCP) (via Events) are natural principle when following the pattern.
- Adherence to Clean Code Principles: Meaningful names, functions that do one thing, keeping functions small and focused, using a consistent coding style, separating concerns, encapsulating behaviour (via CommandHandler class), and creating code that is easy to read and understand are all naturally followed by the pattern.
The in-depth discussion of the last two bullet points, examining the intricate relationship between the SOLID and Clean Code principles and the pattern presented here is certainly worth exploring in a dedicated blog post.
As this post is already quite comprehensive, we will leave it as it is, providing an excellent starting point for future discussions on the subject. Stay tuned for that upcoming exploration!
In conclusion, I would like to thank my readers for taking the time to engage with this content. Your interest and support are truly appreciated, and I hope you found the information valuable and relevant.
As we continue on our development journey, let’s keep sharing ideas and fostering a collaborative atmosphere.
Thank you once again, and happy coding!
Want to Connect?
Follow me here on Medium ☝️
Resources
- A Functional Foundation for CQRS/ES by Mathias Verraes ❤
- Domain Drive Design is linguistic by Mathias Verreas
- Domain Driven Design wikipedia
- Service Object in Rails by TopTal
- Command vs. Event in Domain Driven Design
- Domain events as a preferred way to trigger side effects
- AggregateRoot by Martin Fowler
- EvansClassification by Martin Fowler
- SOLID Principles
- Clean Code Principles by Bob Martin