banner

Writing maintainable code is one of the most essential aspects of software development, and it becomes even more critical in large-scale Ruby applications. Clean Architecture, a set of principles introduced by Robert C. Martin (Uncle Bob), provides a solid foundation for structuring code in a way that maximizes flexibility, testability, and maintainability.

In this post, we’ll explore how to apply Clean Architecture principles to Ruby code, ensuring your applications remain clean, scalable, and easy to maintain over time.

What is Clean Architecture?

Clean Architecture is an approach to software design that separates an application’s concerns into distinct layers. This separation allows for easier maintenance, testing, and scalability. It emphasizes the following key principles:

  • Separation of concerns: Each component or module should have a single responsibility and be loosely coupled to others.
  • Independence of frameworks: The core business logic should be independent of external libraries, frameworks, and databases.
  • Testability: Code should be structured in a way that makes it easy to write tests.
  • Flexibility: The architecture should allow easy modification and extension without affecting the rest of the system.

In Clean Architecture, the application is divided into concentric circles, each representing different layers of responsibility:

  1. Entities: The core business objects.
  2. Use Cases: Application-specific business rules.
  3. Interface Adapters: The UI, web controllers, and other components that interface with the outside world.
  4. Frameworks & Drivers: External libraries, databases, and frameworks like Rails or Sinatra.

The key idea is that dependencies should always point inward, from outer layers to inner layers, and the core business logic should never depend on external libraries.

1. Structuring Your Ruby Code

One of the first steps in applying Clean Architecture to Ruby applications is structuring your code correctly. A typical Ruby application built with Clean Architecture might look like this:

  • app/ – Contains all the core application logic.
    • entities/ – Defines the business objects and entities (e.g., User, Order).
    • use_cases/ – Contains the application logic (e.g., CreateUser, PlaceOrder).
    • interface_adapters/ – Handles interactions with the outside world, like controllers and serializers.
    • frameworks/ – Contains external dependencies like database models, third-party libraries, and web frameworks.

This structure helps keep the application’s core business logic isolated from external concerns like databases, APIs, and frameworks.

2. Focusing on Single Responsibility

The Single Responsibility Principle (SRP) is one of the most important principles in Clean Architecture. Each class, method, and module should have a single responsibility and should not be concerned with anything outside that responsibility.

In Ruby, this can be achieved by breaking down large classes and methods into smaller, more focused ones. For example, if you have a User class that handles both user data validation and communication with the database, consider splitting it into two classes: one for validation and another for persistence.

Example: Splitting Responsibilities
# Before: One class handling both responsibilities
class User
def initialize(name, email)
@name = name
@email = email
end

def save
# Validation and database logic together
if valid?
Database.save(self)
else
raise 'Invalid User'
end
end

def valid?
# Validation logic
!@name.empty? && !@email.empty?
end
end

# After: Split into two classes
class UserValidator
def initialize(user)
@user = user
end

def valid?
!@user.name.empty? && !@user.email.empty?
end
end

class UserPersistence
def self.save(user)
Database.save(user)
end
end

This separation allows for easier testing and modification. Each class has a single responsibility, making your code cleaner and more maintainable.

3. Dependency Injection for Flexibility

In Clean Architecture, it’s important to minimize dependencies between layers. One way to achieve this is through dependency injection. Instead of directly creating instances of classes within other classes, we inject dependencies via constructors or setters.

This approach allows you to easily swap out implementations, which is especially useful for testing. For example, you can inject a mock service or repository when writing unit tests.

Example: Dependency Injection in Ruby
class UserService
def initialize(user_repository, email_service)
@user_repository = user_repository
@email_service = email_service
end

def create_user(name, email)
user = User.new(name, email)
@user_repository.save(user)
@email_service.send_welcome_email(user)
end
end

Here, UserService depends on the user_repository and email_service. These dependencies are injected into the service, making it easy to replace them in tests or swap them out with different implementations.

4. Writing Testable Code

Clean Architecture heavily emphasizes writing code that is easy to test. Since the core business logic is separated from external concerns, it becomes much easier to write unit tests for individual components.

For example, the UserService class in the previous example can be easily tested by mocking the user_repository and email_service.

Example: Testing UserService
require 'minitest/autorun'

class TestUserService < Minitest::Test
def setup
@user_repository = Minitest::Mock.new
@email_service = Minitest::Mock.new
@user_service = UserService.new(@user_repository, @email_service)
end

def test_create_user
user = User.new('John Doe', 'john@example.com')

    @user_repository.expect(:save, true, [user])
    @email_service.expect(:send_welcome_email, true, [user])
    
    @user_service.create_user('John Doe', 'john@example.com')
    
    @user_repository.verify
    @email_service.verify
end
end

This test ensures that the UserService interacts correctly with the repository and email service, without being concerned with their actual implementations.

5. Benefits of Clean Architecture in Ruby

  • Scalability: As your application grows, Clean Architecture helps manage complexity by separating concerns and making it easier to extend and modify the application.
  • Testability: The structure of the application encourages the writing of unit tests for individual components.
  • Maintainability: The separation of concerns and clear organization of code makes the system easier to maintain and refactor.
  • Flexibility: Changes in external frameworks or libraries (e.g., switching databases) have minimal impact on the core business logic.

Conclusion

Writing maintainable Ruby code is all about making smart design decisions. By adopting Clean Architecture principles, you can ensure that your Ruby applications remain modular, scalable, and easy to test. The separation of concerns, dependency injection, and focus on single responsibilities make your codebase more flexible and maintainable in the long run.

Whether you’re building a small app or an enterprise-grade system, applying these principles will make your code more sustainable and easier to manage as your project grows.