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
Talks x
Talks
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Talks
Talks
Book
Book
Pack It Up - SF Ruby 2025 - @stephanhagemann.com
Talks
Talks
Book
Book
Book
Tools
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

danger-packwerk

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
Gannon McGibbon
Eileen Uchitelle
Brandon Weaver
Stephan Hagemann
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

Like many other tools in the Rails ecosystem, Packwerk is a sharp knife, and it must be wielded with care. Be intentional about how you use it, and how you fix the violations it raises.

https://shopify.engineering/a-packwerk-retrospective

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

[Modularity is] not a technical issue. It's a social, incentives, organizational, and educational issue.

https://bsky.app/profile/baweaver.bsky.social/post/3lbgw6tedd22v

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

What is our hammer? What is our tool?

Packwerk is a tool to shape application architecture

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?

... and which tools to use?

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)
  # Charge customer
  customer = Customer.find(customer_id)
  customer.charge(amount)

  # Update stock
  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

# packs/purchasing/app/services/purchasing/charge_service.rb
module Purchasing
  class ChargeService
    def call(customer_id, amount)
      customer = Customer.find(customer_id)
      customer.charge(amount)
    end
  end
end

# packs/inventory/app/services/inventory/stock_service.rb
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

  • The only tool in our toolbelt that has "learning an architecture" ... and "learning architecture" built in

Pack It Up - SF Ruby 2025 - @stephanhagemann.com

Gem

# purchasing-gem/lib/purchasing.rb
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

# inventory-gem/lib/inventory.rb
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

  • It all depends

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
Gannon McGibbon
Eileen Uchitelle
Brandon Weaver
Stephan Hagemann
Pack It Up - SF Ruby 2025 - @stephanhagemann.com

THE TAKEAWAY. Take 6

  • All tools create an opportunity for value creation -- All tools cost something

  • 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.