Remove Circular Dependencies by Using Dependency Injection and the Repository Pattern in Ruby

There are dependencies between gems and the platforms that use them. In scenarios where the platforms have the data and the gem has the knowledge, there is a direct circular dependency between the two and both need to talk to each other. I’ll show you how we used the Repository pattern in Ruby to remove that circular dependency and help us make gems thin and stateless. Plus, I’ll show you how using Sorbet in the implementation made our code typed and cleaner.

Ignacio Chiazzo
8 min readbeginner
--
View Original

Overview

This article discusses how to eliminate circular dependencies in Ruby applications by utilizing the Repository pattern and Dependency Injection. It highlights the implementation process within Shopify's architecture, emphasizing the importance of clean separation between data and logic, and the use of Sorbet for type safety.

What You'll Learn

1

How to implement the Repository pattern to remove circular dependencies in Ruby applications

2

Why using Dependency Injection improves code maintainability and testability

3

How to leverage Sorbet for type safety in Ruby code

Prerequisites & Requirements

  • Understanding of Ruby programming and design patterns
  • Familiarity with Sorbet for type checking in Ruby(optional)

Key Questions Answered

What is the Repository pattern and how does it help in Ruby applications?
The Repository pattern acts as a mediator between the domain and data mapping layers, allowing for a clean separation of concerns. It enables the gem to remain stateless while defining a contract that consumers must implement, thus eliminating direct circular dependencies.
How does Dependency Injection work in the context of Ruby gems?
Dependency Injection allows the gem to receive implementations of the required interfaces from consumers, rather than creating dependencies directly. This promotes loose coupling and enhances testability by enabling the use of mocks during testing.
What role does Sorbet play in the implementation process?
Sorbet enforces type safety by requiring that each function in the Repository implementation returns the expected types. This ensures that consumers adhere to the contract defined by the gem, reducing runtime errors and improving code quality.
What steps are involved in implementing the Repository pattern in Ruby?
The implementation involves defining a contract in the gem, creating classes in consumers that implement this contract, and passing these implementations to the gem using Dependency Injection. This structured approach helps maintain a clean architecture.

Technologies & Tools

Some links below are affiliate links. We may earn a commission if you make a purchase.

Backend
Ruby
Used for building the Shopify Core and Storefront Renderer applications.
Tools
Sorbet
Used for type checking and enforcing contracts in Ruby code.

Key Actionable Insights

1
Implement the Repository pattern to decouple your application's logic from data access, which enhances maintainability.
By separating concerns, you can modify the data layer without affecting business logic, making it easier to adapt to changes in requirements or technology.
2
Utilize Dependency Injection to manage dependencies effectively, allowing for easier testing and flexibility in your codebase.
This approach enables you to swap out implementations without changing the dependent code, facilitating unit testing and code reuse.
3
Adopt Sorbet for type checking in your Ruby applications to catch errors early and ensure that your code adheres to defined contracts.
By enforcing type safety, you reduce the likelihood of runtime errors and improve the overall reliability of your application.

Common Pitfalls

1
Failing to define clear contracts between the gem and consumers can lead to runtime errors and tight coupling.
Without well-defined interfaces, changes in one part of the system may inadvertently break functionality in another, making the system harder to maintain.
2
Neglecting to implement proper testing for both the gem and consumer can result in undetected issues.
Testing is crucial to ensure that both components interact correctly and that the gem behaves as expected within the consumer context.

Related Concepts

Dependency Injection
Design Patterns
Type Safety With Sorbet