Previous slide Next slide Toggle fullscreen Open presenter view
Pack It Up
Why Packwerk Can't Save Your Messy Rails App (But You Can)
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
2024 was a year of packwerk retrospectives (big and small)...
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
"The final nail in the coffin for packwerk!"
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Ever since these talks I felt I should do some retrospecting too...
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Hi, I am Stephan
Lead Developer Productivity teams at Gusto
Teams working on builds, dev envs, Typescript, GraphQL, Rails, Kafka, and ...
Modularity
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Why Packwerk Can't Save Your Messy Rails App (But You Can)
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Packwerk on a slide
add the packwerk (or pks) gem
mark folders as packages by adding a package.yml file
Selectively, set enforce_dependencies: true
Accept some dependencies between packages (via package.yml config)
See other dependencies (violations) in package_todo.yml files
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
packwerk-extensions
Adds new enforcements types
privacy
visibility
folder_privacy
layer
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
pks
Rust implementation of packwerk. Loads faster
Contains all packwerk-extensions enforcements
Solves autoloading limitation of packwerk
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
packs
Rails-like structure for package internals
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
packs-rails
Autoloading for packs in a Rails app
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
code_ownership
Define ownership of files, including...
ownership per package
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
query_packwerk
Multitude of querying capabilities for packwerk violations
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
visualize_packs
Visualize package and enforcement structures and statuses
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Back to those Retrospectives!
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Non-autoloaded files get missed by packwerk - hiding real dependencies
Yes. But fixable* with pks
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Privacy checks created architectural debt
Privacy enforcements, naively "fixed" without question created architectural debt
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Package boundaries created bad dependency structures
Enforcing undesirable package boundaries leads to ... undesirable outcomes
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Technical modularity didn't solve the real pains (tight coupling, flaky CI, difficult onboarding).
Even with all the work... what fraction of engineering years went into improving the monolith vs adding to it?
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Violation lists only ever grow
Enforcement configuration in a file does not create the organizational conditions to actually enforce anything
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Domain-based packaging conflicted with functional realities
Drawing useful boundaries is difficult and takes learning and iteration
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Conclusions - Some Hightlights!
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
We cannot keep hiring developers and failing to train them on how to write Rails the Rails way. [...] If we don't train new hires why we use Rails, how to write Rails, how to organize code, how to write tests, how to use the features of the framework, how to avoid sharp knives and how to follow Rails conventions we're doing ourselves a disservice.
https://www.youtube.com/watch?v=olxoNDBp6Rg
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
The myth of the modular monolith is that architecture cannot fix human and culture problems but fixing human and culture problems can improve our architectural operational and organizational challenges
https://www.youtube.com/watch?v=olxoNDBp6Rg
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
So... What does all this mean for you ?
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Feel disempowered?
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Were all of these messages directed only at the top levels of engineering leadership??
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
THE TAKEAWAY
When the youtube page about it is sooo long, there is a skill that's important
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
THE TAKEAWAY. Take 2
Don't use it is a "sharp knife"?!?
How are we supposed to learn?
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Do you feel comfortable with your ability to argue for a better application architecture?
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
THE TAKEAWAY. Take 3
You should play with and learn from packwerk to get better at application architecture
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Let's try!
Functions
Classes
Modules
Packs
Gems
Apps
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Function
def process_purchase (customer_id, product_id, amount, quantity )
customer = Customer .find(customer_id)
customer.charge(amount)
product = Product .find(product_id)
product.update!(stock: product.stock - quantity)
end
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Class
class PurchasingService
def charge_customer (customer_id, amount )
customer = Customer .find(customer_id)
customer.charge(amount)
end
def update_stock (product_id, quantity )
product = Product .find(product_id)
product.update!(stock: product.stock - quantity)
end
end
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Module
module Purchasing
def self .charge_customer(customer_id, amount)
customer = Customer .find(customer_id)
customer.charge(amount)
end
end
module Inventory
def self .update_stock(product_id, quantity)
product = Product .find(product_id)
product.update!(stock: product.stock - quantity)
end
end
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack
module Purchasing
class ChargeService
def call (customer_id, amount )
customer = Customer .find(customer_id)
customer.charge(amount)
end
end
end
module Inventory
class StockService
def call (product_id, quantity )
product = Product .find(product_id)
product.update!(stock: product.stock - quantity)
end
end
end
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
THE TAKEAWAY. Take 4
Packwerk → Gradual Modularization
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Gem
module Purchasing
class Engine < Rails::Engine
end
class ChargeService
def call (customer_id, amount )
customer = Customer .find(customer_id)
customer.charge(amount)
end
end
end
module Inventory
class Engine < Rails::Engine
end
class StockService
def call (product_id, quantity )
product = Product .find(product_id)
product.update!(stock: product.stock - quantity)
end
end
end
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
App
purchasing-app/
app/
controllers/
charges_controller.rb
models/
customer.rb
config/
routes.rb
inventory-app/
app/
controllers/
stock_controller.rb
models/
product.rb
config/
routes.rb
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
THE TAKEAWAY. Take 5
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
A retrospective is the reflection of a
a specific person or group
in a specific context
at a specific time
after specific work
to decide on specific actions!
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
What can you take away from these retrospectives?
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
THE TAKEAWAY. Take 6
Think! Is the tradeoff worth it for you?
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
THE REAL TAKEAWAY
Don't let others do the thinking for you
Keep learning and growing
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
I am still Stephan
Thank you very much!
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Anything outside Zeitwerk autoload paths—manual `require`s, engines, routes, fixtures, initializers—remains invisible. Teams could "fix" every violation yet still hit NameError at runtime when loading a supposedly isolated package ([Retrospective](https://railsatscale.com/2024-01-26-a-packwerk-retrospective/); [README limitations](https://github.com/Shopify/packwerk?tab=readme-ov-file#limitations)).
Enforcing public/private boundaries via `app/public` felt intuitive but diverged from Rails conventions, spawned duplicate directories, and produced undocumented "public" APIs that were never intended for consumption, so privacy checks were removed in Packwerk 3.0 ([Retrospective](https://railsatscale.com/2024-01-26-a-packwerk-retrospective/)).
Packwerk happily enforces whatever packages you define, even if those directories don't map to runtime coupling. Misplaced models (e.g., fraud detection living in "billing") led to expensive refactors with little payoff because the graph had been drawn around names instead of behavior ([Retrospective](https://railsatscale.com/2024-01-26-a-packwerk-retrospective/)).
After years of Packwerk packaging, Shopify still saw tight coupling, flaky CI, slow deploys, and hard onboarding—the slide deck explicitly answers "Improve structure & organization? No" and similar questions with "No" or "Not yet" ([Speakerdeck slides](https://speakerdeck.com/eileencodes/the-myth-of-the-modular-monolith-day-2-keynote-rails-world-2024)).
Even Shopify only recently emptied a `package_todo.yml`; Packwerk had a bug that never removed stale TODOs because almost no one reached zero violations. The backlog of flagged issues tended to grow faster than teams could remediate them ([Retrospective](https://railsatscale.com/2024-01-26-a-packwerk-retrospective/)).
Components named after commerce domains produced massive cyclical graphs because runtime flows cut across them. Treating components as domains but packages as functional slices proved less confusing ([Retrospective](https://railsatscale.com/2024-01-26-a-packwerk-retrospective/)).
So, let's talk about *hammers*
Hammers were used in the making of what's on the left as well as on the right.
Hammers were used in the making of what's on the left as well as on the right.
In a world where change is supercharged by economic uncertainty and AI, being able to learn new things and being adaptable strike me as very good skills to have.