Slides from 2023/03/09 NYC.rb
A presentation about managing large Ruby applications using gems.
Here's an example repository: https://github.com/agenteo/lab-best_health
Find more blog posts I wrote (in 2016) on the topic here: https://teotti.com/create-dependency-structures-with-local-ruby-gems/
and here https://teotti.com/topics/maintainability/
21. 6 months later
– The law of continuing change (1974) Lehman, M
“Any software system used in the real-world must change or
become less and less useful in that environment.”
– The law of increasing complexity (1974) Lehman, M
“As a program evolves, it becomes more complex, and extra
resources are needed to preserve and simplify its structure.”
29. # lib/blog/after_publish.rb
module Blog
class AfterPublish
private
def subscribe_blogger_to_promotion
Promotions::Submission.new
end
end
end
# lib/promotions/new_member.rb
module Promotions
class Submission
private
def fetch_member(id)
# lib/membership/finder.rb
Membership::Finder.new(id)
end
end
end
promotions
blog membership
namespaces
context context
30. # lib/blog/after_publish.rb
module Blog
class AfterPublish
private
def subscribe_blogger_to_promotion
Promotions::Submission.new
end
end
end
# lib/promotions/new_member.rb
module Promotions
class Submission
private
def fetch_member(id)
# lib/membership/finder.rb
Membership::Finder.new(id)
end
end
end
promotions
blog membership
namespaces
context context
48. piadina gem
pizza gem
C
shared
ingredients gem
main Ruby application
desserts gem
D
A
E
calzone gem
pizza dough gem
F
Conway’s Law
“organizations which design systems … are constrained to produce designs which
are copies of the communication structures of these organizations"
51. A
C
D
B
E
your health plan
drug information
claims platform
product
information
membership
gem
gem
gem
gem
gem
dependency
main Ruby application
Sinatra / Rails / Hanami
http://teotti.com/create-dependency-structures-with-local-ruby-gems/
53. ├── Gemfile
├── Gemfile.lock
├── local_gems
├── run.rb
└── spec
path 'local_gems' do
gem 'health_plan'
end
source 'https://rubygems.org'
group :test do
gem 'rspec'
end
bundler’s Gemfile
uses a path directive
to find local gems
main Ruby application
54. ├── Gemfile
├── Gemfile.lock
├── local_gems
├── run.rb
└── spec
bundler’s Gemfile
uses a path directive
to find local gems
path 'local_gems' do
gem 'health_plan'
end
source 'https://rubygems.org'
group :test do
gem 'rspec'
end
main Ruby application
55. ├── Gemfile
├── Gemfile.lock
├── local_gems
├── run.rb
└── spec
directory where your local gems are
$ cd local_gems
$ bundle gem health_plan
create health_plan/Gemfile
create health_plan/Rakefile
create health_plan/LICENSE.txt
create health_plan/README.md
create health_plan/.gitignore
create health_plan/health_plan.gemspec
create health_plan/lib/health_plan.rb
create health_plan/lib/health_plan/version.rb
Initializing git repo in /Users/me/code/lab/gem-dependency-structure/local_gems/health_plan
$ rm -Rf health_plan/.git*
main Ruby application
58. # local_gems/health_plan/spec/health_plan/aggregator_spec.rb
require 'spec_helper'
describe HealthPlan::Aggregator do
describe "#details" do
it "should not throw exceptions" do
aggregator = HealthPlan::Aggregator.new(12345)
expect(aggregator.details).to eq({ name: 'The full package plan'})
end
end
end
your health plan
├── Gemfile
├── Gemfile.lock
├── local_gems
│ └── health_plan
├── run.rb
└── spec
59. # local_gems/health_plan/lib/health_plan/aggregator.rb
module HealthPlan
class Aggregator
def initialize(id)
@subscriber_id = id
end
def details
{ name: 'The full package plan'}
end
end
end
# local_gems/health_plan/lib/health_plan.rb
require "health_plan/version"
require "health_plan/aggregator"
module HealthPlan
end
gem entry point
your health plan
├── Gemfile
├── Gemfile.lock
├── local_gems
│ └── health_plan
├── run.rb
└── spec
60. ├── Gemfile
├── Gemfile.lock
├── local_gems
│ ├── drug_information
│ └── health_plan
├── run.rb
└── spec
your health plan
drug information
main Ruby application
$ cd local_gems
$ bundle gem drug_information
create drug_information/Gemfile
create drug_information/Rakefile
create drug_information/LICENSE.txt
create drug_information/README.md
create drug_information/.gitignore
create drug_information/drug_information.gemspec
create drug_information/lib/drug_information.rb
create drug_information/lib/drug_information/version.rb
drug information
61. # local_gems/health_plan/spec/health_plan/aggregator_spec.rb
require 'spec_helper'
describe HealthPlan::Aggregator do
describe "#details" do
let(:fetched_drugs) { 'something' }
before do
fetcher_double = double('DrugInformation::Fetcher', details: fetched_drugs)
allow(DrugInformation::Fetcher).to receive(:new).and_return(fetcher_double)
end
it "should not throw exceptions" do
aggregator = HealthPlan::Aggregator.new(12345)
expect(aggregator.details).to eq({ name: 'The full package plan’,
drugs: fetched_drugs })
end
end
end
your health plan
├── Gemfile
├── Gemfile.lock
├── local_gems
│ ├── drug_information
│ └── health_plan
├── run.rb
└── spec
67. A
C
main Ruby application
B
loaded in memory, deamon or webserver
unit tested
unit tested
not unit tested
http://teotti.com/create-dependency-structures-with-local-ruby-gems#gotcha-flaky-bugs-caused-by-missing-requirement-statements
70. A
C
D
B
E
main Ruby application
F
H
I L
membership
payment API
payment
platform
bank transaction
credit card
transaction
your health plan
API
drug information
claims platform
product
information
membership
71. A
C
D
B
E
main Ruby application
F
H
I L
membership
payment API
payment
platform
bank transaction
credit card
transaction
your health plan
API
drug information
claims platform
product
information
membership
72. A
C
D
B
E
main Ruby application
F
H
I L
membership
payment API
payment
platform
bank transaction
credit card
transaction
your health plan
API
drug information
claims platform
product
information
membership
73. A
C
D
B
E
main Ruby application
F
H
I L
membership
payment API
payment
platform
bank transaction
credit card
transaction
your health plan
API
drug information
claims platform
product
information
membership
74. A
C
D
B
E
main Ruby application
F
H
I L
membership
payment API
payment
platform
bank transaction
credit card
transaction
your health plan
API
drug information
claims platform
product
information
membership
78. A
C
D
B
E
main Ruby application
F
H
I L
membership
payment API
payment
platform
bank transaction
credit card
transaction
your health plan
API
drug information
claims platform
product
information
membership
79. main Ruby application
your health
plan API
drug
information
claims
platform
product
information
membership
membership
payment
API
payment
platform
bank
transaction
credit card
transaction
80. deploy parts of a monolith
http://teotti.com/deploy-parts-of-a-ruby-on-rails-application/
I am not gonna come up with a gem that solves all your problems.
This talk is not about performance—I am not talking about large loads
I mean large business rules growing as the application development progresses— but team size always contained between 5 to 10 developers—the bulk of the logic was on the backend.
That’s a joke, but devs coming from compiled languages tend to say Ruby shouldn’t be used in large projects. I don’t think that’s true but large Ruby application pose challenges
that in the 10 years I worked with Ruby needed a combination of team diligence, the use of local Ruby gems and test driven development.
But delivering large maintainable web applications was possible with their combination.
* XXX alone was not sufficient
You will have code that developers want to work on and understand on their first day rather then being overload with details.
By now you might be wondering where my accent is from. I am Italian, I learned to speak english watching family guy, I lived in Australia for over 6 years and in the US for about 2 years.
so my accent will be affected by those 4 regions
your project ruby files deliver features like ingredients in a recipe deliver a dish
imagine a kitchen worktop with the following ingredients
can you tell what recipe they’re for?
Those ingredients are for piadina a popular italian street snack priced around 5 AUD
But in Sydney Australia it can only be found in one cafe opened by two young Italians near Bondi Beach for 17 Dollars!.
A simple recipe can be very profitable.
This story is inspired by real facts so I am gonna make up some names… I am gonna call the owners… Mario and Luigi.
Mario and Luigi are from Brescia, a province where piadina isn’t exceptionally good but they are the first and only place selling piadina in Sydney so when me and my Italian friends wanted to have it we had to go there, and at first it was pretty busy.
Whenever we went there I never saw Mario and Luigi making piadinas, they had two ozzie students with no cooking experience named… Stevo and Anna.
Stevo and Anna, familiarised themselves with the ingredients, learned the recipe and started making the profitable piadinas for Mario and Luigi.
Everything is going well until 3 months later Anna goes on a gap year and Mario has to hire someone new: Diego, a new student also with no cooking experience.
Diego doesn’t really know what’s going on Mario, Luigi laugh at how slow he is in picking up the recipe but Stevo helps Diego overcome the intrinsic load of learning the recipe and the ingredients and everything is back to normal.
Piadina was selling just ok so to be more competitive Mario and Luigi decide to add more recipes to the menu: pizza, tiramisu and carrot cake
Can you find the ingredients for the new recipes? We probably find one ingredient for carrot cake… but we might struggle to find the rest.
Can you still find piadina’s ingredients?
Stevo's reaction was a mix of confusion and frustration. On top of the intrinsic difficulty of learning new recipes, there is an extraneous (ecstrinius) load of finding the ingredients on the messy kitchen worktop.
To Mario and Luigi’s eyes there are obviously four recipes now.
To Stevo’s it just takes longer to find the ingredients and prepare the dishes.
Stevo is affected by cognitive overload.
He feels like he’s over his depth, is it because he’s not smart enough?
Or maybe the worktop is too disorganised?
He’s a novice and unsure about his skills on how to proceed so he ask Mario and Luigi to organise these ingredients.
Stevo come in the following day and ingredients on the worktop are actually labelled by colour
this is making it very difficult to recognise a recipe ingredients
some of you might have noticed the ingredients were grouped by color from the first slide, did they bother you then?
So when cooking pizza Stevo needs to think, which ingredients are white? which are red? which are green?
When he complains Mario replies: that’s the convention in Italy!
Have you ever seen this happening in a Ruby application?
are ok in a tiny app but in a large app it’s harder to navigate large codebases
describes an attribute of that class but it says nothing at all about its operational context
you might be using single responsibility and good object oriented patterns but how does that help in finding boundaries in your application?
[pause ..]
Stevo suggests dedicated areas of the worktop to each recipe?
This allows Stevo to get the ingredients required to work on a specific dish, there are shared ingredients but it’s easy enough to recognise them and it doesn’t create too much overhead.
He can look at Diego next to him focusing on preparing pizza.
But then one day Stevo is rushing preparing a carrot cake, he wants to leave the kitchen and go play beach volleyball down in Bondi. He reaches and grab the milk jar but it slips off his big hands and cracks on the worktop, milk leaking everywhere spoiling the tiramisu cream and the raising pizza dough sitting next to it.
this is similar to Ruby namespaces, they provide a simple form of separation in a Ruby application
it’s a first step in the right direction, it ensures your application has some boundaries
a group of classes defined within a namespace becomes specific to that context and force external clients to use the full namespace to access them
[explain code]
There are dependencies but you won’t be able to tell unless you look and familiarise yourself with the source code.
it’s a first step in the right direction, it ensures your application has some boundaries
a group of classes defined within a namespace becomes specific to that context and force external clients to use the full namespace to access them
[explain code]
There are dependencies but you won’t be able to tell unless you look and familiarise yourself with the source code.
In a Rails project ongoing for 3 years (team of 5) we used namespaces to keep separation between vertical slices of the application’s functionality but when the project was about one year old we had a tangle of cross namespaces dependencies hard to follow and slowing down development.
In a Rails project ongoing for 3 years (team of 5) we used namespaces to keep separation between vertical slices of the application’s functionality but when the project was about one year old we had a tangle of cross namespaces dependencies hard to follow and slowing down development.
Some laughed when we were concerned about the tangling dependencies.
Onboarding new developers was a long task causing them cognitive overload. The vertical boundaries were leaking and out of control. Simple changes in the middle of the tangle would take a long time and constantly introduce unpredicted side effects that tests were unable to catch.
There was a desire to fix the problem but we couldn’t commit to team wide diligent work. In the end it became the app nobody wanted to touch.
For many that’s just how Ruby is. I disagree.
Some laughed when we were concerned about the tangling dependencies.
Onboarding new developers was a long task causing them cognitive overload. The vertical boundaries were leaking and out of control. Simple changes in the middle of the tangle would take a long time and constantly introduce unpredicted side effects that tests were unable to catch.
There was a desire to fix the problem but we couldn’t commit to team wide diligent work. In the end it became the app nobody wanted to touch.
For many that’s just how Ruby is. I disagree.
To prevent more incidents Mario, Luigi Stevo and Diego agree to have separate worktops for each recipe… and a shared worktop.
If there is a leak it will be contained on that recipe kitchen worktop.
We can now assign a cook to a specific recipe.
this is similar to using local Ruby gems
everyone is probably familiar with open source Ruby gems distributed over the internet
and there is a misconception that is all gems can do.
I don’t want to make my business logic open source! Fair point.
gems don’t need to be published. They can live within the application repository, used as building blocks creating solid and distinct boundaries. An intention revealing dependency structure that helps navigating and changing code.
the separation isn’t as solid as with different worktops because when a Ruby application requires your first entry point gem it’ll require all its dependencies so all the classes will be in memory
we’ll see how you can ensure the dependency structure is solid by unit testing the local gem
At the beginning there is just one gem.
The structure will evolve over weeks and months as more responsibilities piled up.
Each gem has a specification file that indicate its local dependencies.
That’s your application map, that’s how you reduce cognitive load.
The objective is an intention revealing dependency structure that reduce the developers cognitive load and facilitate conversations with business owners.
The risk is to split in too fine grained components creating a dependency structure that is purely technical and doesn’t represent any business rule impeding conversations with business owners.
[pause ..]
My guideline is to map business operational areas to objects names and gems. When more then two or three concepts are living in the same gem I ask the business owner if they think it’s a different context, perhaps a different team or company operating at the other end of this feature?
The objective is an intention revealing dependency structure that reduce the developers cognitive load and facilitate conversations with business owners and within the team.
The risk is to split in too fine grained components creating a dependency structure that is purely technical and doesn’t represent any business rule impeding conversations with business owners.
Without product owners a team can self determine those boundaries.
[pause ..]
My guideline is to map business operational areas to objects names and gems. When more then two or three concepts are living in the same gem I ask the business owner if they think it’s a different context, perhaps a different team or company operating at the other end of this feature?
Conway’s law help with this.
It’s hard work and expect the boundaries to change, the gem might be renamed stuff moved or added as the application evolves. And that’s where the term evolutionary design comes from.
A local gems dependency structure is complementary to good object oriented programming. You should still use your favourite flavour of patterns.
The objective is an intention revealing dependency structure that reduce the developers cognitive load and facilitate conversations with business owners and within the team.
The risk is to split in too fine grained components creating a dependency structure that is purely technical and doesn’t represent any business rule impeding conversations with business owners.
Without product owners a team can self determine those boundaries.
[pause ..]
My guideline is to map business operational areas to objects names and gems. When more then two or three concepts are living in the same gem I ask the business owner if they think it’s a different context, perhaps a different team or company operating at the other end of this feature?
Conway’s law help with this.
It’s hard work and expect the boundaries to change, the gem might be renamed stuff moved or added as the application evolves. And that’s where the term evolutionary design comes from.
A local gems dependency structure is complementary to good object oriented programming. You should still use your favourite flavour of patterns.
For the next five minutes we’ll be diving in technical details of setting up a local gem dependency structure to ensure you get a first pass
the example is explained in detail on my blogpost and code is on github
, if I loose you come to me after the talk and I’ll try to clarify, after the next 5 we’ll go back to the same high level with real usages of local gems
You can use local Ruby gems with any framework.
The framework is used as a a delivery mechanism triggering calls to your business logic encapsulated within gems
This is gonna be a skeleton example focused on connecting two classes living in two separate local gems. In reality each gem has multiple classes with one responsibility helping that one class doing its job.
A simple script triggers our entry point gem aggregator class.
create gem in a directory meaningful to your team
bundle provides a gem option to create gems
delete gem repository
* lock to a specific version to prevent multiple local gems to install different versions of spec
This is a simplified example focusing on connecting the local gems. Within drug_information you’d have a 5/6 classes with one responsibility each helping fetcher to do its job.
unit tests exercising each gem’s responsibility and a high level acceptance test ensuring integrations worked
if somebody change product information code to access membership breaking the dependency structure only the automated test will warn us about that
flaky bugs can spawn depending on gem requirement order
When your application first access B, and B doesn’t require C but uses its code a run time error will be triggered.
And this is why you must use automated tests to adopt this strategy.
unit tests exercising each gem’s responsibility and a high level acceptance test ensuring integrations worked
if there is a in breaking the dependency structure only the automated test will warn us about that
giving the team an opportunity to talk about the boundaries.
At runtime, since usually all the gems are loaded in memory you’re unlikely to see any problem.
say you have an A and F team,
You can even assign teams to a boundary.
You can even assign teams to a boundary.
The whole team is responsible for the shared behaviour, that’s why diligence is important.
This should be totally achievable for teams up to ~10/12
team diligence should be achievable in a team of 6/10 people… perhaps you can convince skeptics to run an experiment? See how they feel?
as the team and the company grows it will be harder to ensure in process boundaries, that’s when the modular monolith is ready to be broken up in to services to introduce an explicit boundary
perhaps moving parts of the modular monolith in to services is the natural evolution for your project. And the modular monolith is ready to be sliced!
How would it look with just using namespaces?
*** try switch from gem boundaries to recipe
without a dependency structure you’ve lost visibility of how those namespaces relate to each other and affected by cognitive overload
that’s the monolith you want to avoid
This strategy allowed us to deploy parts of a Rails application.
This strategy allowed us to deploy parts of a monolithic app.
3 months in to development we were told half the app had to be deployed to a separate server.
Some manager was pushing for services, an API used by the two contexts of the application.
The two entry points had a shared local gems dependency structure.
we uses an env variable on the admin server to tell Ruby what functionality to load. That functionality was encapsulated in an entry point gem.
Thanks for listening, reach out if you have any question.
Hanami is a 2 years old web framework that brings a solid modular approach to the Ruby ecosystem.
Container applications are core to the framework as well as a better separation between the delivery mechanism and the persistence layer (entities and respositories).
But when complex business logic is not encapsulated you will end up with the usual tangle of cross namespace dependencies littering the lib folder.
Your code might be simply making piadina today but how likely is it to stay that way?
You can keep piling files on one kitchen worktop or start thinking in terms of separate worktops for separate contexts.
A team in a fixed mindset won’t even try and this strategy results will be very limited.
A team in a fixed mindset believes skills and abilities are set… you either have it or your don’t… and failure means you don’t.
A team in a fixed mindset believes they are not talented enough to use gems
Or the opposite, they’re too smart to use local gems… a hack… not the Ruby way…
—
So rather then taking on new challenges—like local Ruby gems—this team sticks to what they know well… that way they can be safe, keep looking smart.
speak with your team about local gems encapsulating your application boundaries, ensure they are on board
run an experiment on a section of your code, measure the results: bugs and velocity between the modularised section of your app and the rest to convince skeptical with empirical data.
And don't forget to TDD your gems to ensure their dependency structure is solid
I believe it will help you as it helped me build and maintain large Ruby applications.
Thanks for listening, reach out if you have any question.