CrackedRuby CrackedRuby

Overview

Clean Architecture defines a system organization where business logic remains independent of external concerns like databases, web frameworks, or user interfaces. Robert C. Martin introduced this pattern to address common problems in software design: tight coupling, difficulty testing, and resistance to change.

The architecture arranges code in concentric circles, where inner circles contain business logic and outer circles contain implementation details. Dependencies point inward only—outer layers depend on inner layers, never the reverse. This dependency rule ensures that business logic never depends on frameworks, databases, or delivery mechanisms.

Core layers include entities (enterprise business rules), use cases (application business rules), interface adapters (controllers, presenters, gateways), and frameworks/drivers (web frameworks, databases, external interfaces). Each layer serves a specific purpose and maintains clear boundaries.

# Directory structure reflecting Clean Architecture layers
lib/
  entities/           # Enterprise business rules
    user.rb
    order.rb
  use_cases/          # Application business rules
    create_order.rb
    process_payment.rb
  adapters/           # Interface adapters
    controllers/
    presenters/
    gateways/
  frameworks/         # External interfaces
    web/
    persistence/

The pattern addresses several architectural problems. Traditional layered architectures often result in database-centric designs where business logic depends on database schemas. Clean Architecture inverts these dependencies, placing business logic at the center and treating databases as implementation details that can be swapped without affecting core logic.

Clean Architecture differs from related patterns like Hexagonal Architecture and Onion Architecture in terminology and emphasis, but shares the core principle of dependency inversion. All three patterns separate business logic from infrastructure concerns and enforce dependency rules pointing toward the domain.

Key Principles

The Dependency Rule governs all interactions in Clean Architecture: source code dependencies point only inward. Outer circles know nothing about inner circles. Inner circles declare interfaces that outer circles implement. This rule applies at every level—classes, modules, functions, and data structures.

Entities represent enterprise business rules that would exist regardless of any particular application. An entity encapsulates critical business logic that multiple applications might share. Entities change only when fundamental business requirements change, not when UI or database concerns change.

# Entity - pure business logic with no external dependencies
class Order
  attr_reader :id, :items, :customer_id, :status
  
  def initialize(id:, customer_id:, items: [])
    @id = id
    @customer_id = customer_id
    @items = items
    @status = :pending
  end
  
  def add_item(item)
    raise "Cannot modify completed order" if completed?
    @items << item
  end
  
  def total
    items.sum(&:price)
  end
  
  def complete
    raise "Order must have items" if items.empty?
    @status = :completed
  end
  
  def completed?
    status == :completed
  end
end

Use Cases contain application-specific business rules. A use case orchestrates the flow of data between entities and coordinates entity behavior to accomplish a specific application task. Use cases know about entities but remain independent of databases, frameworks, and UI concerns.

# Use Case - application-specific business rules
class CreateOrder
  def initialize(order_repository:, notification_service:)
    @order_repository = order_repository
    @notification_service = notification_service
  end
  
  def execute(customer_id:, items:)
    order = Order.new(
      id: generate_id,
      customer_id: customer_id,
      items: items
    )
    
    @order_repository.save(order)
    @notification_service.notify_order_created(order)
    
    { success: true, order_id: order.id }
  rescue => e
    { success: false, error: e.message }
  end
  
  private
  
  def generate_id
    SecureRandom.uuid
  end
end

Interface Adapters convert data between the format most convenient for use cases and entities and the format most convenient for external agencies like databases or web frameworks. Controllers, presenters, and gateways reside in this layer. Controllers receive requests from the web framework and translate them into use case inputs. Presenters format use case outputs for display. Gateways translate between domain objects and database records.

The Dependency Inversion Principle enables inner layers to remain independent. Inner layers define interfaces (contracts) that outer layers must implement. Use cases depend on repository interfaces, not concrete database implementations. This allows swapping databases, frameworks, or external services without modifying business logic.

# Use case depends on abstraction, not implementation
class ProcessPayment
  def initialize(payment_gateway:)
    @payment_gateway = payment_gateway
  end
  
  def execute(order_id:, amount:, payment_method:)
    result = @payment_gateway.charge(
      amount: amount,
      payment_method: payment_method
    )
    
    if result.success?
      { success: true, transaction_id: result.transaction_id }
    else
      { success: false, error: result.error_message }
    end
  end
end

# Interface defined by use case layer
module PaymentGateway
  def charge(amount:, payment_method:)
    raise NotImplementedError
  end
end

Boundaries between layers remain strict. Inner layers never reference names, classes, functions, or data structures from outer layers. When use cases need external services, they define interfaces that outer layers implement. When outer layers need to invoke use cases, they do so through defined input/output structures.

Data crossing boundaries follows specific rules. Simple data structures cross boundaries, not entity objects. When a controller passes data to a use case, it sends primitive types or simple data transfer objects, not ActiveRecord models. When a use case returns data, it returns basic data structures that presenters can format, not domain entities.

Design Considerations

Clean Architecture trades implementation complexity for long-term maintainability. The pattern requires more initial setup than simpler architectures—defining interfaces, creating adapters, and organizing code into distinct layers demands effort. Systems with straightforward CRUD operations may not justify this overhead, but applications with complex business logic benefit significantly.

The architecture excels when business logic complexity exceeds infrastructure complexity. Applications with intricate business rules, multiple business constraints, or frequently changing requirements gain the most. E-commerce platforms, financial systems, and healthcare applications typically contain sufficient complexity to warrant Clean Architecture's structure.

Testing becomes dramatically simpler because business logic depends only on abstractions. Tests run without databases, web servers, or external services. A test suite that runs in milliseconds rather than minutes becomes achievable.

# Testing use case without any infrastructure
RSpec.describe CreateOrder do
  let(:repository) { instance_double(OrderRepository) }
  let(:notifier) { instance_double(NotificationService) }
  let(:use_case) { CreateOrder.new(
    order_repository: repository,
    notification_service: notifier
  )}
  
  it "creates order and sends notification" do
    items = [Item.new(name: "Widget", price: 10.00)]
    
    allow(repository).to receive(:save)
    allow(notifier).to receive(:notify_order_created)
    
    result = use_case.execute(customer_id: "123", items: items)
    
    expect(result[:success]).to be true
    expect(repository).to have_received(:save)
    expect(notifier).to have_received(:notify_order_created)
  end
end

Framework independence protects against framework obsolescence. Rails, Sinatra, or any web framework becomes a plugin rather than the foundation. When a framework reaches end-of-life or better alternatives emerge, migration affects only outer layers. Business logic remains untouched.

Database independence follows the same principle. PostgreSQL, MongoDB, or Redis can be swapped without altering business rules. Development environments might use SQLite while production uses PostgreSQL. Testing might use in-memory storage while production uses a distributed database.

The pattern introduces indirection that some developers find excessive. Every external interaction requires defining an interface, implementing an adapter, and wiring dependencies. Simple operations like saving a record involve multiple classes. Teams must decide whether this indirection serves their specific needs.

Smaller applications or prototypes may not warrant Clean Architecture's ceremony. A startup validating product-market fit needs rapid iteration more than long-term maintainability. A small utility application with minimal business logic gains little from elaborate layering.

Teams transitioning from framework-centric architectures face a learning curve. Developers accustomed to putting business logic in ActiveRecord models or controllers must learn new patterns. The team needs shared understanding of boundaries and dependency rules.

Gradual adoption provides a practical path. New features can follow Clean Architecture while legacy code remains unchanged. Critical business logic can be extracted first, leaving simple CRUD operations in framework-dependent code. This approach allows teams to learn the pattern incrementally.

Ruby Implementation

Ruby's dynamic nature and module system support Clean Architecture's requirements while introducing specific considerations. Duck typing enables dependency inversion without formal interface definitions, but explicit contracts improve clarity in complex systems.

Entities in Ruby should focus on business behavior rather than persistence. Unlike ActiveRecord models that couple business logic with database concerns, entities represent pure domain concepts. They contain no database knowledge, no associations, and no persistence callbacks.

# Entity with rich business behavior
class Account
  attr_reader :id, :balance, :account_type
  
  def initialize(id:, balance:, account_type:)
    @id = id
    @balance = balance
    @account_type = account_type
  end
  
  def deposit(amount)
    raise ArgumentError, "Amount must be positive" unless amount > 0
    @balance += amount
  end
  
  def withdraw(amount)
    raise ArgumentError, "Amount must be positive" unless amount > 0
    raise InsufficientFunds if amount > available_balance
    @balance -= amount
  end
  
  def available_balance
    case account_type
    when :checking then balance
    when :savings then balance - minimum_balance
    else raise "Unknown account type"
    end
  end
  
  private
  
  def minimum_balance
    100.00
  end
  
  class InsufficientFunds < StandardError; end
end

Use cases coordinate business operations using dependency injection. Constructor injection makes dependencies explicit and testable. Ruby's keyword arguments provide clear dependency specification.

class TransferFunds
  def initialize(account_repository:, transaction_repository:, event_publisher:)
    @account_repository = account_repository
    @transaction_repository = transaction_repository
    @event_publisher = event_publisher
  end
  
  def execute(from_account_id:, to_account_id:, amount:)
    from_account = @account_repository.find(from_account_id)
    to_account = @account_repository.find(to_account_id)
    
    return failure("Source account not found") unless from_account
    return failure("Destination account not found") unless to_account
    
    from_account.withdraw(amount)
    to_account.deposit(amount)
    
    @account_repository.save(from_account)
    @account_repository.save(to_account)
    
    transaction = create_transaction(from_account_id, to_account_id, amount)
    @transaction_repository.save(transaction)
    
    @event_publisher.publish(:transfer_completed, transaction)
    
    success(transaction_id: transaction.id)
  rescue Account::InsufficientFunds
    failure("Insufficient funds")
  rescue => e
    failure("Transfer failed: #{e.message}")
  end
  
  private
  
  def create_transaction(from_id, to_id, amount)
    Transaction.new(
      id: SecureRandom.uuid,
      from_account_id: from_id,
      to_account_id: to_id,
      amount: amount,
      timestamp: Time.now
    )
  end
  
  def success(data = {})
    { success: true }.merge(data)
  end
  
  def failure(message)
    { success: false, error: message }
  end
end

Repository adapters translate between domain entities and persistence mechanisms. Repositories present a collection-like interface to use cases while handling database operations internally.

# Repository adapter for database persistence
class PostgresAccountRepository
  def initialize(database)
    @database = database
  end
  
  def find(id)
    row = @database.execute(
      "SELECT * FROM accounts WHERE id = ?", 
      id
    ).first
    
    return nil unless row
    
    Account.new(
      id: row['id'],
      balance: BigDecimal(row['balance']),
      account_type: row['account_type'].to_sym
    )
  end
  
  def save(account)
    @database.execute(
      "INSERT INTO accounts (id, balance, account_type) 
       VALUES (?, ?, ?)
       ON CONFLICT (id) DO UPDATE 
       SET balance = ?, account_type = ?",
      account.id, 
      account.balance, 
      account.account_type.to_s,
      account.balance,
      account.account_type.to_s
    )
  end
  
  def all
    rows = @database.execute("SELECT * FROM accounts")
    rows.map { |row| hydrate(row) }
  end
  
  private
  
  def hydrate(row)
    Account.new(
      id: row['id'],
      balance: BigDecimal(row['balance']),
      account_type: row['account_type'].to_sym
    )
  end
end

Controllers in Ruby web frameworks serve as adapters between HTTP and use cases. They parse request parameters, invoke use cases, and format responses. Controllers contain no business logic.

# Rails controller as an adapter
class AccountsController < ApplicationController
  def transfer
    result = transfer_use_case.execute(
      from_account_id: params[:from_account_id],
      to_account_id: params[:to_account_id],
      amount: BigDecimal(params[:amount])
    )
    
    if result[:success]
      render json: { 
        transaction_id: result[:transaction_id] 
      }, status: :created
    else
      render json: { 
        error: result[:error] 
      }, status: :unprocessable_entity
    end
  end
  
  private
  
  def transfer_use_case
    TransferFunds.new(
      account_repository: account_repository,
      transaction_repository: transaction_repository,
      event_publisher: event_publisher
    )
  end
  
  def account_repository
    @account_repository ||= PostgresAccountRepository.new(database)
  end
  
  def transaction_repository
    @transaction_repository ||= PostgresTransactionRepository.new(database)
  end
  
  def event_publisher
    @event_publisher ||= RedisEventPublisher.new(redis_client)
  end
  
  def database
    @database ||= Database.new(ENV['DATABASE_URL'])
  end
  
  def redis_client
    @redis_client ||= Redis.new(url: ENV['REDIS_URL'])
  end
end

Dependency injection containers simplify wiring in Ruby applications. Dry-container provides a lightweight container that manages dependencies.

require 'dry-container'
require 'dry-auto_inject'

class Container
  extend Dry::Container::Mixin
  
  register(:database) { Database.new(ENV['DATABASE_URL']) }
  register(:redis_client) { Redis.new(url: ENV['REDIS_URL']) }
  
  register(:account_repository) { 
    PostgresAccountRepository.new(resolve(:database)) 
  }
  
  register(:transaction_repository) { 
    PostgresTransactionRepository.new(resolve(:database)) 
  }
  
  register(:event_publisher) { 
    RedisEventPublisher.new(resolve(:redis_client)) 
  }
  
  register(:transfer_funds) {
    TransferFunds.new(
      account_repository: resolve(:account_repository),
      transaction_repository: resolve(:transaction_repository),
      event_publisher: resolve(:event_publisher)
    )
  }
end

Import = Dry::AutoInject(Container)

class AccountsController < ApplicationController
  include Import[:transfer_funds]
  
  def transfer
    result = transfer_funds.execute(
      from_account_id: params[:from_account_id],
      to_account_id: params[:to_account_id],
      amount: BigDecimal(params[:amount])
    )
    
    # Handle result...
  end
end

Ruby's module system enables defining explicit contracts even without static typing. Modules document expected interfaces and can validate implementations at runtime during development.

module AccountRepository
  def find(id)
    raise NotImplementedError
  end
  
  def save(account)
    raise NotImplementedError
  end
  
  def all
    raise NotImplementedError
  end
end

class PostgresAccountRepository
  include AccountRepository
  
  def initialize(database)
    @database = database
  end
  
  def find(id)
    # Implementation
  end
  
  def save(account)
    # Implementation
  end
  
  def all
    # Implementation
  end
end

Common Patterns

The Repository pattern provides collection-like access to entities while encapsulating storage details. Repositories expose methods like find, save, delete, and query operations that return domain entities, never database records.

class UserRepository
  def find_by_email(email)
    row = database.execute(
      "SELECT * FROM users WHERE email = ?", 
      email
    ).first
    
    return nil unless row
    hydrate_user(row)
  end
  
  def find_active_users
    rows = database.execute(
      "SELECT * FROM users WHERE status = 'active'"
    )
    
    rows.map { |row| hydrate_user(row) }
  end
  
  def save(user)
    if exists?(user.id)
      update_user(user)
    else
      insert_user(user)
    end
  end
  
  private
  
  def hydrate_user(row)
    User.new(
      id: row['id'],
      email: row['email'],
      name: row['name'],
      status: row['status'].to_sym
    )
  end
  
  def exists?(id)
    database.execute(
      "SELECT 1 FROM users WHERE id = ?", 
      id
    ).any?
  end
  
  def insert_user(user)
    database.execute(
      "INSERT INTO users (id, email, name, status) VALUES (?, ?, ?, ?)",
      user.id, user.email, user.name, user.status.to_s
    )
  end
  
  def update_user(user)
    database.execute(
      "UPDATE users SET email = ?, name = ?, status = ? WHERE id = ?",
      user.email, user.name, user.status.to_s, user.id
    )
  end
end

The Use Case pattern encapsulates single application operations. Each use case represents one user intent or system operation. Use cases coordinate entities, repositories, and external services to accomplish specific goals.

class RegisterUser
  def initialize(user_repository:, email_service:, password_hasher:)
    @user_repository = user_repository
    @email_service = email_service
    @password_hasher = password_hasher
  end
  
  def execute(email:, password:, name:)
    return failure("Email already registered") if email_taken?(email)
    return failure("Invalid email format") unless valid_email?(email)
    return failure("Password too weak") unless strong_password?(password)
    
    user = User.new(
      id: SecureRandom.uuid,
      email: email,
      name: name,
      password_hash: @password_hasher.hash(password),
      status: :pending_verification
    )
    
    @user_repository.save(user)
    @email_service.send_verification(user.email, user.verification_token)
    
    success(user_id: user.id)
  rescue => e
    failure("Registration failed: #{e.message}")
  end
  
  private
  
  def email_taken?(email)
    @user_repository.find_by_email(email) != nil
  end
  
  def valid_email?(email)
    email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
  end
  
  def strong_password?(password)
    password.length >= 12 && 
    password.match?(/[A-Z]/) && 
    password.match?(/[a-z]/) && 
    password.match?(/[0-9]/)
  end
  
  def success(data)
    { success: true }.merge(data)
  end
  
  def failure(message)
    { success: false, error: message }
  end
end

The Presenter pattern formats use case output for display. Presenters transform domain data into view-specific formats, handling concerns like date formatting, currency display, and conditional rendering.

class OrderPresenter
  def initialize(order)
    @order = order
  end
  
  def as_json
    {
      id: @order.id,
      customer_id: @order.customer_id,
      items: formatted_items,
      total: formatted_total,
      status: status_display,
      created_at: formatted_date(@order.created_at)
    }
  end
  
  def as_html
    <<~HTML
      <div class="order" data-order-id="#{@order.id}">
        <h3>Order #{@order.id}</h3>
        <div class="status #{status_class}">#{status_display}</div>
        <div class="total">#{formatted_total}</div>
        <ul class="items">
          #{item_list_html}
        </ul>
      </div>
    HTML
  end
  
  private
  
  def formatted_items
    @order.items.map do |item|
      {
        name: item.name,
        quantity: item.quantity,
        price: format_currency(item.price)
      }
    end
  end
  
  def formatted_total
    format_currency(@order.total)
  end
  
  def status_display
    @order.status.to_s.split('_').map(&:capitalize).join(' ')
  end
  
  def status_class
    case @order.status
    when :completed then 'success'
    when :cancelled then 'danger'
    when :pending then 'warning'
    else 'info'
    end
  end
  
  def formatted_date(date)
    date.strftime("%B %d, %Y at %I:%M %p")
  end
  
  def format_currency(amount)
    "$%.2f" % amount
  end
  
  def item_list_html
    @order.items.map do |item|
      "<li>#{item.name} - #{item.quantity} × #{format_currency(item.price)}</li>"
    end.join("\n")
  end
end

The Gateway pattern wraps external services with domain-friendly interfaces. Gateways handle external API communication, data format translation, and error handling while presenting clean interfaces to use cases.

class StripePaymentGateway
  def initialize(api_key:)
    @api_key = api_key
    @client = Stripe::Client.new(api_key)
  end
  
  def charge(amount:, currency:, payment_method:, description:)
    response = @client.charges.create(
      amount: (amount * 100).to_i,
      currency: currency,
      source: payment_method.token,
      description: description
    )
    
    PaymentResult.success(
      transaction_id: response.id,
      amount: amount,
      timestamp: Time.at(response.created)
    )
  rescue Stripe::CardError => e
    PaymentResult.failure(
      error_code: e.code,
      error_message: e.message
    )
  rescue Stripe::APIError => e
    PaymentResult.failure(
      error_code: 'api_error',
      error_message: 'Payment service temporarily unavailable'
    )
  end
  
  def refund(transaction_id:, amount:)
    response = @client.refunds.create(
      charge: transaction_id,
      amount: (amount * 100).to_i
    )
    
    RefundResult.success(
      refund_id: response.id,
      amount: amount
    )
  rescue Stripe::APIError => e
    RefundResult.failure(error_message: e.message)
  end
end

class PaymentResult
  attr_reader :success, :transaction_id, :amount, :timestamp, :error_code, :error_message
  
  def self.success(transaction_id:, amount:, timestamp:)
    new(
      success: true,
      transaction_id: transaction_id,
      amount: amount,
      timestamp: timestamp
    )
  end
  
  def self.failure(error_code:, error_message:)
    new(
      success: false,
      error_code: error_code,
      error_message: error_message
    )
  end
  
  def initialize(success:, transaction_id: nil, amount: nil, timestamp: nil, 
                 error_code: nil, error_message: nil)
    @success = success
    @transaction_id = transaction_id
    @amount = amount
    @timestamp = timestamp
    @error_code = error_code
    @error_message = error_message
  end
  
  def success?
    @success
  end
end

The Value Object pattern represents immutable concepts without identity. Value objects enable expressing domain concepts precisely and ensure invariants hold.

class Money
  attr_reader :amount, :currency
  
  def initialize(amount, currency)
    raise ArgumentError, "Amount cannot be negative" if amount < 0
    raise ArgumentError, "Invalid currency" unless valid_currency?(currency)
    
    @amount = amount
    @currency = currency
    freeze
  end
  
  def +(other)
    raise ArgumentError, "Currency mismatch" unless currency == other.currency
    Money.new(amount + other.amount, currency)
  end
  
  def -(other)
    raise ArgumentError, "Currency mismatch" unless currency == other.currency
    Money.new(amount - other.amount, currency)
  end
  
  def *(multiplier)
    Money.new(amount * multiplier, currency)
  end
  
  def ==(other)
    other.is_a?(Money) && 
    amount == other.amount && 
    currency == other.currency
  end
  
  def to_s
    "#{currency} #{format('%.2f', amount)}"
  end
  
  private
  
  def valid_currency?(currency)
    %i[USD EUR GBP JPY].include?(currency)
  end
end

Practical Examples

A task management application demonstrates Clean Architecture organization. The system manages tasks, projects, and user assignments with complex business rules around task dependencies and project workflows.

# Entity: Task with business rules
class Task
  attr_reader :id, :title, :description, :status, :project_id, :assignee_id, :due_date
  
  def initialize(id:, title:, project_id:, description: nil, assignee_id: nil, due_date: nil)
    @id = id
    @title = title
    @description = description
    @project_id = project_id
    @assignee_id = assignee_id
    @due_date = due_date
    @status = :pending
    @dependencies = []
  end
  
  def assign_to(user_id)
    @assignee_id = user_id
  end
  
  def add_dependency(task_id)
    @dependencies << task_id unless @dependencies.include?(task_id)
  end
  
  def start
    raise "Cannot start task with incomplete dependencies" unless dependencies_completed?
    raise "Task must be assigned" unless @assignee_id
    @status = :in_progress
  end
  
  def complete
    raise "Task not started" unless @status == :in_progress
    @status = :completed
  end
  
  def overdue?
    @due_date && @due_date < Date.today && @status != :completed
  end
  
  def blocked?
    !dependencies_completed?
  end
  
  private
  
  def dependencies_completed?
    # Will be checked by use case with repository
    @dependencies.empty?
  end
end

# Use Case: Complete Task
class CompleteTask
  def initialize(task_repository:, notification_service:, activity_logger:)
    @task_repository = task_repository
    @notification_service = notification_service
    @activity_logger = activity_logger
  end
  
  def execute(task_id:, completed_by:)
    task = @task_repository.find(task_id)
    return failure("Task not found") unless task
    
    task.complete
    @task_repository.save(task)
    
    @activity_logger.log(
      event: :task_completed,
      task_id: task.id,
      user_id: completed_by,
      timestamp: Time.now
    )
    
    dependent_tasks = @task_repository.find_dependent_on(task_id)
    notify_dependent_tasks(dependent_tasks)
    
    success(task_id: task.id, status: task.status)
  rescue => e
    failure("Failed to complete task: #{e.message}")
  end
  
  private
  
  def notify_dependent_tasks(tasks)
    unblocked = tasks.select { |t| !t.blocked? }
    unblocked.each do |task|
      @notification_service.notify_task_ready(
        task_id: task.id,
        assignee_id: task.assignee_id
      )
    end
  end
  
  def success(data)
    { success: true }.merge(data)
  end
  
  def failure(message)
    { success: false, error: message }
  end
end

# Repository Implementation
class SequelTaskRepository
  def initialize(database)
    @db = database
  end
  
  def find(id)
    row = @db[:tasks].where(id: id).first
    return nil unless row
    
    hydrate_task(row)
  end
  
  def save(task)
    data = {
      title: task.title,
      description: task.description,
      status: task.status.to_s,
      project_id: task.project_id,
      assignee_id: task.assignee_id,
      due_date: task.due_date
    }
    
    if @db[:tasks].where(id: task.id).any?
      @db[:tasks].where(id: task.id).update(data)
    else
      @db[:tasks].insert(data.merge(id: task.id))
    end
  end
  
  def find_dependent_on(task_id)
    rows = @db[:task_dependencies]
      .where(dependency_id: task_id)
      .select(:task_id)
    
    task_ids = rows.map { |r| r[:task_id] }
    tasks = @db[:tasks].where(id: task_ids).all
    
    tasks.map { |row| hydrate_task(row) }
  end
  
  private
  
  def hydrate_task(row)
    task = Task.new(
      id: row[:id],
      title: row[:title],
      project_id: row[:project_id],
      description: row[:description],
      assignee_id: row[:assignee_id],
      due_date: row[:due_date]
    )
    
    # Reconstitute status
    task.instance_variable_set(:@status, row[:status].to_sym)
    
    task
  end
end

# Controller Adapter
class TasksController
  include Import[
    :complete_task_use_case,
    :task_presenter
  ]
  
  def complete
    result = complete_task_use_case.execute(
      task_id: params[:id],
      completed_by: current_user_id
    )
    
    if result[:success]
      task = task_repository.find(result[:task_id])
      render json: task_presenter.as_json(task), status: :ok
    else
      render json: { error: result[:error] }, status: :unprocessable_entity
    end
  end
end

An e-commerce checkout process illustrates coordinating multiple use cases and external services.

# Use Case: Process Checkout
class ProcessCheckout
  def initialize(
    cart_repository:,
    order_repository:,
    inventory_service:,
    payment_gateway:,
    shipping_calculator:,
    email_service:
  )
    @cart_repository = cart_repository
    @order_repository = order_repository
    @inventory_service = inventory_service
    @payment_gateway = payment_gateway
    @shipping_calculator = shipping_calculator
    @email_service = email_service
  end
  
  def execute(cart_id:, shipping_address:, payment_method:)
    cart = @cart_repository.find(cart_id)
    return failure("Cart not found") unless cart
    return failure("Cart is empty") if cart.empty?
    
    # Verify inventory
    inventory_check = @inventory_service.check_availability(cart.items)
    return failure("Items out of stock: #{inventory_check.unavailable}") unless inventory_check.available?
    
    # Calculate shipping
    shipping_cost = @shipping_calculator.calculate(
      items: cart.items,
      destination: shipping_address
    )
    
    # Create order
    order = Order.new(
      id: SecureRandom.uuid,
      customer_id: cart.customer_id,
      items: cart.items,
      shipping_address: shipping_address,
      shipping_cost: shipping_cost
    )
    
    # Process payment
    total_amount = order.subtotal + shipping_cost.amount
    payment_result = @payment_gateway.charge(
      amount: total_amount,
      currency: :USD,
      payment_method: payment_method,
      description: "Order #{order.id}"
    )
    
    return failure("Payment failed: #{payment_result.error_message}") unless payment_result.success?
    
    # Reserve inventory
    @inventory_service.reserve(order.items, order.id)
    
    # Save order
    order.confirm_payment(payment_result.transaction_id)
    @order_repository.save(order)
    
    # Clear cart
    @cart_repository.delete(cart_id)
    
    # Send confirmation
    @email_service.send_order_confirmation(
      email: order.customer_email,
      order: order
    )
    
    success(order_id: order.id, total: total_amount)
  rescue => e
    # Rollback payment if order save fails
    @payment_gateway.refund(payment_result.transaction_id) if payment_result&.success?
    failure("Checkout failed: #{e.message}")
  end
  
  private
  
  def success(data)
    { success: true }.merge(data)
  end
  
  def failure(message)
    { success: false, error: message }
  end
end

A reporting system demonstrates read-side patterns and query optimization within Clean Architecture.

# Query Use Case for reporting
class GenerateSalesReport
  def initialize(sales_repository:, report_formatter:)
    @sales_repository = sales_repository
    @report_formatter = report_formatter
  end
  
  def execute(start_date:, end_date:, group_by: :day, format: :json)
    sales_data = @sales_repository.find_in_range(start_date, end_date)
    
    grouped_data = group_sales(sales_data, group_by)
    metrics = calculate_metrics(grouped_data)
    
    report = SalesReport.new(
      period: { start: start_date, end: end_date },
      group_by: group_by,
      data: grouped_data,
      metrics: metrics
    )
    
    formatted_report = @report_formatter.format(report, format)
    
    success(report: formatted_report)
  rescue => e
    failure("Report generation failed: #{e.message}")
  end
  
  private
  
  def group_sales(sales, group_by)
    case group_by
    when :day
      sales.group_by { |s| s.date.to_date }
    when :week
      sales.group_by { |s| s.date.beginning_of_week }
    when :month
      sales.group_by { |s| s.date.beginning_of_month }
    end
  end
  
  def calculate_metrics(grouped_data)
    {
      total_revenue: grouped_data.values.flatten.sum(&:amount),
      average_order_value: grouped_data.values.flatten.sum(&:amount) / 
                          grouped_data.values.flatten.size.to_f,
      total_orders: grouped_data.values.flatten.size,
      periods: grouped_data.size
    }
  end
  
  def success(data)
    { success: true }.merge(data)
  end
  
  def failure(message)
    { success: false, error: message }
  end
end

class SalesReport
  attr_reader :period, :group_by, :data, :metrics
  
  def initialize(period:, group_by:, data:, metrics:)
    @period = period
    @group_by = group_by
    @data = data
    @metrics = metrics
  end
end

Testing Approaches

Testing entities focuses on business logic in isolation. Entity tests verify business rules, state transitions, and invariants without any infrastructure dependencies.

RSpec.describe Order do
  describe "#add_item" do
    it "adds item to pending order" do
      order = Order.new(id: "1", customer_id: "user123")
      item = Item.new(name: "Widget", price: 10.00)
      
      order.add_item(item)
      
      expect(order.items).to include(item)
    end
    
    it "raises error when adding to completed order" do
      order = Order.new(id: "1", customer_id: "user123")
      order.instance_variable_set(:@status, :completed)
      item = Item.new(name: "Widget", price: 10.00)
      
      expect { order.add_item(item) }.to raise_error("Cannot modify completed order")
    end
  end
  
  describe "#complete" do
    it "completes order with items" do
      order = Order.new(id: "1", customer_id: "user123")
      order.add_item(Item.new(name: "Widget", price: 10.00))
      
      order.complete
      
      expect(order.completed?).to be true
    end
    
    it "raises error when completing empty order" do
      order = Order.new(id: "1", customer_id: "user123")
      
      expect { order.complete }.to raise_error("Order must have items")
    end
  end
end

Use case tests verify orchestration logic using test doubles for dependencies. Tests confirm the use case coordinates entities and services correctly.

RSpec.describe CreateOrder do
  let(:order_repository) { instance_double(OrderRepository) }
  let(:notification_service) { instance_double(NotificationService) }
  let(:use_case) { 
    CreateOrder.new(
      order_repository: order_repository,
      notification_service: notification_service
    ) 
  }
  
  describe "#execute" do
    let(:items) { [Item.new(name: "Widget", price: 10.00)] }
    
    it "creates order and sends notification" do
      allow(order_repository).to receive(:save)
      allow(notification_service).to receive(:notify_order_created)
      
      result = use_case.execute(customer_id: "123", items: items)
      
      expect(result[:success]).to be true
      expect(result[:order_id]).to be_present
      expect(order_repository).to have_received(:save)
      expect(notification_service).to have_received(:notify_order_created)
    end
    
    it "returns failure when repository raises error" do
      allow(order_repository).to receive(:save).and_raise("Database error")
      allow(notification_service).to receive(:notify_order_created)
      
      result = use_case.execute(customer_id: "123", items: items)
      
      expect(result[:success]).to be false
      expect(result[:error]).to include("Database error")
      expect(notification_service).not_to have_received(:notify_order_created)
    end
  end
end

Repository tests verify data persistence and retrieval. In-memory implementations enable fast testing without databases, while integration tests verify actual database behavior.

# Shared examples for any repository implementation
RSpec.shared_examples "order repository" do
  describe "#save and #find" do
    it "persists and retrieves order" do
      order = Order.new(
        id: "order123",
        customer_id: "user456",
        items: [Item.new(name: "Widget", price: 10.00)]
      )
      
      repository.save(order)
      retrieved = repository.find("order123")
      
      expect(retrieved.id).to eq(order.id)
      expect(retrieved.customer_id).to eq(order.customer_id)
      expect(retrieved.items.size).to eq(order.items.size)
    end
    
    it "returns nil for non-existent order" do
      result = repository.find("nonexistent")
      
      expect(result).to be_nil
    end
  end
end

# Test in-memory implementation
RSpec.describe InMemoryOrderRepository do
  let(:repository) { InMemoryOrderRepository.new }
  
  include_examples "order repository"
end

# Test database implementation
RSpec.describe PostgresOrderRepository do
  let(:database) { test_database }
  let(:repository) { PostgresOrderRepository.new(database) }
  
  before do
    database.run("DELETE FROM orders")
  end
  
  include_examples "order repository"
end

Integration tests verify the complete flow through all layers. These tests use real implementations rather than test doubles.

RSpec.describe "Complete order flow", type: :integration do
  let(:container) { build_test_container }
  let(:create_order) { container.resolve(:create_order_use_case) }
  let(:complete_order) { container.resolve(:complete_order_use_case) }
  let(:order_repository) { container.resolve(:order_repository) }
  
  it "creates and completes order end-to-end" do
    items = [
      Item.new(name: "Widget", price: 10.00),
      Item.new(name: "Gadget", price: 20.00)
    ]
    
    # Create order
    create_result = create_order.execute(
      customer_id: "user123",
      items: items
    )
    expect(create_result[:success]).to be true
    order_id = create_result[:order_id]
    
    # Verify order saved
    order = order_repository.find(order_id)
    expect(order).not_to be_nil
    expect(order.status).to eq(:pending)
    
    # Complete order
    complete_result = complete_order.execute(order_id: order_id)
    expect(complete_result[:success]).to be true
    
    # Verify completion
    completed_order = order_repository.find(order_id)
    expect(completed_order.status).to eq(:completed)
  end
end

def build_test_container
  Container.new.tap do |c|
    c.register(:database) { test_database }
    c.register(:notification_service) { FakeNotificationService.new }
    c.register(:order_repository) { 
      PostgresOrderRepository.new(c.resolve(:database)) 
    }
    c.register(:create_order_use_case) {
      CreateOrder.new(
        order_repository: c.resolve(:order_repository),
        notification_service: c.resolve(:notification_service)
      )
    }
    c.register(:complete_order_use_case) {
      CompleteOrder.new(
        order_repository: c.resolve(:order_repository)
      )
    }
  end
end

Reference

Layer Responsibilities

Layer Purpose Dependencies Examples
Entities Enterprise business rules None User, Order, Account, Payment
Use Cases Application business rules Entities CreateOrder, ProcessPayment, RegisterUser
Interface Adapters Data format conversion Use Cases, Entities Controllers, Presenters, Repositories, Gateways
Frameworks & Drivers External interfaces Interface Adapters Web Framework, Database, External APIs

Dependency Rules

Allowed Prohibited Reason
Use Case depends on Entity interface Entity depends on Use Case Entities must remain pure business logic
Controller depends on Use Case Use Case depends on Controller Use cases should not know about delivery mechanisms
Repository implements interface defined by Use Case Use Case depends on concrete Repository Enables testing without database
Gateway wraps external API Use Case calls external API directly Isolates external dependencies

Common Use Case Structure

Component Purpose Required
Constructor Accept dependencies via injection Yes
Execute method Perform single operation Yes
Input validation Verify preconditions Recommended
Entity coordination Orchestrate business logic As needed
Repository calls Persist/retrieve data As needed
Return value Success/failure indicator with data Yes

Repository Interface Pattern

Method Pattern Purpose Return Type
find(id) Retrieve single entity Entity or nil
save(entity) Persist entity void or boolean
delete(id) Remove entity void or boolean
all Retrieve all entities Array of entities
find_by_attribute(value) Query by specific field Entity or nil
find_where(criteria) Query with conditions Array of entities

Entity Design Guidelines

Principle Description Example
No persistence knowledge Entities never reference database No ActiveRecord inheritance
Rich behavior Business logic in entity methods Order.complete, Account.withdraw
Invariant protection Constructor enforces valid state Validate required fields
Immutability where appropriate Value objects should be frozen Money, Address, DateRange
No framework coupling No Rails, Sinatra, or framework dependencies Pure Ruby objects

Testing Strategy

Test Type Purpose Dependencies Speed
Entity tests Verify business rules None Milliseconds
Use case tests Verify orchestration Test doubles Milliseconds
Repository tests Verify persistence In-memory or database Seconds
Integration tests Verify complete flows Real implementations Seconds
System tests Verify end-to-end scenarios Full stack Minutes

Dependency Injection Patterns

Pattern Implementation When to Use
Constructor injection Pass dependencies to initialize Most use cases and adapters
Setter injection Provide setter methods Optional dependencies
Container-based Use DI container like dry-container Complex dependency graphs
Factory pattern Factory creates with dependencies Multiple implementation choices

Error Handling Strategies

Strategy Use Case Implementation
Result object Return success/failure indicator Return hash with success and data/error keys
Exception raising Unrecoverable errors Raise custom exception classes
Null object Handle missing data gracefully Return null object instead of nil
Validation object Collect multiple errors Return validation result with error collection

Architecture Decision Criteria

Consider Clean Architecture When Consider Simpler Architecture When
Complex business logic Simple CRUD operations
Multiple delivery mechanisms Single interface type
Long-term maintenance expected Short-lived prototype
Team experienced with pattern Small team learning basics
Frequent requirement changes Stable, well-understood domain
Critical business application Internal utility tool