Software Development
Nicolas Nisoria
2022-01-05

Ruby on Rails modularization with Packwerk Episode I

Humans find it difficult to see the big picture of a problem without devoting a lot of time and effort. This happens especially while working with large and complex applications. What are the side effects of my changes? Why is this line here affecting the tests of a remote part of the codebase? A perfect or complete solution does not exist, but Shopify came out with a tool that will probably help you and your team.

Introduction

In order to talk about Packwerk, we need to introduce a few concepts first.
  • Cohesion: refers to the measure of how much elements in a module or class belong together.
  • Coupling: refers to the level of dependency between modules or classes.
  • Boundaries: refers to barriers between code. In this case, a code boundary refers to different domains of concern within the same codebase.
  • Modularization: the process of dividing a software system into multiple separate modules where each module works independently.

Problems

As we know, Ruby doesn't provide a good solution to enforce code boundaries. We can specify the visibility but all the dependencies will be loaded into the global namespace. In large or monolith applications, this lack of boundaries produces the following problems.

  • Low cohesion,
  • High coupling,
  • Spaghetti code.

In an attempt to modularize Shopify's monolith and enforce boundaries, they tried different solutions without achieving the expected results:

  • Setting private constants,
  • Establishing boundaries through gems,
  • Using tests to prevent cross-component associations,
  • Using Ruby‘s Modulation gem,
  • Creating microservices.

With all the knowledge from previous attempts, they decided to create their own tool: Packwerk.

Packwerk

What is Packwerk?

Packwerk is a static analysis tool used to enforce boundaries between groups of Ruby files called packages.

What is a package?

A package is a folder containing autoloaded code. Shopify's team encourages to use the best design practices while creating packages.

  • We should pack together things that have high functional cohesion,

  • Packages should be relatively loosely coupled with each other.

Types of boundary checks

We can enforce privacy and dependency boundaries, check violations of the boundaries and cyclic dependencies.

Packwerk into practice

There is not a single specific way to structure or re-structure your application while creating packages. In this article, we are going to follow the approach suggested by Stephan Hagemann in Gradual Modularization for Ruby on Rails.

Choose the project

You can create a new project or choose one of your projects. I decided to use an open-source project called CodeTriage. It is important to mention that we need a Rails 6 application since Packwerk uses Zeitwerk.

Initialize Packwerk

First, we need to add the gem to our Gemfile like gem 'packwerk' and then run bundlein the console. Then we are ready to initialize the gem running packwerk init.

After that, we notice that Packwerk generated three files for us:

  • packwerk.yml

  • package.yml

  • inflections.yml

packwerk.yml is the configuration file of Packwerk where we will define included and excluded files, list the load paths, define the inflections file, among other things;

package.yml is the configuration file of a package. In this file, we will add the configuration for the boundaries of our package. Any folder with package.yml will be recognized as a package by Packwerk. That's it, Packwerk created our first package and we call it the root package.

inflections.yml is where we will place our custom inflections and acronyms in case we are using them.

You can find more about the files and their configuration in Packwerk.

Packwerk properties

For modularization to work we need three basic properties: a named container, its content, and explicit dependencies on other containers. So let's define those properties in Packwerk:

  • Name: The name of a package is its relative path from the root of the

application.

  • Content: When we place a package.yml into a folder, all the files in the folder are now the content of the package.

  • Dependencies: We can define dependencies on other packages adding dependencies key to the package.yml.

Another file that's not included by default but is recommended is README. It is important to provide information about the usage of the package.

The End of Episode I

Digital product development consulting

Read More

GraphQL Ruby. What about performance?

Rails and Other Means of Transport

Rails Development with TMUX, Vim, Fzf + Ripgrep