Thursday, October 13, 2016
Domain Driven Design Through Onion Architecture
architecture
this is a technique that i started using a few years ago and it's a technique
that I feel since i started using it has increased my code quality dramatically
in particular domain driven design was sort of the kickoff . for starting down
this path but it wasn't really until I started learning onion architecture in
conjunction with domain driven design that i really started to feel like my
code quality was improving dramatically suddenly things were far more readable
far more understandable and far easier to maintain
so throughout this discussion we're going to use the example of bowling
score sheet if you've ever been to a bowling alley you may recognize one of
these images or you may have seen something similar to them at the very
least
so it's an example that most people will probably be able to look at and
understand to some extent it's also something that's got a reasonable amount
of depth to it so it's a good example to choose for exploring domain-driven
design
so what is domain-driven design while to answer that first we have to answer the
question what is a domain the domain is a sphere of knowledge for most software
domain refers to the business knowledge that your software is trying to model
ok so based on that what is domain-driven design
this is a technique that emphasizes cooperation between technical experts
and domain experts domain experts can also be called business experts
these are the people who know about the business rules that you are trying to
model and this could be people like marketing specialists account executives
office managers etc they may or may not have any technical knowledge
these are the people like I said who have knowledge of the business they
don't necessarily have knowledge of the technology now when dealing with the
domain experts as a technical expert is very important to have a language that
you can communicate in so that everybody understands what you're talking about
and
call that language the ubiquitous language the ubiquitous language
basically is taking the real world business rules and the real world
business items and reflecting them directly in code
it's it's a shared language that can be used by both the developers and the
domain experts and everybody will understand what you what you're
referring to
so some examples of this might be things like a customer or a purchase or in our
bowling example it could be a player or bowling pin
so let's talk more specifically about our bowling domain
well who r domain experts in the bowling example this is going to be people like
the alley staff or the players if there is a professional bowlers involved that
may include coaches and it could even include the maintenance staff
well then what is the ubiquitous language in our bowling domain
well there's going to be a lot of terms in a bowling domain but let's just give
a couple of examples i mentioned before things like player and pin
there's also a ball spare strike split pin center and again these are terms
that anybody who has knowledge of the domain will be able to understand
so if we write our code using these terms then we will be able to speak to
those domain experts about our code and have them understand what we are trying
to say one of the
problems when trying to model the domain is that oftentimes domains can be quite
large so even if we look at our bowling domain there's a lot going on there
potentially you have all the terms that i mentioned like pin and player etc but
if you start expanding that to the bowling business then you run into
situations where ok now I've got a deal with customers as well because we've got
a cell
we've got to sell products to our customers and then you also have to deal
with other things like you may have maintenance concerns that have to be
dealt with and all of that gets very cumbersome and very difficult to deal
with
so what we do is we split our domain into something called bounded context
and bounded context allow you to subdivide the larger domain into smaller
domains each of which can have their own ubiquitous language and their own model
some concepts may be shared between the bounded context but they may actually
look very different from one context to the next
so looking at our bowling example we might have a bounded context just for
scoring and so this is the bounded context that we've chosen to use as our
example but a scoring bounded contacts deals with only a single game
now you may have a league of bowlers who need to keep track of their statistics
across multiple games and so for that you might have a separate statistics
bounded context then you also have to have lain reservations so that might be
another bounded context which might also be separate from the sales bounded
context and then we could also have a maintenance context
one of the keys to this is that some of the concepts in one context might appear
in another context
so a player for example may appear in the scoring context but it may also
appear in the statistics context however that the player in a scoring context may
look very different from the player in a specific statistics context and the
rules surrounding that player might be very different
now when building a domain using domain-driven design there's different
architectures you can use theirs
for example the traditional layered architecture and the traditional layered
architecture has multiple layers
usually there's at least three layers that are present that and then those may
be subdivided into into smaller layers as well so the two major or sorry the
three major layers that are generally present is the presentation layer and
then the business or domain layer and then the data access layer
the basic idea though is that your user interfaces on the top and your databases
on the bottom with the domain in between onion architecture takes a slightly
different approach
I don't want to be clear here and say that you can use traditional layered
architecture to build applications using domain-driven design you can use other
architectures if you want to build using domain-driven design onion architecture
just happens to be one of the choices that you could make and it happens to be
a reasonably good one
so onion architecture I have also heard it called other things like ports and
adapters and hexagonal architecture
realistically I think ports and adapters and hexagonal architecture are super
sets onion architecture is sort of a subset of those those other
architectural styles you can and as I said domain-driven design and our onion
architecture are not necessarily the same thing you can you could actually
potentially use onion architecture with out using domain-driven design although
some of the concepts of domain driven design obviously have to be present in
the domain layer of the onion
this is a another simplified is a design
there are other onion architecture diagrams that you will see which will
present additional layers much like with the layered architecture each layer can
be subdivided into into smaller pieces
the other thing that I want to mention is that I i have traditionally seen this
as consisting of four layers the infrastructure
then there is a services layer that or application services layer and then a
domain layer and a core layer now I personally don't like the term
application services and the reason for that is because domain-driven design
also has a concept of a service and it is subtly different from the concept of
an application service so it gets confusing when you're trying to talk
about the the services and you're not you have to always qualify to say I am I
talking about the application services or am I talking about the domain
services so i think it's more straightforward to simply call that
services layer or that application services later
api and I think it also represents what that layer is trying to do
one of the things that I like to do when building my code is that I like to
represent these layers in through the use of packages or libraries so in my
code i will have a package named API and a package name domain a package name
core and a package name infrastructure
the other thing that i should point out is the arrow that points from the
infrastructure to the core
that represents the fact that each layer can see the underlying layers but the
inner layers have no visibility or knowledge of the outer layers so when
building using onion architecture
you start off with the centerpiece the core and the core is basically the
building blocks that you use to build your application
it's not specific to any particular domain or technology it includes things
like lists Maps in skala you might have case classes you might have actors
lenses but these are not necessarily I mean I mentioned a lot of scalloped
terms just because that's what I'm used to but this this can be done in other
languages as well you can do it in c-sharp you can even do it in
non object-oriented languages although in an object-oriented language you have
to get a little bit more creative with how you define things but it is still
possible
one thing to point out is that the core will never include technological
concepts like rest or SQL or database
these are these are concepts that do not belong in the core and the other thing
is that the core in fact can have no knowledge of any of the other layers so
it doesn't know anything about your domain it doesn't know that your domain
even exists or that your API exists or that your infrastructure exist
alright so i'm going to skip one layer and go one layer higher to the API and
I'll explain why in a moment but the API is basically the entry point into your
domain and it should use domain terms
it should also only expose immutable objects
the reason for this is because it prevents people from using the API to
gain access to a back door to the domain so if you had returned mutable objects
through your API then you're the people using that code could gain access to
parts of the domain that you perhaps didn't intend to expose by providing
only immutable objects it prevents them from manipulating the domain except in
the ways that you intended
the API has access to the core and the domain but it doesn't know anything
about the infrastructure
now the reason that I skip to this layer is because often this is where i will
start writing my code so i will write an API method usually just a skeleton and
then i'll write a high-level functional tests around that method once I've done
that then I'll start fleshing out all of the logic that I need in order to make
that high-level functional tests pass and that will drive me to flesh out the
domain underneath
I'm a at this point while I'm writing the API I may write a few small
skeletons in the domain so i may just have a couple of quick class definitions
maybe even a method definition or two but i likely won't have any logic in
them at this point
that will be when i start fleshing out the domain and writing the test around
the domain that i will add the logic
so let's talk about our bowling API what's that going to look like
well it's good probably going to have some methods in it like create game
add or remove player get players set Penn State skip player set lane
these are all things you might find in the bowling API layer
now let's talk about the domain so stepping down one layer from the API
again I'll classes and methods in the domain must be or should be named
according to the ubiquitous language
so this is where you're going to see classes that are named things like
player or game or pin
there are some exceptions to that there are some domain driven design specific
terms like repository or service and you may see them present in here as well but
generally speaking you should try to name what all of your objects in your
domain
according to the ubiquitous language
the key thing to point out here is that in the domain is where all of your
business logic goes
so if you have any business rules that they belong here
they don't belong in your database they don't belong in your API they belong in
the domain
the reason for that is that by controlling your domain through the API
and by putting all of your business logic into the domain that means that
your application is portable you can extract any of the technology bits
without losing any of your business logic
so let's talk about our bowling domain we've talked about a lot already but
let's talk about it more specifically in the context of us are scoring
application so in our scoring application or in our scoring context
will have terms like game player in frame strike spare these are all aspects
of the scoring context
now there are other bowling terms that would appear in the overall bowling
domain but may not appear in the scoring context
this would include things like pinsetter customer lane reservation then there are
also non domain terms these are things that would belong in the infrastructure
and that is things like user interface and database so in our domain for
bowling we might have a class which would be called game and it might have a
method on it like add player and our player than might have a class called
player with a method called set name
so those are examples of some of these things you might see in a bowling to me
now this isn't required but one of the things that I like to do when building a
domain as i like to avoid having primitives in my domain
so for example instead of having a player named be a simple string
I would have a container class 4 player named this provides me a home for
validation logic if i ever need to add any and it also provides me strong
typing on those fields now it
Scala is actually a particular particularly good language for doing
this kind of modeling because of the presence of things like case classes
it makes it very easy to make those little wrapper classes
it becomes even easier when you start talking about implicit classes and
implicit methods now when building a domain we have a few building blocks
that we can draw upon this includes things like value objects entities
aggregate roots repositories factories and services
so let's talk about value objects value objects are immutable they have no
identity to out to value objects of the same type with the same attributes are
considered to be the same
they're often used for things like message passing and in fact they are
particularly useful in the API to expose your domain concepts without necessarily
exposing the mutable aspects of the domain
next we have entities and entities are potentially mutable
in an entity's case it is actually uniquely identified by an ID rather than
by its attributes
what this means is that the state of the entity may change but if you have two
entities of the same type with the same ID then they're considered the same
regardless of what other attributes they have now an aggregate route is a an
entity that binds together other entities
one of the key features of an aggregate route is the external objects are not
allowed to hold a reference to an aggregate roots child entities
so if you need access to one of the aggregate roots child entities that you
must go through the aggregate route
the other thing is that all operations in general on the domain should where
possible go through an aggregate Groot
there are some exceptions to this factory's repositories and services are
exceptions but wherever possible if you can create if you can require that an
operation go through the agar your route
that's going to be better for your domain now repositories
these are actually one of the more important concepts in domain driven
design in my opinion and the reason is because they have they helped to
abstract away a lot of the storage concerns and such that could otherwise
pollute your domain
repeat now a repository could be a file-based it could be a database it
could be memory it could be rest api s or it could be any combination of those
the important thing is that they're just a way to abstract away from some some
form of storage now a repository should not be confused with the data store a
repositories job is to store aggregate roots underneath that repositories
implementation may actually have to talk to multiple different storage locations
in order to construct the aggregate so a single aggregate route may be drawn from
a REST API as well as a database
as well as files and all of those might need to be read in order to construct
that a new route and so you may you may wrap those in something called the data
store but the repository is sort of a further layer of abstraction on top of
all of those individual data stores usually the repository is going to be
implemented as a trait or an interface and then the logic for the repository is
going to be defined in the infrastructure the factories are similar
to repositories
now in the case of factory they abstract away new object construction and a
factory could potentially return an aggregate route or it could return an
entity or perhaps a value object
often times when you need a factory method for an aggregate root it will be
rolled into the repository so your repository might have a finder create
method on it and again like a repository a factory is often implemented as a
trait or an interface with the logic defined in the infrastructure now
mrs. are a little different in that a service is basically there to provide a
home for operations that don't quite fit into an aggregate route
so if you have a situation where you have a you have an operation and you
can't decide which aggregate route that goes into perhaps it actually operates
on multiple aggregate roots or maybe it just doesn't fit into any of your
existing regular routes in that case you can put that operation into a service
however we should not be
we should be careful not just to jump to putting everything into services if you
find yourself in a situation where you have an operation and you don't know
what aggregate route to put it into the first thing you should ask yourself is
am I missing an aggregate route and try to figure out if perhaps there are
domain concepts that you haven't considered that should be brought into
your domain so let's talk about the aggregate root for bowling
so in our bowling domain we have a few candidates for aggregate root out there
may be others but for the time being let's just narrow it down and say that
we we're going to look at the possibility of player and game being a
negative your room well how do we decide which one is the a group your route
well one question we can ask ourselves as which entities are going to be
present in most or all of the API operations
is there a single entity or is there multiple we may have a situation where
there's actually multiple entities present in multiple operations in which
case we may actually have multiple aggregate roots on the other hand we may
look at our domain and say you know what this one entity is present in every
single operation and that might make it a good candidate for an aggregate route
that doesn't necessarily guarantee it's the aggregate route but it's a good
candidate anyway so let's look at our two examples player and game well some
of the operations in our domain would include add player set lane set pin
state etc
now each of these is going to have to involve a game you need to add a player
to a game and you need to set the lane for a game and you need to set the Penn
State in that game so game is definitely an interesting candidate but let's not
rule out player just yet
so another question we can ask ourselves is which entity if deleted would result
in the deletion of some or all of the other entities
well if we delete a player does the game still exists
well we do know that a game can have multiple players so certainly deleting
one player from the game is not going to delete the game but if we deleted all
players from the game
that certainly might delete the game so there's a possibility that deleting all
the players with the leave the game
well let's flip that on its head if we delete the game does the player still
exists now
your instinct might be to say yes the player does still exist even when the
game does not but the key here is not to confuse a person and a player
so yes if we delete a game the person still exists but if they're not assigned
to a game
are they really a player i would probably say no and therefore i would
say that if you delete the game then you delete the player as well and because of
that i would say that game is a very good candidate for next for an aggregate
route
not good enough candidate that if i were to build this right now i would probably
start with that and see if i have to add extra aggregate roots later or see if it
starts to get more complex and that's one of the other keys is as you make as
you take the steps through this if you ever reach a point where something
starts to feel awkward
then it might be time to reevaluate your domain and deter determine are you
missing an aggregate route or have you perhaps chosen the angry but for now we
can just say that the game is the group
alright so let's step into the infrastructure
now the infrastructure is the outermost layer of the onion and the
infrastructure includes adapters for various things like databases user
interfaces and external services like rest api etc it will have access to all
areas of the API the domain and the core
although generally speaking most operations accessing the domain should
go through the API the exception to this would include things like domain
interfaces that have infrastructure implementations
so speaking of our bowling infrastructure
well we've got concepts like a pin center now we've talked about the
pinsetter as a domain concept in bowling but keep in mind that we are limiting
ourselves to a very specific context and that context doesn't necessarily require
a pin center so in our domain a bowling expert will know what a pin setter does
and in the overall bowling domain perhaps it belongs there
but from a score keeping perspective you don't really care how the pins get reset
in reality you don't even care if they get reset as long as someone tells you
whether the pins are up or down you can calculate the scores and it doesn't
really matter too much how they got there
so in our scorekeeping context we can consider the pinsetter to be a part of
the infrastructure
even though it might be part of the overall bowling domain
another example of bowling infrastructure might be something like
the user interface for the lane screens we saw in the beginning there were four
different user interfaces
now we shouldn't have to be tied to those user interfaces
if if we want to swap out a different user interface that shouldn't require us
to affect anything in our API or our domain and so that's why that belongs in
the infrastructure
and database is another good example we might start out with an SQL database and
then something changes we decide to use Mongo or Cassandra or we may even decide
that we don't want a database at all we want to file system and we should be
tied to those in any way inside the domain or the API and so again that's
why the database layers belong out in the infrastructure
ok so let's put this all together now so assuming we've built our bowling domain
and we've done it
well using a domain driven design and onion architecture architecture concepts
let's go through a few different scenarios so what happens as i just
mentioned what happens if a new database technology comes along that you feel
would be better suited to your requirements for a bowling application
are you free to use it or if you tied yourself to the technology
another question we could ask ourselves is what happens if a company wants you
want to use your score keeper application but for whatever reason they
don't have automatic pinsetters maybe they use people to reset the pins
perhaps they have their in this nostalgic bowling alley and they want to
do everything manually so they have people doing all that work while
are you going to be able to simply drop the portions of the infrastructure that
are tied to pin setting and continue to use your domain and your API or have you
tied yourself to the notion of the pinsetter well what happens if that same
company perhaps requires that bowlers
or maybe those human pin setters I have to input the state's manually of the
pins so that scores can be calculated
can we adapt to this or let's go way out on a limb and say what if you're bowling
system became so popular that a video game publisher asked you to develop a
bowling game for them
would you have to completely redo implement the scoring system or could
you relatively easily reuse that existing code if we have carefully
follow you followed domain-driven design and onion architecture principles
then it should be maybe not trivial but it really should be possible to do all
of these things