Video summary
Gary Bernhardt presents a talk titled "Deconstructing the Framework," which advocates for a minimal web application architecture designed to scale and maintain clarity over time. He argues that standard Rails applications often suffer from tight coupling between Active Record models and external systems, such as email servers or APIs, leading to fragile tests where stubbing becomes necessary just to keep existing code running. To solve this, Bernhardt proposes separating concerns by introducing stateless service objects that handle interactions with the outside world while keeping database records thin and focused solely on data persistence. This approach involves creating simple wrappers around third-party libraries and external services, which not only isolates business logic but also provides stable interfaces for testing without needing to mock complex Rails internals or worry about breaking changes in future versions of the framework.
The presentation further introduces a design rule called "the squishing rule," derived from the Single Responsibility Principle (SRP), suggesting that splitting large classes into smaller ones almost always results in better-designed code, even if the split is not perfect. Bernhardt demonstrates how to decompose traditional Rails controllers by extracting responsibilities such as authentication, authorization, database reads and writes, HTTP handling, and routing logic into distinct components like presenters, entities, services, and injectors. By pulling these concerns out of the controller, developers can create a system where each piece has a single reason to change; for instance, validation errors become view concerns rather than model responsibilities, and complex state transitions related to user permissions are handled by the router based on the current world state rather than scattered throughout action methods.
To illustrate that this decomposition is practical beyond theory, Bernhardt introduces Raptor, an experimental web framework he co-developed specifically as a proof of concept for his architectural ideas. Unlike Rails, which relies heavily on inheritance and DSLs (Domain Specific Languages), Raptor uses plain Ruby objects with explicit arguments injected via a simple name-based system rather than a heavy container or configuration files. The framework demonstrates that complex behaviors like creating resources can be achieved by routing to arbitrary methods within service objects, effectively replacing the need for "fat controllers" and "skinny models." While acknowledging that Rails has its own merits through meta-programming and DSLs, Bernhardt concludes that developers should strive to insulate their core business logic from the chaotic nature of framework updates by maintaining a clear boundary between simple Active Record layers on one side and stateless services handling external dependencies on the other.
Read the full video transcript
[Music]
you guys ready um
i'm gary bernhardt i look like this on
the internet i look like this
in real life sometimes i run a company
called destroy all software that
produces screencasts on various advanced
development topics
and the title of this talk is
deconstructing the framework
it's about web frameworks i use rails as
an example it's not rails
specific and it has five
different parts the first of which is
design in the small so the design of
small fine-grained pieces and i'm going
to use one very specific
example i'm sure you've seen a code like
this
in rails apps before i have a comment
model active record model
it has an after create callback that
notifies all commenters so this is the
behavior that
github just has for example you comment
on a gist
and for for the rest of time you will
receive notifications and you can't turn
them off
sadly you guys should fix that
the the problem with designing things in
this way is that you end up with a
system where all your tests are testing
some
system under test the the systems under
test are using model objects
and the model objects want to send email
or want to hit an external system of
some kind whatever that system is
rails happens to turn email off for you
in tests so email's not a big deal but
other external systems are a problem so
you end up first of all having to stub
the external system
to create your model objects which seems
extremely wrong
to have to do that and you also end up
with this situation which i'm sure you
guys have seen before
you break the external system and
suddenly all your tests break because
they all depend on this thing that
really wasn't relevant to the test
or to the model object the fundamental
problem here is that the record is
coupled directly to the api the active
record object is coupled
to the api and the obvious solution as
with anything in software is to add a
layer of indirection
so if we create a service that
aggregates the the process of
of creating a user or creating a
whatever
it can make the record it can hit the
api it contains that logic
and here's the same example done that
way so
this service posts comments it creates
it in the database
it then for each of the commenters in
the thread it emails them
and now the activerecord object has no
coupling to the external system to
the email server this is
a small sort of obvious example but it's
going to motivate
the rest of the talk starting with
services wrappers and records
this is sort of the minimal web
application architecture that i think
makes sense and can scale you to what we
would probably consider a medium
large rails app which of course in the
grand scheme of things is not that big
the idea is that you have services
things like i just showed you stateless
objects with behavior but no data in
them the services will talk to records
these are the things in the database
and the records are very simple and
they're they're very
thin layers over the actual database
itself so for example
here's what i'd like an active record
record to look like this is an example
taken from the destroy all software
sucks rocks series of screencasts
it's a cash score this is a system where
you type in a word and it tells you how
good it is based on what the internet
says so if you type ruby in
it will google for ruby rocks and ruby
sucks
so very english centric and it will
compute a score from from the relative
numbers of results
i have two methods in it only the first
of which saves a new score
it's a very thin wrapper around create
as you can see but it narrows the
interface so it takes exactly two
arguments they have to be there
there's no ambiguity it also gives me a
point that i can stub
in my tests that i control so i'm not
afraid to stub it or i'm less afraid
to stop it and it hides any changes in
rails that might come
now create bang probably is not going to
change but if you think about rails 2 to
rails 3
the whole query api changed a lot and
basically
everyone was screwed if you had a rails
app at that time i'm sure you remember
it sucked because
you probably had controllers that were
calling the querying api query
query api changed bad times
so this prevents that as well in the
future and the
the second method there is uh the query
method speaking of queries
it finds a single term and returns the
score and the interesting about this
interesting thing about this is that
it's a function from a string
to a floating point it does not return
any model objects in fact
the rest of the system never actually
sees a cache score
it sees the numbers it sees the the
terms that it asks for but it never sees
the actual active record object
in general i want my active record
objects to load things save things
update themselves so like change
password might be a method i would put
on there it might have to change
multiple fields at the database level
but
it provides a slightly higher level
update and then very simple aggregate
functions like
full name is first name plus last name
so those are records and the third thing
is wrappers around third party apis so
in that same system that i built for
those screencasts
i had a search engine class and this is
how i search for those
phrases it's an extremely simple wrapper
around our bang which is a ruby
library two lines of code as you can see
but
these two lines of code give me so many
benefits for the design and especially
the evolution of the system
it gives me a place to stub in my tests
if i want to it protects me from changes
in our bing
it also allows me to swap the entire
search engine out if i want to for it
for google or whatever
so there's all kinds of advantages to
writing even these two small lines of
code
and finally in this very simple
application architecture services can
call other services so they can be
aggregates and you have a tree of
services
is what ends up happening so this is the
simplest way i know
to design an application that will
grow reasonably over time it won't turn
into a ball of mud
with your thousand line controllers or
if you read
skinny controller fat model your
thousand line model which has exactly
the same
design problems which
speaking of thousand line things brings
me to part three
the squishing rule which i'm going to
motivate with everyone's favorite
principle the srp
often quoted rarely understood a clash
should have one and only one reason to
change so i thought we would go through
very briefly and look at the reasons to
change that your typical rails
controller has
they tend to do authentication so that's
a reason to change
they tend to do authorization which of
course is different that's a reason to
change
they tend to wrap http in both
directions so coming in you have params
and headers going out you have status
code and headers
reason to change they tend to manipulate
the model
calling methods on activerecord objects
that you defined that's your model
they tend to manipulate the database
which is actually a different thing
if you call the reload method in a
controller that's not
model your application conceptual model
doesn't have reload in it reload is a
database idea and it's an idea that not
all databases even have
so the controller if it calls reload has
knowledge about the nature of the
underlying data store
so another reason to change query they
often query the database controllers do
which means they understand the
structure of the database and and the
way the data is organized
they often present models and that could
be something as simple as computing
aggregate values from single columns or
it could be something like group by
where you're aggregating
several objects
they often contain view content so a
flash is content pushed into the view to
be rendered
they often contain response content
which is for example rendering json that
is actual literal values that will be
output to the
client they often do routing not on the
way in
hopefully but on the way out conditional
redirect is routing
they often choose the content type and
they often
contain model logic which is the most
sinful of all of these
you should never have model logic in a
controller in my opinion
so the single responsibility principle
says the class should have one and only
one reason to change and
this is the one reason for a controller
to change
which doesn't seem right to me
that does not seem like good design and
i'm not a fan of treating the srp or any
principle as
an absolute rule but as a guiding
principle it seems
seems to indicate something is wrong so
the squishing rule
which i propose here to you as a
certainly a much simpler to an easier to
understand idea than the srp
is that if you take a big thing and you
just cut it in half the small squares
are exactly half the
area of the big one don't worry if you
take a 400 line class
and split it into two 200 line classes
probably the 200 line classes are better
designed even if you split it
poorly even if you choose the wrong line
to draw
they're probably still going to be
better designed
so that's part three of the talk we've
so far had this minimal architecture for
rails app
and we've had this rule that i basically
argue by
argue by i don't know my own my own
intelligence that
if you squish something those pieces
will be better so now i want to put
these two together and consider
uh the decomposition of a controller
especially in rails app but really just
the whole rails app what is it actually
doing if you look at the things rails
gives you
this is basically what you have putting
things like helpers aside
things come into the route things go out
of the template and in between you have
a controller and a model
and this makes it very obvious uh why
there have only been two
architectures that were popular
originally there was fat controller
skinny model and then it got inverted
but now what there's no other option if
you only have controllers and models
so let's start decomposing and figuring
out what is actually going on in here
well presenters have become popular
they're really just object decorators
rails culture has this weird thing where
it uses the wrong words for everything
but i like the idea it's cool so let's
put it in there that was something the
controller used to do
in 2006 let's say it was very common
and now we pull it out into a separate
object the
model here is really we can really
separate the reads and the rights
and suddenly we've exposed boundaries in
the system that were not obvious before
reading is actually a boundary
and writing is a boundary and
furthermore you can actually do the
reads in many cases based purely on the
route
so based on the action you route to
before you call the action
you could theoretically pull all the
data out of the database that it needs
give it to it let it do its thing and
then when it's done
write it all back it's a perfectly
reasonable thing to do and it's sort of
what the unit of work pattern
is meant to do of course you need
entities so let's put the entities in
between those are the active record
objects
you can do the same exact thing for http
if if a route is matched you can in
almost every circumstance immediately
pull out all the
headers it's going to need the params
it's going to need and pass them into
the controller the controller doesn't
need to know about them
because they're for for 99 of controller
actions the headers and params
request are used are the same in all
cases
and then after the controller is done we
could do a write a high level write
so a high level http write would be
something like redirect 2.
that's a pretty high level idea it
translates into a status code 304 i'm
guessing
and a location header but you don't say
304 and location header you say redirect
2.
continuing on to in decomposition
i've become confused what changed oh yes
so when you pull out http stuff and
database
stuff from the controller you really
just have logic left
that's sort of what you have it's like a
service class or it's like an interactor
if you've seen
uncle bob's talks about this kind of
stuff
so continuing to decompose uh there's a
value being
communicated between this logic this
controller and the presentation so let's
put that in here just to be clear that
it's there and uh the http write can
come out of that
it's sort of straightforward obvious
kind of thing
and finally this is the really weird one
but
i have this thing that i'm kind of
obsessed with which is
routing is not just about verbs and urls
it's about
the state of the world relative to the
user making the request
so i really think that authorization is
a routing concern
i think that the router should i should
be able to say to my router
this url or this route matches verb x
url
url y and state z of the world
and state z might be users and admin or
user is authenticated
so that's another thing we can pull out
of the controller
and you can see this is a heck of a lot
more complicated
than what we started with or at least it
looks that way there's a lot more pieces
but all these pieces are there
these are all things you're doing it's
just that if i color this differently
the dark parts are what you normally put
in a controller in traditional rails
design if you do
a skinny controller fat model it changes
slightly but not that much
so all these different things are going
into the controller
which is sort of unfortunate and
people when i tell people this they have
a hard time imagining
how i would fix it what what what would
the code look like if it weren't like
this
um so i made that code i made this thing
with my friend tom crayford called the
raptor web framework it is
an experimental framework it's not
designed for building applications it's
designed for
proving that you don't need things to be
in giant ugly controllers or giant ugly
models
so here's a raptor application this is
actually a complete application assuming
there's a database layer for it to talk
to
this is sort of like saying um what
would the rails equivalent be
resources users only
create yes only create it's like saying
that underneath the hood it's
translating
into this this is what raptor's actually
doing creating a route named create
it accepts posts to slash user because
it's in the slash user path
the second line is what it's routing to
this is a path to an arbitrary method in
the system so there's no idea of
controllers it just routes to some
method
and line three says if it succeeds
redirect to show
that's what you normally want to do on
create right you make the thing you
redirect to show if it succeeds
if it throws a validation error then you
should re-render new because there are
errors and you want to re-render the
form
so in other words in raptor by saying
the word create you get the standard
rails
create behavior that you would have
coded by hand in normal cases
usually you're going to override the
target of the route so this is
uh sort of like specifying the action
you would run in a rails controller or
in a rails route
and it's just an arbitrary method path
and then the
method might look something like this
it's in a plain old object no base class
and it takes
the params as an argument and then does
whatever it needs to do in this case
it's creating users so it makes the
database record
and then hits braintree which is a
payment system to
put a record in there as well
now there's an important question that
this should be raising in your mind and
i hope it is but before i get to it
so far we have routes we have routes to
delegate to services
and the services hit some kind of
database layer but the route the the
services in the database layer are your
problem raptor doesn't doesn't tell you
how to do those
the question that should have uh come up
in your mind though is what's with the
params
controllers need more than params
sometimes you need headers sometimes you
need current users sometimes they need
cookies or session state
they need all kinds of stuff there's
like 800 methods i think currently on
action controller base
and raptor doesn't have action
controller base
it doesn't it actually doesn't have any
inheritance in it whatsoever
instead you get arguments but the
arguments can vary
so your one service the top service
there wants params the bottom service
wants
current user and params and the way that
this works is that raptor
does in fact have an injector
but is not a container it's not an ioc
container
there's no configuration and i don't
mean there's no xml i mean you can't
configure it there's no configuration to
do
it's purely name based so when i have a
method that takes current user and
params raptor knows params out of the
box
current user is actually one that you
will define using a fairly simple method
telling raptor what current user means
and then whenever it sees an argument
current user it'll pass
the current user in
raptor also has requirements on routes
which are like the guards that i
mentioned earlier things like
authorization
and they look like this i have a route
update it requires an administrator only
an administrator can do it
what does that mean though raptor
doesn't know administrator doesn't have
auth built in
well i define a class my app whatever
your app name is
double colon requirements double colon
admin raptor will look that up based on
the fact that you're requiring admin
the injector will inject the current
user argument because it happens to take
it
and then the match method will return
true or false for whether the route
should match or not
so much like you can hook into the
injector you can hook into the router
and specify arbitrary high level
requirements with this sort of
conventional name-based interface
and there's lots of other stuff about
raptor i don't want to go on and on
about raptor because i don't think it's
very interesting
and this talk isn't about raptor but
raptor doesn't have controllers and it
actually
works as a web framework i mean we've
not built
production applications in it or
anything but you can you can look at
pretty much any rails
controller and figure out how to
decompose it into raptor and then it
makes
much more sense all the concerns are
separated routing is in one place
logic is in one place and the injectors
tend to be
sort of a junk drawer but they tend to
have single responsibilities
of course the real graph looks like this
the injectors everywhere
every time raptor calls your code it
injects and uh it doesn't inject
arbitrary method boundaries but whenever
it calls you it injects
if you want anything you don't have to
use it if you don't want to
so raptor is a real thing you can go to
github and find it and download it's
been there for over a year
but once again the point my point is not
raptor raptor is an existence proof
that you can decompose applications into
pieces that make more sense than just
well the request comes in and there's a
controller action
that's my architecture i have a function
it's not it's not much of a design
the the point the the more immediate
point that i hope you take away
from this is that stateless services
will help you to decouple your
persistence from other bits of the
system and you want that
and simple third-party wrappers will
help protect you from changes
in other ruby gems or services
ruby has a culture of rapid change and
not a lot of regard for backwards
compatibility so you really want to
insulate yourself from that
chaos in in the ecosystem
and these two things i think should be
on one side of a very important line in
your application which
is the rails line and on the other side
are very simple active record objects
and then rails
and you want those things away from your
services and your third-party wrappers
because you want to be able to test
without rails
and you want to insulate yourself from
the chaotic
nature of rails over time to be honest
uh the two to three transition
demonstrates that things don't always go
so smoothly when you upgrade
so that that is the end of the talk i
realize it's sort of all over the place
but i hope you get something useful out
of it as far as thinking about
what an application does what are the
pieces of it what especially what's a
controller doing and what should
a controller be doing so once again i'm
gary bernhardt
i own destroyal software which produces
screencasts on things like
oo design tdd and also all of this stuff
basically
uh this there's a series called sucks
rocks
that i took the examples directly from
and it talks about exactly these things
so thank you guys very much for for
listening to me for 20 minutes
any any questions
hey hey um so basically i've been trying
to
use some of those uh paradigms to do
it falls in in software um
and i find that for some some things it
works really well
especially when you deal with pretty
simple uh input output
uh scale up values strings and so forth
but oftentimes in rails we really do
create a lot of qrad applications
and it doesn't feel like it scales to
that
uh really because oftentimes you use
you add a validation to your model which
really just is a view concern often
uh like putting in an error message and
that just works
and the just works part is the bit that
drives rails people right
and i feel that it's it's difficult to
get there
yeah it's um the problem with rails is
that it is so opinionated that the more
design you impose the harder it becomes
and so
um uncle bob for example has been
talking about
design and web apps and the design the
sort of
architectural vision that he he
describes is
quite complex there are a lot of moving
parts and he advocates
complete separation of your basically
your application
from the web framework like rails and in
rails i don't think that makes sense
i think that is that is far too much the
basically pulling out services which is
the core of what i advocate
uh i think does make sense not all the
time for example that validation case
yes it is a view concern yes it's these
sometimes it's a view concern
yes it's easier to put it in the model
and that's probably what you should do
because if you fight that you're going
to end up writing 20 lines of code
to avoid writing the one line in the
active record class
so i i try not to take it to an extreme
but as long as it doesn't hurt
oftentimes it won't hurt
like the case as you mentioned sometimes
it really is nice and in those cases
it'll be obvious that it's nice
when you when you do it and that's when
i think it can be really beneficial
even if 75 percent of your app was built
with services
and the other 25 was plain old ball mud
rail style
it's a huge improvement
we have one in the middle here
yeah as pointed out prior about the
validations a lot of the beauty of rails
lies in the
meta programming and dsls um
was it a conscious this decision i mean
of course it was probably
um to go the same route or even drive
that further because
i thought the decomposition aspects were
really clear and
going into a layered architecture having
some kind of domain driven design
input in that um but i thought kind of
that
having a more clear like using the basic
apis of ruby itself
approach would also be beneficial for
for yeah the basic api is like the core
types yeah
exactly oh okay um i'm not entirely sure
can you give an example oh you've lost
the microphone
i gave it away um yeah um when you
when you use the um dsls to define your
your actions and you have the the
injector which is a cool thing i guess
um but um where you where you actually
have everything in these dsl calls what
what you basically do
where you could rely basically on the
libraries that provide
output and stuff so make it more
explicitly in the car
so that you just use what's basically
there and not define
like a layer of dsl in between
i'm yeah i i i'm not impressed by
by the extreme level of sort of dsl
obsession
that that rails took as a design i mean
if i were building a web app today i
would probably use rail once the right
so it's not as if i think it's awful or
anything but
um i think it takes it very far and in
many cases hides
intent uh there are a lot of cases
in activerecord especially where if you
just define a method it's
like one more line and then you know
exactly what it's doing
and so that's why that's one of the
reasons wrapper doesn't actually touch
the database layer because
uh it's hard to do that tersely without
introducing lots of confusing stuff
the only actual dsl in raptor is the
router
and it's a pretty complex one but
other than that there's there's nothing
it's just all methods all the way down
methods with arguments
i don't know if that answers your
question at all but
are we on time still good i guess so
there's one over here
far away from all the microphones
everyone's waiting so nicely during my
questions i don't know why why i'm
special uh yeah i'm also concerned a bit
about
their magic router stuff uh what's about
uh
connecting it will be web machine
instead that was machined by
strong crips i know it has some issues
with track and so on but
like going this direction it wouldn't be
the nicest way you think
um they make sure i understood you're
asking about um
integrating something like raptor into
yeah
into like heroku deploying it um
actually no to
web machine but oh web machine oh i'm
sorry i misunderstood um
i have no idea uh
um i'm not super into the whole async
thing so i haven't paid a lot of
attention to web machine
yeah because i i really like how raptor
is is designed by
all the all the constraints and right
and
nice nice design but yeah the router is
like
very magic thing in there and yeah i
didn't like it
yeah it is very it is very magical and
that's the thing the router had the most
design put into it and it's the part i'm
the least happy with i think it's real
it's a
i have a lot of respect for rails after
writing that router um
and ours is i mean the router and
raptors i would guess
uh 150 to 200 lines it's extremely
simple actually i was playing with
router
and after that i don't have respect for
routers
for me it's useless thing i have respect
for one machine right now yeah it's
awesome um when we were so the the thing
the thing that uh makes me want to
design something like web machine is the
fact that it bakes this the protocol in
right it's the protocol is part
it is web machine web machine is the
protocol it's like one or wait am i
confusing web machine with um
no web machine is is like a direct
implementation of http where
state transitions are yeah um
and we thought about doing that but we
just could not reconcile it with what we
wanted an application to look like
especially the router and because
raptor's router raptor's router routes
on everything from like a verb an http
verb all the way into
model level stuff like you're an admin
or not and we could not figure out how
to fit that into a web machine style
model it's it's like two fundamentally
different ways of slicing
the problem is encode the protocol
directly or
build your own thing that uses the
protocol okay thanks
yeah great presentation by you thank you
i think we i guess we still have time no
one's kicking me off
one more i can't see
very well i don't think we have one more
all right thanks guys
[Music]
you