Introduction
One of the hard things to do in designing software is creating a simple and useful model from the complex problem domain. In this article, I introduce methods to facilitate the distilling of domain knowledge to solve complex domain problems.
A Domain Model can contain a large number of domain objects (entities, value objects, and so forth). No matter how much time we spend modeling it, it often happens that many objects depend on one another; this creates a set of relationships, and you cannot be 100% sure about the result. In this case, you should be aware of the invariants of your domain that must always be consistent. Only with that knowledge, you can reason about your system with confidence.
Invariants are business rules that must always be consistent. When we talk about invariants, we are referring to transactional consistency. It is kind of the consistency that is considered immediate and atomic. Invariants should never be violated and all invariants of the whole boundary must be satisfied. Whenever an invariant is violated, the consistency will be lost and the boundary must assert it immediately.
The other important thing you should consider in your domain model is the qualifying association between objects by preferring IDs over object references. Object references in ORM will bring you too much overhead. By sing that technique, unnecessary associations according to behavioral needs are removed. I recommend using a repository to loosely couple objects with their IDs.
In your system, the Persistence Model is responsible for what and how data is stored. Bi-directional associations in your persistence model can lead to deep object graphs and reduced runtime performance. In this case, we need to reduce bi-directional associations by changing to a single traversal direction. It’s important to know that you should keep the owner of association.
What Is an Aggregate?
An aggregate is cohesive groups of domain objects that are involved in the same business use-case(s). Its duty is to explicitly group the domain objects that are designed to support the behaviors and invariants of a domain model while acting as a consistency and transactional boundary.
Aggregates are chiefly about consistency boundaries and not driven by a desire to design object graphs. -Vaughn Vernon |
To enforce consistency, each aggregate has one root. The root is an Entity, and it is the only object accessible from the outside. The root is a gate with a global identity. Entities inside the boundary have a local identity, unique only within the aggregate.
Each aggregate must have a matching repository that abstracts the underlying database and that will only allow aggregates to be persisted and hydrated. Recently, I saw a question in StackOverflow; someone asked about the rule that aggregates can’t access repositories directly? Vernon Vaughn simply replied:
Use a repository or domain service to look up dependent objects ahead of invoking the aggregate behavior. A client application service may control this. |
Developers may misunderstand and try to inject an application service to aggregate the root. Jérémie Chassaing wrote an awesome article about this problem. He suggests execution inversion of control through delegate injection.
If you need to communicate with external aggregates, you can eventually send a copy of information to the outside aggregate. By copy, I mean it should not have any reference. This kind of communication between aggregates is used with the purpose of informing or notifying. In general, a reporting issue needs deep object graph relationships. In such a scenario, you may want to consider CQRS.
When you delete an aggregate root, you must remove all the child domain objects as well within the same transaction. Eric Evans believes:
A delete operation must remove everything within the aggregate boundary at once. |
Each part of an aggregate must have access to the latest state of other parts.
Factories
In the OOP world, a factory refers to an object that has the single responsibility of creating other objects. In domain-driven design, factories are used to encapsulate the complex creation logic.
Factories provide an important layer of abstraction that protects the client from being dependent on a particular concrete object. Exactly, in fact, it is the Dependency Inversion Principle. If the dependent object creates his own dependencies, the SRP is violated. On the other hand, factories can encapsulate internals of aggregates.
An aggregate root that provides a factory method for producing instances of inner parts will have the primary responsibility of providing its main aggregate behavior, the factory method being just one of those.
There are times when a factory is not needed, and a simple constructor is enough. Use a constructor when:
- The construction is not complicated.
- The creation of an object does not involve the creation of others, and all the attributes needed are passed via the constructor.
- The developer is interested in the implementation, and perhaps wants to choose the strategy used.
- The class is the type. There is no hierarchy involved, so no need to choose between a list of concrete implementations.
Repositories
A Repository is basically a layer that sits between the domain model and the persistence model and is responsible to manage aggregate persistence and retrieval. Martin Fowler, in his Patterns of Enterprise Application Architecture book, writes that a repository:
Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects. |
The programmer does not need to be aware of the details needed to access a database. Because the database is in the infrastructure layer, it has to deal with lots of infrastructure details instead of dealing with domain concepts. Furthermore, if the developer requests a query to run, this will lead to exposing even more of the internal details of the query than needed.
If we do not have a repository, the domain focus will be lost and the design will be compromised. So, if developers use queries to access data from a database or pull a few specific objects, domain logic moves into queries and developer code; that makes the aggregates useless.
The repository should not be open to extension (should not follow the open-closed principle). Instead, you can write a specific method to handle the job. Also, repositories do not support ad-hoc queries. It can enable your resistance model to leak into your domain model.
Finally, repositories are not good for reporting issues. It is better to use a framework to directly query a read store; this could be the same data store you use for the transactional work, or it could be a denormalized store.
How to Implement on Rails
After description part, it’s time to see how we can implement all of this. Let’s assume you already know rails. After installation, you must segregate the domain driven-design part from the rails default. To do that, add backend folders to the root of the project. It might look something like this:
Figure 1: Adding the backend folders
I prefer the CQRS approach for this, but do not hesitate to choose your own, if desired. There are useful gems that can help. Let’s list them:
- Interactor: Used to encapsulate your application’s business logic.
- IceNine: Deep freeze Ruby objects. Used to immunize value objects.
- AggregateRoot: Event-sourced aggregate root implementation.
- RailsEventStore: a Ruby implementation of an EventStore based on Active Record.
The implementation example is about an online game application. The admin can start a match between two teams and each match has a result and specifications. A team cannot participate in two matches at the same time and a team that is out of the league cannot play until coming back to the league.
The first thing to do is to create a Http port. Mine looks like the following:
class Framework::Adapters::HttpController
< ApplicationController
# other details ...
def start_match
cmd = Framework::Commands::StartNewMatch.new
({game_id: SecureRandom.uuid ,team_h: params[:home],
team_a: params[:away], date: params[:date],
time: params[:time] })
execute = Framework::Commands::Execute.new
execute.execute(cmd)
head :ok
end
# other details ...
end
Exactly, in fact, it’s a simple rails controller that I placed in the framework layer. It acts as an adapter for the HTTP port. I categorize commands of our project. The framework layer can communicate with (its lower layer) application layer. All communication must be inward (up to down). Lower layers will define protocols which let upper layers communicate. This can be done with an interface implementation, command dispatch, or publish subscribe patterns.
Every command in our project must be executed. I believe we can do some simple validation (not business rules). In the framework layer, make sure the command is valid enough to be executed. The implementation of commands and its executor are shown next:
class Framework::Commands::StartNewMatch < SimpleDelegator attr_reader :game_id , :team_h , :team_a , :match_time , :stadium alias :aggregate_id :game_id def validate! Raise RuntimeError 'sorry no valid game id detected' if (@game_id.nil? || @game_id.empty?) Raise RuntimeError 'sorry no valid team home' if UUID.validate(@team_h) Raise RuntimeError 'sorry no valid team away' if UUID.validate(@team_a) Date.parse @match_time Raise RuntimeError 'sorry no valid stadium' if (@stadium.nil? || @stadium.empty?) end end class Framework::Commands::Execute def execute(command) command.validate! dispatcher = Object.const_get('Application::Dispatcher::' +command.class.name).new(Application::Dispatcher:: dispatch.new) dispatcher.call(command) end end
After executing the command, the command will dispatch in the application layer:
class Application::Dispatcher::StartNewMatch < SimpleDelegator
def call(command)
team_repo = Framework::Adapters::TeamRepository.new
# it is the copy only , not source
command.team_h = team_repo.find_by_id command.team_h
command.team_a = team_repo.find_by_id command.team_a
with_aggregate(Domain::Model::Aggregate::Game,
command.aggregate_id) do |game|
result= game.start command
raise RuntimeError,'sorry not able to start match'
if result.failure?
end
end
end
As you might see in the preceding code, the team_repo only returns a copy of the team aggregate, not a reference to it. I mentioned earlier in the description part that an aggregate is not allowed in the body of the other aggregate, except for a copy of it that is injected via the aggregate root. You might wonder how I implement the repository:
class Framework::Adapters::GameRepository
def start_game(event)
params = event.data
return if ::Domain::Model::Entity::
Game.where(id: params[:id]).exists?
::Domain::Model::Entity::Game.create!(
id: params[:id],
team_h: params[:team_h][:id] ,
team_a: params[:team_a][:id] ,
match_time: params[:match_time],
stadium: params[:stadium]
)
end
end
The repository uses the entity to store the game details in a database. Do not forget that aggregates are not allowed to have any access to the data model. Exactly as you see in the previous code, aggregates must ask the repository to do so.
You must force your business rules in your entity, I mean that your entity must have behavior; otherwise, it’s just an anemic domain model. The following code displays an entity that forces the time of game to be in a fixed range of time. You can force your business rules as I did, but take care about leaky abstraction and let your abstraction leak out of the entity.
class Domain::Model::Entity::Game < ApplicationRecord
def match_time= value
super
# business rules go here
Raise RuntimeError,'Game Time is out of range '
if(value.hour > 9 )&& (value.hour < 14)
# and so on . . .
end
end
The Dispatch class stores the new state of aggregate and makes the aggregate accessible. You might question why I do not inherit from the Dispatch class. I believe inheritance is a bad idea for code reuse and as Sandi Metz recommends, I prefer delegation over inheritance. You may ask me about equality of inheritance and delegation. In some cases, I may agree, but the reason I do not use inheritance is more than this.
class Application::Dispatcher::Dispatch
def with_aggregate(aggregate_class, aggregate_id, *params)
stream = "#{aggregate_class.name}$#{aggregate_id}"
if params
aggregate = aggregate_class.new(aggregate_id,params)
.load(stream)
else
aggregate = aggregate_class.new(aggregate_id).load(stream)
end
yield aggregate
aggregate.store
end
end
The aggregate root part is the important part. You should assign state in the aggregate to prevent conflicts. The RailsEventStore gem will handle the event sourcing and make it easy to switch the state of the aggregate. If the change you consider is in object terms, you can roll back the object state via the Interactor gem. Let’s see an example:
def rollback context.game.destroy end
DO NOT FORGET: The interactor that fails is not rolled back. |
I place aggregate roots in DomainModel directory. This is how I do it:
class Domain::Model::Aggregate::Game
include AggregateRoot
include Interactor
AlreadyStarted = Class.new(RuntimeError)
GameFinished = Class.new(RuntimeError)
HalfFinished = Class.new(RuntimeError)
Scheduled = Class.new(RuntimeError)
def initialize(id, team_a , team_h )
@id = id
@state = :draft
@team_a = team_a
@team_h = team_h
end
def start match_time , stadium
raise AlreadyStarted unless state == :draft
match_time = Domain::Model::ValueObjects
::GameTime(match_time)
return context.fail! unless check_team(@team_a)
&& check_team(@team_h)
apply Events::StartNewMatch.new(data: {game_id: @id ,
team_h: @team_h , team_a: @team_a ,
match_time: match_time , stadium: stadium })
end
attr_reader :id
private
def check_team data
ActiveSupport::Notifications.instrument :team_add_to_game,
data do
# do your stuff here
end
ActiveSupport::Notifications.subscribe
(:result_team_add_to_game) do |result|
@result = result
end
@result
end
def apply_start_new_match
@state = :started
end
end
As you see, the Game aggregate communicates with the Team aggregate eventually. The communication is made with the purpose of notification, not modification. Indeed, the game asked the state of the team and made a decision upon it. I use the GameTime value object to clarify more about the time of the match. Here is a simple implementation of GameTime:
class Domain::Model::ValueObjects::GameTime
attr_reader :value
def initialize value
@value = value
IceNine.deep_freeze self
end
def time
@value.strftime("%H:%M:%S").to_s
end
def date
@value.strftime("%Y-%m-%d").to_s
end
def hour
@value.strftime("%I").to_i
end
def time_stamp
@value.to_i
end
def to_s
@value.strftime("%Y-%m-%d %H:%M:%S").to_s
end
def ==(other)
self.to_s == other.to_s
end
end
Let see what happened in the Team aggregate:
class Domain::Model::Aggregate::Team
include AggregateRoot
include Interactor
Currently_playing = Class.new(RuntimeError)
OutofLeague = Class.new(RuntimeError)
def initialize(id)
@id = id
@state = :draft
end
def add_to_match
context.fail! if state == :out_league
context.fail! if state == :playing
apply Events::AddToMatch.new(data: {team_id: @id})
end
private
def apply_add_to_match
@state = :playing
end
end
Done. That’s how easy and elegant it is.
Conclusion
Covering all of these definitions needs many articles, but I have done my best to summarize them here. That was only a simple example of an aggregate root, but you can create your own sophisticated aggregate, and I hope this example helped. By the way, if you are still confused with the above descriptions, you can read Vaughn Vernon’s book.