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 |