CrackedRuby CrackedRuby

Overview

Repository architecture defines how source code organizes across version control systems. Two primary approaches exist: monorepo (monolithic repository) stores all project code in a single repository, while polyrepo (multi-repository) distributes code across multiple independent repositories.

A monorepo contains source code for multiple projects, services, libraries, and applications within one version control repository. Google, Facebook, and Microsoft use monorepos containing millions of lines of code across thousands of projects. The repository structure includes shared libraries, multiple applications, common tooling, and infrastructure code all versioned together.

A polyrepo architecture splits each project, service, or library into separate repositories. Each repository maintains independent versioning, dependencies, and release cycles. This approach mirrors the traditional one-repository-per-project model that dominated before monorepo adoption increased.

The choice between these approaches affects team collaboration, dependency management, code reuse, build systems, testing strategies, and deployment pipelines. Neither approach is universally superior; the decision depends on team size, project relationships, organizational structure, and technical requirements.

Ruby applications commonly use polyrepo architectures, with separate repositories for Rails applications, gems, and service components. However, Ruby projects increasingly adopt monorepo patterns, particularly for microservice architectures and companies managing multiple related applications.

Key Principles

Repository Scope and Boundaries

Monorepos eliminate repository boundaries by containing all related code in one location. Developers access any code through a single clone operation. Dependencies between projects exist as internal references rather than external package dependencies. Code changes affecting multiple projects occur in single commits with atomic guarantees.

Polyrepos establish strict boundaries between projects. Each repository defines clear ownership, access control, and versioning policies. Projects depend on each other through published packages or explicit version references. Changes spanning multiple repositories require coordinated commits across repositories.

Dependency Management Models

In monorepos, dependencies exist at the source level. When Project A depends on Library B, Project A references Library B's source code directly within the same repository. Dependency versions stay synchronized automatically since both exist at the same repository commit. Breaking changes in Library B immediately affect all dependent projects, forcing compatibility maintenance.

Polyrepos manage dependencies through version declarations. Project A specifies Library B's version in its dependency manifest (Gemfile for Ruby). Library B publishes versioned releases that Project A consumes. Projects can use different versions of shared dependencies, allowing gradual upgrades but creating version fragmentation.

Versioning and Release Management

Monorepos often use trunk-based development with continuous integration. The main branch represents the current state of all projects. Individual projects may tag releases at specific commits, but the repository itself doesn't version. Deployment strategies extract specific project artifacts from the monorepo state.

Polyrepos version each repository independently. Each project follows its own release cycle, version numbering scheme, and branching strategy. Projects release asynchronously without coordinating with other repositories. This independence simplifies individual project releases but complicates cross-repository features.

Code Sharing Mechanisms

Monorepos share code through direct file imports and module references. Shared utilities, common libraries, and reusable components exist in designated directories. All projects access these shared resources without publication steps. Refactoring shared code affects all consumers immediately and visibly.

# Monorepo structure
# /libs/auth/authentication.rb
module Libs
  module Auth
    class Authenticator
      def authenticate(token)
        # Shared authentication logic
      end
    end
  end
end

# /services/api/app.rb
require_relative '../../libs/auth/authentication'

class ApiApp
  def initialize
    @auth = Libs::Auth::Authenticator.new
  end
end

Polyrepos share code through published packages. Shared code exists in separate gem repositories. Teams publish gems to package registries (RubyGems.org or private gem servers). Consumer projects declare gem dependencies in Gemfiles. Updates require publishing new gem versions and updating consumer dependencies.

# Separate gem repository: auth-lib
# lib/auth_lib/authenticator.rb
module AuthLib
  class Authenticator
    def authenticate(token)
      # Shared authentication logic
    end
  end
end

# Consumer repository: api-service
# Gemfile
gem 'auth-lib', '~> 2.1'

# app.rb
require 'auth_lib'

class ApiApp
  def initialize
    @auth = AuthLib::Authenticator.new
  end
end

Build and Test Coordination

Monorepos require build systems that understand project relationships. Build tools determine which projects changed and which dependent projects require rebuilding. Test suites run across multiple projects, detecting integration issues immediately. Continuous integration builds the entire repository or affected subset.

Polyrepos build each repository independently. Build configuration exists within each repository. Tests run in isolation without dependencies on other repository changes. CI pipelines focus on single repository validation. Integration testing requires coordinating multiple repository states.

Design Considerations

Team Size and Structure

Small teams (under 10 developers) manage polyrepos effectively with standard Git workflows. Each repository remains comprehensible, and coordination overhead stays minimal. Teams handle cross-repository changes through communication and sequential updates.

Medium teams (10-50 developers) face increasing coordination challenges with polyrepos. Multiple repositories require more overhead for dependency updates, shared code distribution, and cross-cutting changes. Monorepos reduce coordination by providing single source of truth and atomic commits.

Large teams (50+ developers) benefit significantly from monorepo tooling. Code search across all projects becomes critical. Refactoring tools that update all usages reduce breaking change risk. Large polyrepo setups require extensive automation for dependency management and version coordination.

Project Relationships and Coupling

Tightly coupled projects sharing significant code benefit from monorepos. When changes in one project frequently require changes in others, atomic commits prevent inconsistent states. Shared libraries used across many projects avoid version fragmentation issues.

Loosely coupled projects with minimal dependencies suit polyrepos. Independent services with well-defined APIs release independently without coordination. Each project maintains autonomy in technology choices, versioning, and release schedules.

Projects with hierarchical dependencies (platform with plugins, framework with applications) work in either architecture. Monorepos simplify plugin development with direct framework access. Polyrepos enforce clear API boundaries and version contracts.

Code Review and Collaboration

Monorepos enable cross-project code review. Developers see changes affecting multiple projects in single pull requests. Reviewers verify compatibility across project boundaries. Large changes spanning multiple projects maintain coherence.

Polyrepos limit code review scope to single repositories. Reviews focus on specific project changes. Cross-repository coordination happens through multiple pull requests requiring synchronization. Reviewers may miss broader implications of changes.

Deployment Independence

Polyrepos excel when projects require independent deployment schedules. Each repository deploys without affecting others. Deployment pipelines remain simple and repository-specific. Rollback affects only individual repositories.

Monorepos require deployment tooling that extracts specific projects. Deployment pipelines must identify which projects changed and need deployment. Multiple projects may deploy from single commits. Rollback strategies need project-level granularity despite repository-level versioning.

Technology Diversity

Polyrepos accommodate diverse technology stacks naturally. Each repository chooses appropriate languages, frameworks, and tools. Build systems differ across repositories without conflict. Teams adopt new technologies independently.

Monorepos with diverse technologies require sophisticated build systems. Bazel, Buck, or Pants handle multi-language builds. Ruby projects alongside JavaScript, Python, or Go complicate tooling. Build configuration grows complex managing multiple ecosystems.

For Ruby-specific monorepos, homogeneity simplifies tooling:

# Monorepo Rakefile handling multiple Ruby projects
namespace :test do
  desc 'Run all project tests'
  task :all do
    projects = Dir['services/*'].select { |f| File.directory?(f) }
    projects.each do |project|
      Dir.chdir(project) do
        puts "Testing #{project}..."
        sh 'bundle exec rspec'
      end
    end
  end
end

namespace :lint do
  desc 'Run RuboCop across all projects'
  task :all do
    sh 'bundle exec rubocop services/**/*.rb libs/**/*.rb'
  end
end

Access Control and Security

Polyrepos provide repository-level access control. Each repository grants permissions independently. Teams restrict access to sensitive code repositories. Fine-grained permissions protect proprietary components.

Monorepos grant repository-wide access or require path-based access control. Git submodules or sparse checkout partially address this. Some organizations split monorepos when security boundaries require separation. Service-based access control operates at the VCS level rather than repository structure.

Implementation Approaches

Monorepo Organizational Strategies

Directory-based organization groups projects by type or function:

monorepo/
├── services/
│   ├── api/
│   ├── web/
│   └── worker/
├── libs/
│   ├── auth/
│   ├── database/
│   └── models/
├── tools/
│   ├── deploy/
│   └── generators/
└── shared/
    ├── config/
    └── scripts/

Each service directory contains a complete Ruby application with its own Gemfile, configuration, and code structure. Shared libraries in libs/ provide common functionality across services. Tools directory holds development and deployment utilities.

Namespace-based organization mirrors package structures:

# libs/payment/processor.rb
module Payment
  class Processor
    def charge(amount, token)
      # Process payment
    end
  end
end

# libs/payment/refund.rb
module Payment
  class Refund
    def process(transaction_id)
      # Process refund
    end
  end
end

# services/checkout/app.rb
require_relative '../../libs/payment/processor'

class CheckoutService
  def initialize
    @payment = Payment::Processor.new
  end
end

Gem-style organization treats internal libraries as internal gems:

# Gemfile at monorepo root
source 'https://rubygems.org'

# Internal gems from subdirectories
gem 'payment-lib', path: 'libs/payment'
gem 'auth-lib', path: 'libs/auth'

# External dependencies
gem 'rails', '~> 7.0'
gem 'pg', '~> 1.4'

Services declare dependencies on internal libraries through Gemfile paths. This approach combines monorepo advantages with familiar gem workflows.

Polyrepo Organizational Strategies

One-repository-per-service creates independent repositories for each service or application. Each repository contains complete application code, configuration, and deployment scripts. Services communicate through APIs and consume shared libraries as gem dependencies.

Library extraction pattern identifies shared code and extracts it into separate gem repositories. Common functionality becomes versioned gems published to package registries. Applications depend on specific gem versions through Gemfile declarations.

# auth-gem repository
# auth.gemspec
Gem::Specification.new do |spec|
  spec.name = 'company-auth'
  spec.version = '2.1.0'
  spec.summary = 'Authentication library'
  spec.files = Dir['lib/**/*.rb']
  spec.add_dependency 'jwt', '~> 2.7'
end

# Application repository
# Gemfile
source 'https://rubygems.org'
gem 'company-auth', '~> 2.1'

Workspace-based polyrepo uses tools like Git submodules or meta-repositories to coordinate multiple repositories. A meta-repository references specific commits from service repositories. Developers clone the meta-repository to obtain consistent versions of all services.

Hybrid Approaches

Some organizations use domain-based monorepos: separate monorepos for distinct domains or business units. Each domain monorepo contains related services and libraries. Domains interact through published contracts and shared infrastructure.

# payments-monorepo/services/processor/
# checkout-monorepo/services/web/

# checkout service consuming payment APIs
class CheckoutController
  def initialize
    @payment_api = PaymentApiClient.new(
      base_url: ENV['PAYMENT_SERVICE_URL']
    )
  end
  
  def complete_purchase
    response = @payment_api.charge(
      amount: cart.total,
      token: params[:payment_token]
    )
  end
end

Migration Strategies

Converting from polyrepo to monorepo requires consolidating repositories while preserving history:

# Create new monorepo
mkdir monorepo && cd monorepo
git init

# Import first repository preserving history
git remote add service1 ../service1-repo
git fetch service1
git merge --allow-unrelated-histories service1/main
mkdir -p services/service1
git mv * services/service1/
git commit -m "Import service1"

# Import additional repositories
git remote add service2 ../service2-repo
git fetch service2
git merge --allow-unrelated-histories service2/main
mkdir -p services/service2
git mv * services/service2/
git commit -m "Import service2"

Converting from monorepo to polyrepo extracts subdirectories with history:

# Extract service to new repository
git clone --no-local monorepo service1-repo
cd service1-repo
git filter-branch --subdirectory-filter services/service1 -- --all
git remote remove origin
git remote add origin git@github.com:company/service1.git

Tools & Ecosystem

Monorepo Build Tools

Rake-based coordination manages Ruby monorepo builds with custom Rake tasks:

# Rakefile
require 'rake/testtask'

SERVICES = FileList['services/*'].select { |f| File.directory?(f) }
LIBS = FileList['libs/*'].select { |f| File.directory?(f) }

namespace :test do
  SERVICES.each do |service|
    service_name = File.basename(service)
    namespace service_name.to_sym do
      Rake::TestTask.new do |t|
        t.libs << "#{service}/lib"
        t.test_files = FileList["#{service}/test/**/*_test.rb"]
      end
    end
  end

  desc 'Run all tests'
  task :all do
    SERVICES.each do |service|
      Rake::Task["test:#{File.basename(service)}"].invoke
    end
  end
end

namespace :lint do
  desc 'Run RuboCop on all code'
  task :rubocop do
    sh 'bundle exec rubocop --parallel'
  end
end

namespace :bundle do
  desc 'Install dependencies for all services'
  task :install do
    SERVICES.each do |service|
      Dir.chdir(service) { sh 'bundle install' }
    end
  end
end

Bazel provides language-agnostic build orchestration supporting Ruby through rules_ruby:

# services/api/BUILD
ruby_library(
    name = "api_lib",
    srcs = glob(["lib/**/*.rb"]),
    deps = [
        "//libs/auth:auth_lib",
        "//libs/database:database_lib",
    ],
)

ruby_binary(
    name = "api_server",
    srcs = ["app.rb"],
    deps = [":api_lib"],
)

Pants offers Python and Ruby support with dependency inference:

# services/api/BUILD
ruby_sources(
    name="lib",
    dependencies=[
        "libs/auth",
        "libs/database",
    ],
)

pex_binary(
    name="server",
    entry_point="app.rb",
    dependencies=[":lib"],
)

Version Control Tools

Git sparse checkout allows cloning repository subsets:

git clone --filter=blob:none --sparse https://github.com/company/monorepo
cd monorepo
git sparse-checkout init --cone
git sparse-checkout set services/api libs/auth

Git worktrees enable multiple working directories from single clone:

# Main checkout
git worktree add ../monorepo-feature1 feature1-branch
git worktree add ../monorepo-feature2 feature2-branch

# Work in separate directories on different branches
cd ../monorepo-feature1  # Works on feature1-branch
cd ../monorepo-feature2  # Works on feature2-branch

Dependency Management Tools

Bundler workspaces manage multiple Gemfiles in monorepos:

# Root Gemfile
source 'https://rubygems.org'

# Shared development dependencies
group :development, :test do
  gem 'rspec', '~> 3.12'
  gem 'rubocop', '~> 1.50'
end

# Evaluate service Gemfiles
Dir['services/*/Gemfile'].each do |gemfile|
  eval_gemfile gemfile
end

Private gem servers support polyrepo gem sharing:

# Gemfile
source 'https://rubygems.org'
source 'https://gems.company.com' do
  gem 'company-auth', '~> 2.1'
  gem 'company-models', '~> 1.5'
end

Gemfury and Artifactory host private Ruby gems with version management and access control.

CI/CD Integration

GitHub Actions monorepo workflow detects changed paths:

# .github/workflows/test.yml
name: Test
on: [push, pull_request]

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      api: ${{ steps.filter.outputs.api }}
      worker: ${{ steps.filter.outputs.worker }}
    steps:
      - uses: actions/checkout@v3
      - uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            api:
              - 'services/api/**'
              - 'libs/**'
            worker:
              - 'services/worker/**'
              - 'libs/**'

  test-api:
    needs: detect-changes
    if: needs.detect-changes.outputs.api == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.2
          bundler-cache: true
      - run: bundle exec rspec
        working-directory: services/api

Polyrepo CI configuration remains repository-specific:

# .github/workflows/test.yml in each repository
name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.2
          bundler-cache: true
      - run: bundle exec rspec
      - run: bundle exec rubocop

Practical Examples

Ruby Monorepo: Multi-Service Application

A company builds an e-commerce platform with API, web frontend, and background workers sharing authentication, database models, and business logic:

ecommerce-monorepo/
├── Gemfile
├── services/
│   ├── api/
│   │   ├── Gemfile
│   │   ├── app.rb
│   │   └── config.ru
│   ├── web/
│   │   ├── Gemfile
│   │   ├── app/
│   │   └── config/
│   └── worker/
│       ├── Gemfile
│       └── workers/
└── libs/
    ├── auth/
    │   └── lib/
    ├── models/
    │   └── lib/
    └── payment/
        └── lib/

Shared authentication library:

# libs/auth/lib/auth/authenticator.rb
module Auth
  class Authenticator
    def initialize(secret:)
      @secret = secret
    end

    def generate_token(user_id)
      payload = { user_id: user_id, exp: Time.now.to_i + 3600 }
      JWT.encode(payload, @secret, 'HS256')
    end

    def verify_token(token)
      JWT.decode(token, @secret, true, algorithm: 'HS256')
    rescue JWT::DecodeError
      nil
    end
  end
end

API service consuming shared library:

# services/api/app.rb
require 'sinatra/base'
require_relative '../../libs/auth/lib/auth/authenticator'
require_relative '../../libs/models/lib/models/user'

class ApiApp < Sinatra::Base
  def initialize
    super
    @auth = Auth::Authenticator.new(secret: ENV['JWT_SECRET'])
  end

  post '/login' do
    user = Models::User.find_by(email: params[:email])
    if user&.authenticate(params[:password])
      token = @auth.generate_token(user.id)
      json(token: token)
    else
      halt 401, json(error: 'Invalid credentials')
    end
  end

  get '/profile' do
    token = request.env['HTTP_AUTHORIZATION']&.sub(/^Bearer /, '')
    payload = @auth.verify_token(token)
    halt 401, json(error: 'Unauthorized') unless payload

    user = Models::User.find(payload['user_id'])
    json(user.to_hash)
  end
end

Worker service using same authentication:

# services/worker/workers/email_worker.rb
require_relative '../../../libs/auth/lib/auth/authenticator'
require_relative '../../../libs/models/lib/models/user'

class EmailWorker
  def initialize
    @auth = Auth::Authenticator.new(secret: ENV['JWT_SECRET'])
  end

  def perform(user_id)
    user = Models::User.find(user_id)
    token = @auth.generate_token(user.id)
    
    EmailService.send(
      to: user.email,
      subject: 'Magic Link',
      body: "Click here: https://example.com/verify?token=#{token}"
    )
  end
end

Root Gemfile coordinates dependencies:

# Gemfile
source 'https://rubygems.org'

# Shared dependencies
gem 'jwt', '~> 2.7'
gem 'pg', '~> 1.4'

# Development tools
group :development, :test do
  gem 'rspec', '~> 3.12'
  gem 'rubocop', '~> 1.50'
end

# Include service Gemfiles
Dir['services/*/Gemfile'].each { |f| eval_gemfile(f) }

Ruby Polyrepo: Microservices Architecture

The same e-commerce platform split into separate repositories with published gems:

Authentication gem repository:

# auth-gem/lib/company_auth.rb
require 'jwt'

module CompanyAuth
  class Authenticator
    def initialize(secret:)
      @secret = secret
    end

    def generate_token(user_id)
      payload = { user_id: user_id, exp: Time.now.to_i + 3600 }
      JWT.encode(payload, @secret, 'HS256')
    end

    def verify_token(token)
      JWT.decode(token, @secret, true, algorithm: 'HS256')
    rescue JWT::DecodeError
      nil
    end
  end
end

# auth-gem/company_auth.gemspec
Gem::Specification.new do |spec|
  spec.name = 'company-auth'
  spec.version = '1.2.0'
  spec.authors = ['Engineering Team']
  spec.summary = 'Company authentication library'
  
  spec.files = Dir['lib/**/*.rb']
  spec.require_paths = ['lib']
  
  spec.add_dependency 'jwt', '~> 2.7'
end

API service repository consuming gem:

# api-service/Gemfile
source 'https://rubygems.org'
source 'https://gems.company.com' do
  gem 'company-auth', '~> 1.2'
  gem 'company-models', '~> 2.0'
end

gem 'sinatra', '~> 3.0'
gem 'pg', '~> 1.4'

# api-service/app.rb
require 'sinatra/base'
require 'company_auth'
require 'company_models'

class ApiApp < Sinatra::Base
  def initialize
    super
    @auth = CompanyAuth::Authenticator.new(secret: ENV['JWT_SECRET'])
  end

  post '/login' do
    user = CompanyModels::User.find_by(email: params[:email])
    if user&.authenticate(params[:password])
      token = @auth.generate_token(user.id)
      json(token: token)
    else
      halt 401, json(error: 'Invalid credentials')
    end
  end
end

Updating shared authentication requires versioned release:

# In auth-gem repository
# 1. Update code
# 2. Bump version in gemspec
# 3. Build and publish gem
gem build company_auth.gemspec
gem push company-auth-1.3.0.gem --host https://gems.company.com

# In api-service repository
# 4. Update Gemfile
# Gemfile
gem 'company-auth', '~> 1.3'

# 5. Update dependencies
bundle update company-auth

# 6. Test and deploy
bundle exec rspec
git commit -am "Update company-auth to 1.3.0"

Monorepo Atomic Refactoring

Refactoring authentication to add two-factor support affects multiple services atomically:

# libs/auth/lib/auth/authenticator.rb
module Auth
  class Authenticator
    # Add two-factor authentication
    def generate_token(user_id, two_factor_verified: false)
      payload = {
        user_id: user_id,
        two_factor: two_factor_verified,
        exp: Time.now.to_i + 3600
      }
      JWT.encode(payload, @secret, 'HS256')
    end

    def verify_token(token, require_two_factor: false)
      payload = JWT.decode(token, @secret, true, algorithm: 'HS256')
      return nil if require_two_factor && !payload['two_factor']
      payload
    rescue JWT::DecodeError
      nil
    end
  end
end

# services/api/app.rb - Updated in same commit
post '/login' do
  user = Models::User.find_by(email: params[:email])
  if user&.authenticate(params[:password])
    two_factor = verify_two_factor(user, params[:code])
    token = @auth.generate_token(user.id, two_factor_verified: two_factor)
    json(token: token)
  else
    halt 401
  end
end

# services/worker/workers/email_worker.rb - Updated in same commit
def perform(user_id)
  user = Models::User.find(user_id)
  # Updated call with explicit parameter
  token = @auth.generate_token(user.id, two_factor_verified: false)
  EmailService.send(to: user.email, token: token)
end

Single commit updates library and all consumers, ensuring consistency. Pull request shows complete change scope. Tests verify all services together.

Common Pitfalls

Monorepo Pitfalls

Accidental coupling occurs when developers reference internal code without considering boundaries:

# Bad: Direct coupling between unrelated services
# services/checkout/order_processor.rb
require_relative '../inventory/stock_checker'

class OrderProcessor
  def process(order)
    # Checkout service now depends on inventory implementation details
    checker = InventoryService::StockChecker.new
    checker.verify_availability(order.items)
  end
end

# Good: Use defined interfaces
# services/checkout/order_processor.rb
class OrderProcessor
  def initialize(inventory_client:)
    @inventory = inventory_client
  end

  def process(order)
    # Depend on interface, not implementation
    @inventory.check_availability(order.items)
  end
end

Build performance degradation happens without proper caching and change detection:

# Bad: Rebuild everything always
task :test do
  sh 'bundle exec rspec services/**/spec'
  sh 'bundle exec rspec libs/**/spec'
end

# Good: Detect changes and test affected code
task :test do
  changed_files = `git diff --name-only HEAD~1`.split("\n")
  affected_services = changed_files
    .select { |f| f.start_with?('services/') }
    .map { |f| f.split('/')[1] }
    .uniq

  affected_services.each do |service|
    sh "cd services/#{service} && bundle exec rspec"
  end
end

Merge conflicts increase with more developers modifying shared code:

# Multiple developers modifying same shared constant
# libs/constants/countries.rb
module Constants
  COUNTRIES = {
    'US' => 'United States',
    'CA' => 'Canada',
    # Developer A adds
    'GB' => 'United Kingdom',
    # Developer B adds (conflict)
    'DE' => 'Germany'
  }
end

# Better: Use database or configuration files for data
# config/countries.yml
countries:
  US: United States
  CA: Canada
  GB: United Kingdom
  DE: Germany

Polyrepo Pitfalls

Version fragmentation creates incompatible dependency versions:

# service-a/Gemfile
gem 'company-models', '~> 1.0'

# service-b/Gemfile
gem 'company-models', '~> 2.0'

# company-models 2.0 introduces breaking changes
# Service A and Service B now expect different data structures
# Runtime errors occur when services communicate

Coordinating breaking changes requires multiple repository updates:

# Step 1: Update shared gem with deprecated methods
# company-models 1.5.0
class User
  def name
    warn 'User#name deprecated, use full_name'
    full_name
  end

  def full_name
    "#{first_name} #{last_name}"
  end
end

# Step 2: Update all consuming services
# service-a, service-b, service-c repositories
user.full_name  # Replace user.name calls

# Step 3: Remove deprecated method
# company-models 2.0.0
class User
  def full_name
    "#{first_name} #{last_name}"
  end
  # name method removed
end

Circular dependencies between repositories create deadlock:

# auth-gem depends on user-models-gem
# auth-gem/company_auth.gemspec
spec.add_dependency 'company-user-models', '~> 1.0'

# user-models-gem depends on auth-gem
# user-models-gem/company_user_models.gemspec
spec.add_dependency 'company-auth', '~> 1.0'

# Cannot update either gem without breaking the other
# Solution: Extract shared code to third gem or merge repositories

Inconsistent tooling configuration across repositories:

# service-a/.rubocop.yml
AllCops:
  TargetRubyVersion: 3.1
  NewCops: enable

# service-b/.rubocop.yml
AllCops:
  TargetRubyVersion: 3.0
  NewCops: disable

# Developers see different linting results
# Solution: Share configuration via gem or template

Dependency update overhead multiplies across repositories:

# Security update requires updating 15 service repositories
for repo in service-*; do
  cd $repo
  bundle update rails
  bundle exec rspec
  git commit -am "Update Rails to 7.0.5"
  git push
  cd ..
done

# Monorepo: Single update, single commit, single CI run

Lost context in code search when functionality spans repositories:

# Finding all uses of authentication requires searching multiple repositories
# auth-gem/lib/company_auth.rb
def authenticate(token)
  # Implementation
end

# Must search service-a, service-b, service-c, etc.
# to find all authentication calls

# Monorepo: grep -r "authenticate" shows all usages

Reference

Repository Architecture Comparison

Aspect Monorepo Polyrepo
Code Location Single repository Multiple repositories
Dependency Management Source-level references Version declarations
Code Sharing Direct imports Published packages
Versioning Repository-wide commits Per-repository versions
Build Coordination Cross-project build tools Independent builds
Refactoring Atomic across projects Multi-repository coordination
Access Control Path-based or repository-wide Repository-level
Clone Size Large single clone Small multiple clones
CI Complexity Change detection required Per-repository pipelines

Ruby Monorepo Structure Patterns

Pattern Structure Use Case
Directory Organization services/, libs/, tools/ Clear separation by type
Namespace Organization Mirrors module structure Emphasize code relationships
Gem-Style Organization Path-based Gemfile references Familiar gem workflow
Domain Organization Grouped by business domain Large complex systems

Polyrepo Gem Management

Approach Implementation Trade-offs
Public RubyGems gem push to rubygems.org Simple, no infrastructure
Private Gem Server Gemfury, Artifactory Control, security, cost
Git Dependencies Gemfile git references No publishing step, version control harder
Path Dependencies Local filesystem paths Development only, not production

Monorepo Build Tool Comparison

Tool Language Support Learning Curve Ruby Integration
Rake Ruby-focused Low Native
Bazel Multi-language High rules_ruby
Pants Python, Java, Go Medium ruby_sources
Buck Multi-language High Limited
Lerna JavaScript Medium Not applicable

Common Rake Tasks for Monorepos

Task Pattern Purpose Example
namespace :test Run tests test:all, test:api, test:worker
namespace :lint Code quality lint:rubocop, lint:all
namespace :bundle Dependency management bundle:install, bundle:update
namespace :deploy Deployment deploy:api, deploy:worker
namespace :db Database operations db:migrate:all

Git Workflow Patterns

Pattern Monorepo Implementation Polyrepo Implementation
Feature Branches Single branch, all changes Multiple branches across repos
Pull Requests One PR with all changes Coordinated PRs per repo
Code Review Cross-project visibility Repository-scoped reviews
Merge Strategy Atomic commits Sequential merges
Rollback Repository-level revert Per-repository reverts

Migration Considerations

Migration Type Complexity Key Challenges
Polyrepo to Monorepo Medium History preservation, path reorganization
Monorepo to Polyrepo High Splitting history, dependency extraction
Partial Migration Very High Maintaining both architectures
Hybrid Approach High Coordination between architectures

Decision Matrix

Consider Monorepo When Consider Polyrepo When
Frequent cross-project changes Independent service deployment
Shared code dominates Minimal code sharing
Atomic commits critical Repository autonomy important
Team collaborates closely Teams operate independently
Tooling supports large repos Standard Git workflows sufficient
Code visibility valued Access control granularity needed
Refactoring across projects common Technology diversity required